diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d804082a2d2e700710ae840d8712a2ffe23ff762
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,62 @@
+version: '3'
+services:
+ nacos:
+ container_name: nacos
+ image: nacos/nacos-server:latest
+ restart: always
+ environment:
+ PREFER_HOST_MODE: hostname
+ MODE: standalone
+ ports:
+ - '8848:8848'
+ redis:
+ container_name: redis
+ image: redis:latest
+ restart: always
+ expose:
+ - 6379
+ rabbitmq:
+ container_name: rabbitmq
+ image: rabbitmq:3-management
+ restart: always
+ expose:
+ - 5672
+ ports:
+ - '15672:15672'
+ gateway:
+ container_name: gateway
+ image: seqdata-cloud-gateway
+ depends_on:
+ - nacos
+ ports:
+ - '8080:8080'
+ environment:
+ - server.port=8080
+ - spring.cloud.nacos.config.server-addr=nacos:8848
+ - spring.cloud.nacos.discovery.server-addr=nacos:8848
+ - security.oauth2.resource.token-info-uri=http://authz/oauth/check_token
+ authz:
+ container_name: authz
+ image: seqdata-cloud-authz
+ depends_on:
+ - nacos
+ - redis
+ expose:
+ - 8080
+ environment:
+ - server.port=8080
+ - spring.redis.host=redis
+ - spring.cloud.nacos.config.server-addr=nacos:8848
+ - spring.cloud.nacos.discovery.server-addr=nacos:8848
+ authc:
+ container_name: authc
+ image: seqdata-cloud-authc
+ depends_on:
+ - nacos
+ expose:
+ - 8080
+ environment:
+ - server.port=8080
+ - spring.cloud.nacos.config.server-addr=nacos:8848
+ - spring.cloud.nacos.discovery.server-addr=nacos:8848
+ - security.oauth2.resource.token-info-uri=http://authz/oauth/check_token
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..384f4aa60b8a2cd47e76f3ed5749c45725e292a9
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,146 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.1.RELEASE
+
+ cn.seqdata.cloud
+ seqdata-cloud-parent
+ 2.2.1-SNAPSHOT
+ pom
+
+ 1.8
+ Hoxton.SR1
+ 2.9.2
+ 2.2.1.RELEASE
+
+
+ seqdata-cloud-gateway
+ seqdata-cloud-authz
+ seqdata-cloud-authc
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.24
+ compile
+ true
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+ ${nacos.version}
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+ ${nacos.version}
+
+
+
+
+ io.springfox
+ springfox-swagger2
+ ${swagger.version}
+
+
+ io.springfox
+ springfox-swagger-ui
+ ${swagger.version}
+
+
+
+
+
+ org.springframework.cloud
+ spring-cloud-dependencies
+ ${spring-cloud.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ com.spotify
+ docker-maven-plugin
+ 1.2.2
+
+
+ docker-build
+ install
+
+ build
+
+
+
+ docker-tag
+ deploy
+
+ tag
+
+
+ ${project.artifactId}
+
+ swr.cn-north-1.myhuaweicloud.com/seqdata/${project.artifactId}
+
+
+
+
+ docker-push
+ deploy
+
+ push
+
+
+
+ swr.cn-north-1.myhuaweicloud.com/seqdata/${project.artifactId}
+
+
+
+
+
+ ${project.artifactId}
+ openjdk:8-jdk-alpine
+ /
+
+ ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
+ echo 'Asia/Shanghai' > /etc/timezone
+
+ ["java","-jar","/${project.build.finalName}.jar"]
+
+
+
+ /
+ ${project.build.directory}
+ ${project.build.finalName}.jar
+
+
+
+ http://docker.voltmao.com:2375
+
+
+
+
+
diff --git a/seqdata-cloud-authc/pom.xml b/seqdata-cloud-authc/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..1cab73c76935c6c61f08c9a2ee883aadea41e24d
--- /dev/null
+++ b/seqdata-cloud-authc/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 2.2.1.RELEASE
+
+ seqdata-cloud-authc
+ 2.2.1-SNAPSHOT
+
+
+ joda-time
+ joda-time
+
+
+ org.springframework.data
+ spring-data-jpa
+
+
+ cn.seqdata.cloud
+ seqdata-cloud-starter
+ ${project.version}
+
+
+ javax.persistence
+ javax.persistence-api
+ 2.2
+
+
+
diff --git a/seqdata-cloud-authc/src/main/docker/Dockerfile b/seqdata-cloud-authc/src/main/docker/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..61caeee2a4d2277e549c4a544182cff4e83e38a4
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/docker/Dockerfile
@@ -0,0 +1,6 @@
+FROM openjdk:8-jdk-alpine
+VOLUME /tmp
+ADD seqdata-cloud-authc-2.2.1-SNAPSHOT.jar myapp.jar
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","-Dspring.application.name=authc","/myapp.jar"]
\ No newline at end of file
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/AuthcServerApplication.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/AuthcServerApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..d0ad91eb1c0b3f5f0aaaee0a3167281555c76f46
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/AuthcServerApplication.java
@@ -0,0 +1,21 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+
+import cn.seqdata.syslog.EnableSyslog;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-20 00:37
+ */
+@SpringBootApplication
+@EnableDiscoveryClient
+@EnableSyslog
+public class AuthcServerApplication {
+
+ public static void main(String[] args) {
+ new SpringApplication(AuthcServerApplication.class).run(args);
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JavaTimeController.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JavaTimeController.java
new file mode 100644
index 0000000000000000000000000000000000000000..ab7629e33b6edb470e02713309aa4a6574f411d3
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JavaTimeController.java
@@ -0,0 +1,49 @@
+package cn.seqdata.oauth2.controller;
+
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.security.PermitAll;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author jrxian
+ * @date 2020/11/28 19:42
+ */
+@PermitAll
+@RestController
+@RequestMapping("/javatime")
+public class JavaTimeController {
+
+ @GetMapping
+ public Map jdk8() {
+ Map values = new HashMap<>();
+ values.put("date", LocalDate.now());
+ values.put("time", LocalTime.now());
+ values.put("instant", Instant.now());
+ values.put("localdatetime", LocalDateTime.now());
+ return values;
+ }
+
+ @GetMapping("/date")
+ public LocalDate date(@RequestParam(required = false) LocalDate key) {
+ return key;
+ }
+
+ @GetMapping("/time")
+ public LocalTime time(@RequestParam(required = false) LocalTime key) {
+ return key;
+ }
+
+ @GetMapping("/localdatetime")
+ public LocalDateTime localdatetime(@RequestParam(required = false) LocalDateTime key) {
+ return key;
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JodaController.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JodaController.java
new file mode 100644
index 0000000000000000000000000000000000000000..08836cc64ff4c82e45ed6505d2fc823089b44d4f
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/JodaController.java
@@ -0,0 +1,41 @@
+package cn.seqdata.oauth2.controller;
+
+import javax.annotation.security.PermitAll;
+
+import org.joda.time.DateTime;
+import org.joda.time.LocalDate;
+import org.joda.time.LocalDateTime;
+import org.joda.time.LocalTime;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author jrxian
+ * @date 2020-08-20 16:42
+ */
+@PermitAll
+@RestController
+@RequestMapping("/joda")
+public class JodaController {
+ @GetMapping("/date")
+ public LocalDate date(@RequestParam(required = false) LocalDate key) {
+ return key;
+ }
+
+ @GetMapping("/time")
+ public LocalTime time(@RequestParam(required = false) LocalTime key) {
+ return key;
+ }
+
+ @GetMapping("/datetime")
+ public DateTime datetime(@RequestParam(required = false) DateTime key) {
+ return key;
+ }
+
+ @GetMapping("/localdatetime")
+ public LocalDateTime localdatetime(@RequestParam(required = false) LocalDateTime key) {
+ return key;
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SpringDataController.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SpringDataController.java
new file mode 100644
index 0000000000000000000000000000000000000000..9a576e5dc40ebbb6d71c0377de932f68b8e3259e
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SpringDataController.java
@@ -0,0 +1,30 @@
+package cn.seqdata.oauth2.controller;
+
+import javax.annotation.security.PermitAll;
+
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.PageableDefault;
+import org.springframework.data.web.SortDefault;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author jrxian
+ * @date 2020-08-20 16:43
+ */
+@PermitAll
+@RestController
+@RequestMapping("/spring/data")
+public class SpringDataController {
+ @GetMapping("/sort")
+ public Sort sort(String key, @SortDefault("id") Sort sort) {
+ return sort;
+ }
+
+ @GetMapping("/pageable")
+ public Pageable pageable(String key, @PageableDefault(sort = "id") Pageable pageable) {
+ return pageable;
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SyslogController.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SyslogController.java
new file mode 100644
index 0000000000000000000000000000000000000000..f2c49216fd349c322d436bfe3f7e152d30f6b07b
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/SyslogController.java
@@ -0,0 +1,33 @@
+package cn.seqdata.oauth2.controller;
+
+import javax.annotation.security.PermitAll;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+
+import cn.seqdata.oauth2.client.entity.NamedObject;
+import cn.seqdata.syslog.Syslog;
+
+/**
+ * @author jrxian
+ * @date 2020/12/12 15:50
+ */
+@Api("系统日志")
+@PermitAll
+@RestController
+@RequestMapping("/syslog")
+public class SyslogController {
+
+ @ApiOperation("获取数据")
+ @Syslog(value = "用户`#{details['name']}`登录#{errorCode==null?'成功':'失败'}")
+ @GetMapping("/{id}")
+ public long get(@PathVariable("id") long id) {
+ return id;
+ }
+
+ @ApiOperation("保存数据")
+ @PutMapping
+ public void put(NamedObject object) {
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/UserInfoController.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/UserInfoController.java
new file mode 100644
index 0000000000000000000000000000000000000000..c9e5c26bd820b71963d1495f3bf51a24e1c60d5e
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/controller/UserInfoController.java
@@ -0,0 +1,32 @@
+package cn.seqdata.oauth2.controller;
+
+import java.security.Principal;
+
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-27 17:16
+ */
+@RestController
+@RequestMapping("/current")
+public class UserInfoController {
+
+ @ApiOperation("通行证")
+ @GetMapping("/principal")
+ @PreAuthorize("isAuthenticated()")
+ public Principal principal(Principal principal) {
+ return principal;
+ }
+
+ @ApiOperation("用于测试行级权限是否生效,当输入的id是用户id时通过验证,否则报403")
+ @GetMapping("/permission")
+ @PreAuthorize("hasPermission(#id, null, 'read')")
+ public long permission(long id) {
+ return id;
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/EnhancePrincipalExtractor.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/EnhancePrincipalExtractor.java
new file mode 100644
index 0000000000000000000000000000000000000000..cb4a2ddbd84fcc5b404de6e4f6a834e62d48af0c
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/EnhancePrincipalExtractor.java
@@ -0,0 +1,27 @@
+package cn.seqdata.oauth2.security;
+
+import java.util.Map;
+
+import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
+import org.springframework.stereotype.Component;
+
+import cn.seqdata.oauth2.client.OAuth2Constants;
+
+/**
+ * Author: jrxian
+ * Date: 2020-02-14 08:47
+ */
+@Component
+public class EnhancePrincipalExtractor implements PrincipalExtractor, OAuth2Constants {
+ private static final String[] PRINCIPAL_KEYS = new String[]{USERNAME, MOBILE, EMAIL, ID};
+
+ @Override
+ public Object extractPrincipal(Map map) {
+ for(String key : PRINCIPAL_KEYS) {
+ if(map.containsKey(key)) {
+ return map.get(key);
+ }
+ }
+ return null;
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/TestPermissionEvaluator.java b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/TestPermissionEvaluator.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a4d18712fb1d86537df3de67d5a818a354f224b
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/java/cn/seqdata/oauth2/security/TestPermissionEvaluator.java
@@ -0,0 +1,32 @@
+package cn.seqdata.oauth2.security;
+
+import java.util.Objects;
+
+import org.springframework.stereotype.Component;
+
+import cn.seqdata.oauth2.client.domain.AuthUser;
+import cn.seqdata.oauth2.client.domain.RepoScope;
+import cn.seqdata.oauth2.client.security.EntityPermissionEvaluator;
+
+/**
+ * Author: jrxian
+ * Date: 2020-02-14 20:48
+ */
+@Component
+public class TestPermissionEvaluator implements EntityPermissionEvaluator {
+
+ @Override
+ public Class> entityType() {
+ return Object.class;
+ }
+
+ @Override
+ public long getId(Object entity) {
+ return 0;
+ }
+
+ @Override
+ public boolean hasPermission(AuthUser authUser, long entityId, RepoScope scope) {
+ return Objects.nonNull(authUser.getUserId()) && authUser.getUserId() == entityId && RepoScope.read.equals(scope);
+ }
+}
diff --git a/seqdata-cloud-authc/src/main/resources/application.yml b/seqdata-cloud-authc/src/main/resources/application.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a5ec962200e1d5af0f7f78d8c7aba78b979ada97
--- /dev/null
+++ b/seqdata-cloud-authc/src/main/resources/application.yml
@@ -0,0 +1,24 @@
+spring:
+ application:
+ name: authc
+ profiles:
+ active: logging, jackson
+ security:
+ user:
+ name: authc
+ password: authc@123
+ roles: anonymous
+security:
+ oauth2:
+ resource:
+ prefer-token-info: false
+ token-info-uri: http://authz/oauth/check_token
+ user-info-uri: http://authz/user/info
+ permit-all:
+ - /type/**
+ - /code/**
+ - /joda/**
+ - /jdk8/**
+ client:
+ client-id: client
+ client-secret: secret
diff --git a/seqdata-cloud-authz/pom.xml b/seqdata-cloud-authz/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..233259a0ee2afa5a8139b304a879282461dea9ec
--- /dev/null
+++ b/seqdata-cloud-authz/pom.xml
@@ -0,0 +1,117 @@
+
+
+ 4.0.0
+
+ cn.seqdata.cloud
+ seqdata-cloud-parent
+ 2.2.1-SNAPSHOT
+
+ seqdata-cloud-authz
+ 认证服务器,提供oauth2认证,接入第三方认证(如微信、钉钉)
+
+
+ org.springframework.boot
+ spring-boot-starter-amqp
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-configuration-processor
+ true
+
+
+
+ org.springframework.security
+ spring-security-oauth2-client
+
+
+ org.springframework.security.oauth.boot
+ spring-security-oauth2-autoconfigure
+
+
+
+
+
+ com.aliyun
+ aliyun-java-sdk-core
+ 4.5.2
+
+
+ com.aliyun
+ aliyun-java-sdk-dysmsapi
+ 2.1.0
+
+
+
+ cn.seqdata.cloud
+ seqdata-cloud-wechat
+ ${project.version}
+
+
+ cn.seqdata.cloud
+ seqdata-cloud-oauth2
+ ${project.version}
+
+
+ cn.seqdata.cloud
+ seqdata-cloud-syslog
+ ${project.version}
+
+
+
+ com.microsoft.sqlserver
+ mssql-jdbc
+ runtime
+
+
+ mysql
+ mysql-connector-java
+ runtime
+
+
+ com.oracle.ojdbc
+ ojdbc8
+ 19.3.0.0
+ runtime
+
+
+ com.oracle.database.nls
+ orai18n
+ 19.3.0.0
+ runtime
+
+
+
diff --git a/seqdata-cloud-authz/src/main/docker/Dockerfile b/seqdata-cloud-authz/src/main/docker/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..e99c6341eccbaf20d512bfa282b83346b451cae3
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/docker/Dockerfile
@@ -0,0 +1,6 @@
+FROM openjdk:8-jdk-alpine
+VOLUME /tmp
+ADD seqdata-cloud-authz-2.2.1-SNAPSHOT.jar myapp.jar
+ENV TZ=Asia/Shanghai
+RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo '$TZ' > /etc/timezone
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","-Dspring.application.name=authz","/myapp.jar"]
\ No newline at end of file
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunProperties.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d4d7c95f21ad066efbb876ee15f33e10ff62acc
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunProperties.java
@@ -0,0 +1,15 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author jrxian
+ * @date 2020-06-21 19:02
+ */
+@lombok.Getter
+@lombok.Setter
+@ConfigurationProperties("aliyun")
+public class AliyunProperties {
+ private String accessKey;
+ private String accessSecret;
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunSmsProperties.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunSmsProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..9b371f1f97f734cdfc8cc26ae8f2d313ac950b15
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AliyunSmsProperties.java
@@ -0,0 +1,17 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author jrxian
+ * @date 2020-06-21 19:01
+ */
+@lombok.Getter
+@lombok.Setter
+@ConfigurationProperties("aliyun.sms")
+public class AliyunSmsProperties {
+ private Boolean enabled;
+ private String regionId;
+ private String templateCode;
+ private String signName;
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthcServerConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthcServerConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e47b1d27f1c8f94d75a5bf31628586b574e8dbe
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthcServerConfiguration.java
@@ -0,0 +1,61 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationEventPublisher;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
+import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
+import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
+import org.springframework.security.web.access.channel.ChannelProcessingFilter;
+
+import cn.seqdata.oauth2.filter.AddressFilter;
+import cn.seqdata.oauth2.filter.AddressProperties;
+import cn.seqdata.oauth2.filter.PeriodFilter;
+import cn.seqdata.oauth2.filter.PeriodProperties;
+
+/**
+ * Author: jrxian
+ * Date: 2019-09-29 23:09
+ */
+@Configuration
+@EnableResourceServer
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+@EnableConfigurationProperties({AddressProperties.class, PeriodProperties.class})
+@lombok.RequiredArgsConstructor
+public class AuthcServerConfiguration extends ResourceServerConfigurerAdapter {
+ private final AddressProperties remoteAddrProperties;
+ private final PeriodProperties periodProperties;
+
+ @Override
+ public void configure(ResourceServerSecurityConfigurer resources) {
+ //禁用授权服务器中资源服务器的授权事件发布,会导致大量的内部登录被日志记录
+ resources.eventPublisher(new AuthenticationEventPublisher() {
+ @Override
+ public void publishAuthenticationSuccess(Authentication authentication) {
+ }
+
+ @Override
+ public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
+ }
+ });
+ }
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ http.cors();
+
+ http.addFilterAfter(new AddressFilter(remoteAddrProperties), ChannelProcessingFilter.class);
+ http.addFilterAfter(new PeriodFilter(periodProperties), ChannelProcessingFilter.class);
+
+ http.authorizeRequests()
+ .antMatchers("/csrf", "/oauth2/**", "/rbac/**", "/perm/**"
+ , "/oauth/code/**", "/connect/nonce", "/connect/v2/nonce", "/password/**", "/wxapp/**")
+ .permitAll();
+
+ super.configure(http);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthorizationServerConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthorizationServerConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..2f61f69c2897fe5c454b417045d166b94b6d37c2
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthorizationServerConfiguration.java
@@ -0,0 +1,116 @@
+package cn.seqdata.oauth2;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
+import org.springframework.security.config.annotation.ObjectPostProcessor;
+import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
+import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
+import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
+import org.springframework.security.oauth2.provider.ClientDetailsService;
+import org.springframework.security.oauth2.provider.CompositeTokenGranter;
+import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
+import org.springframework.security.oauth2.provider.TokenGranter;
+import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenGranter;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
+import org.springframework.security.oauth2.provider.code.AuthorizationCodeTokenGranter;
+import org.springframework.security.oauth2.provider.implicit.ImplicitTokenGranter;
+import org.springframework.security.oauth2.provider.refresh.RefreshTokenGranter;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+
+import cn.seqdata.crypto.exchanger.KeyExchanger;
+import cn.seqdata.oauth2.nonce.*;
+import cn.seqdata.oauth2.repos.oauth.ClientDetailRepo;
+import cn.seqdata.oauth2.service.JpaClientDetailsService;
+import cn.seqdata.oauth2.service.JpaUserDetailsManager;
+import lombok.AllArgsConstructor;
+
+/**
+ * Author: jrxian
+ * Date: 2019-10-02 19:11
+ */
+@Configuration
+@EnableAuthorizationServer
+@AllArgsConstructor
+public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
+ private final AuthenticationManager authenticationManager;
+ private final JpaUserDetailsManager userDetailsManager;
+ private final AuthorizationServerTokenServices tokenServices;
+ private final KeyExchanger keyExchanger;
+ private final TokenStore tokenStore;
+ private final TokenEnhancer tokenEnhancer;
+ private final ClientDetailRepo clientDetailRepo;
+ private final List nonceHandlers;
+ private final NonceUserService userService;
+
+ @Override
+ public void configure(AuthorizationServerSecurityConfigurer security) {
+ security
+ .tokenKeyAccess("isAuthenticated()")
+ .checkTokenAccess("isAuthenticated()")
+ .addObjectPostProcessor(new ObjectPostProcessor() {
+ @Override
+ public O postProcess(O provider) {
+ provider.setForcePrincipalAsString(true);
+ return provider;
+ }
+ });
+ }
+
+ @Override
+ public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
+ clients.withClientDetails(new JpaClientDetailsService(clientDetailRepo));
+ }
+
+ @Override
+ public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
+ endpoints
+ .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
+ .authenticationManager(authenticationManager)
+ .userDetailsService(userDetailsManager)
+ .tokenServices(tokenServices)
+ .tokenStore(tokenStore)
+ .tokenEnhancer(tokenEnhancer)
+ .tokenGranter(tokenGranter(endpoints));
+ }
+
+ /**
+ * 重写 AuthorizationServerEndpointsConfigurer.createDefaultTokenServices,添加 NonceTokenGranter
+ */
+ private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
+ ClientDetailsService clientDetails = endpoints.getClientDetailsService();
+ AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
+ AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
+ OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
+
+ List tokenGranters = new ArrayList<>();
+ tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails, requestFactory));
+ tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
+ tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory));
+ tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
+
+ //自定义的手机/邮箱验证码登录
+ //!!!必须在用户名密码验证前面
+ NonceAuthenticationProvider mobileNonceProvider = new NonceAuthenticationProvider(JpaUserDetailsManager::loadUserByMobile, userDetailsManager, nonceHandlers, userService);
+ tokenGranters.add(new NonceTokenGranter(mobileNonceProvider, tokenServices, clientDetails, requestFactory, OAuth2Constants.MOBILE));
+
+ NonceAuthenticationProvider emailNonceProvider = new NonceAuthenticationProvider(JpaUserDetailsManager::loadUserByEmail, userDetailsManager, nonceHandlers, userService);
+ tokenGranters.add(new NonceTokenGranter(emailNonceProvider, tokenServices, clientDetails, requestFactory, OAuth2Constants.EMAIL));
+
+ if(Objects.nonNull(authenticationManager)) {
+ //用户名密码验证,用authz.PasswordTokenGranter代替,需要把账号状态输出到前端
+ tokenGranters.add(new PasswordTokenGranter(keyExchanger, authenticationManager, tokenServices, clientDetails, requestFactory));
+ }
+
+ return new CompositeTokenGranter(tokenGranters);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerApplication.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerApplication.java
new file mode 100644
index 0000000000000000000000000000000000000000..986a5d3d537f244f84e26b898ff93018b0a46be0
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerApplication.java
@@ -0,0 +1,23 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+
+import cn.seqdata.wxapp.WxAppFeignClient;
+
+/**
+ * Author: jrxian
+ * Date: 2019-10-02 19:11
+ */
+@SpringBootApplication
+//@EnableCaching
+@EnableDiscoveryClient
+@EnableFeignClients(clients = WxAppFeignClient.class)
+public class AuthzServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AuthzServerApplication.class, args);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..4789a9177e480e242e58fc9b08007d88bba7f748
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/AuthzServerConfiguration.java
@@ -0,0 +1,97 @@
+package cn.seqdata.oauth2;
+
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.security.crypto.factory.PasswordEncoderFactories;
+import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.provider.ClientDetailsService;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+import org.springframework.security.oauth2.provider.token.TokenEnhancer;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+
+import cn.seqdata.crypto.exchanger.KeyExchanger;
+import cn.seqdata.crypto.exchanger.KeyExchangers;
+import cn.seqdata.oauth2.policy.PasswordPolicy;
+import cn.seqdata.oauth2.policy.checker.*;
+import cn.seqdata.oauth2.repos.rbac.PasswordHistoryRepo;
+import cn.seqdata.oauth2.repos.rbac.UserRepo;
+import cn.seqdata.oauth2.service.DefaultTokenEnhancer;
+import cn.seqdata.oauth2.service.DefaultTokenServices;
+import cn.seqdata.oauth2.service.RedisTokenStore;
+import cn.seqdata.oauth2.service.UserService;
+import lombok.AllArgsConstructor;
+
+/**
+ * @author jrxian
+ * @date 2020-11-23 19:07
+ */
+@Configuration
+@AllArgsConstructor
+public class AuthzServerConfiguration {
+
+ @Bean
+ public KeyExchanger keyExchanger(PasswordPolicy policy, StringRedisTemplate redisTemplate) {
+ final String PRIVATE_KEY = "exchanger.privateKey";
+ final String PUBLIC_KEY = "exchanger.publicKey";
+
+ KeyExchangers keyExchangeMethod = policy.getKeyExchangeMethod();
+ KeyExchanger keyExchanger = keyExchangeMethod.supplier.get();
+
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+
+ if(BooleanUtils.isTrue(redisTemplate.hasKey(PRIVATE_KEY)) && StringUtils.isNotBlank(opsForValue.get(PRIVATE_KEY))) {
+ keyExchanger.setPrivateKey(opsForValue.get(PRIVATE_KEY));
+ keyExchanger.setPublicKey(opsForValue.get(PUBLIC_KEY));
+ } else {
+ keyExchanger.genKeyPair();
+ opsForValue.set(PRIVATE_KEY, keyExchanger.getPrivateKey());
+ opsForValue.set(PUBLIC_KEY, keyExchanger.getPublicKey());
+ }
+
+ return keyExchanger;
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
+ if(encoder instanceof DelegatingPasswordEncoder) {
+ DelegatingPasswordEncoder delegating = (DelegatingPasswordEncoder) encoder;
+ delegating.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance());
+ }
+ return encoder;
+ }
+
+ @Bean
+ public PasswordChecker passwordChecker(PasswordPolicy policy, PasswordEncoder encoder, PasswordHistoryRepo historyRepo) {
+ CompositePasswordChecker passwordChecker = new CompositePasswordChecker();
+ passwordChecker.addChecker(new UsernameChecker());
+ passwordChecker.addChecker(new TextChecker());
+ passwordChecker.addChecker(new StrengthChecker(policy));
+ passwordChecker.addChecker(new HistoryChecker(policy, encoder, historyRepo));
+ return passwordChecker;
+ }
+
+ @Bean
+ public TokenStore tokenStore(StringRedisTemplate redisTemplate, PasswordPolicy passwordPolicy) {
+ RedisTokenStore tokenStore = new RedisTokenStore(redisTemplate);
+ tokenStore.setMaximumSessions(passwordPolicy.getMaximumSessions());
+ return tokenStore;
+ }
+
+ @Bean
+ public TokenEnhancer tokenEnhancer(UserRepo userRepo, UserService userService) {
+ return new DefaultTokenEnhancer(userRepo, userService);
+ }
+
+ @Bean
+ public AuthorizationServerTokenServices tokenServices(ClientDetailsService clientDetailsService,
+ TokenStore tokenStore, TokenEnhancer tokenEnhancer) {
+ return new DefaultTokenServices(clientDetailsService, tokenStore, tokenEnhancer);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/BizlogConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/BizlogConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..75df22ffd354eb7507ecb9ae582b4fa56ebbf095
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/BizlogConfiguration.java
@@ -0,0 +1,134 @@
+package cn.seqdata.oauth2;
+
+import java.util.Map;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.amqp.rabbit.core.RabbitTemplate;
+import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
+import org.springframework.amqp.support.converter.MessageConverter;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.authentication.event.AbstractAuthenticationEvent;
+import org.springframework.security.authentication.event.AbstractAuthenticationFailureEvent;
+import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
+import org.springframework.security.authentication.event.LogoutSuccessEvent;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.util.CollectionUtils;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import cn.seqdata.syslog.*;
+
+/**
+ * @author jrxian
+ * @date 2020/12/13 13:24
+ */
+@Configuration
+@EnableSyslog
+public class BizlogConfiguration {
+ @Value("${spring.application.name}")
+ private String appName;
+
+ @Bean
+ public MessageConverter messageConverter(ObjectMapper objectMapper) {
+ return new Jackson2JsonMessageConverter(objectMapper);
+ }
+
+ @Bean
+ @ConditionalOnProperty("spring.rabbitmq.host")
+ public RabbitSyslogConsumer rabbitSyslogConsumer(RabbitTemplate rabbitTemplate) {
+ return new RabbitSyslogConsumer(rabbitTemplate);
+ }
+
+ @Bean
+ public ApplicationListener syslogAuthSuccessListener(CompositeSyslogConsumer consumer) {
+ return event -> {
+ if(isBuiltInUser(event.getAuthentication())) {
+ return;
+ }
+
+ SyslogRecord.SyslogRecordBuilder builder = logBuilder("login", "登录", event);
+ consumer.accept(builder.build());
+ };
+ }
+
+ @Bean
+ public ApplicationListener syslogAuthFailureListener(CompositeSyslogConsumer consumer) {
+ return event -> {
+ if(isBuiltInUser(event.getAuthentication())) {
+ return;
+ }
+
+ AuthenticationException exception = event.getException();
+ Class extends AuthenticationException> exceptionClass = exception.getClass();
+
+ SyslogRecord.SyslogRecordBuilder builder = logBuilder("login", "登录", event)
+ .errorCode(exceptionClass.getSimpleName());
+ consumer.accept(builder.build());
+ };
+ }
+
+ @Bean
+ public ApplicationListener syslogLogoutSuccessListener(CompositeSyslogConsumer consumer) {
+ return event -> {
+ if(isBuiltInUser(event.getAuthentication())) {
+ return;
+ }
+
+ SyslogRecord.SyslogRecordBuilder builder = logBuilder("logout", "注销", event);
+ consumer.accept(builder.build());
+ };
+ }
+
+ /**
+ * 系统内置用户,不记录登录日志
+ */
+ private boolean isBuiltInUser(Authentication authentication) {
+ //没有任何角色
+ if(CollectionUtils.isEmpty(authentication.getAuthorities())) {
+ return true;
+ }
+
+ //匿名用户
+ if(authentication instanceof AnonymousAuthenticationToken) {
+ return true;
+ }
+
+ //client登录
+ if(authentication instanceof OAuth2Authentication) {
+ return ((OAuth2Authentication) authentication).isClientOnly();
+ }
+
+ return false;
+ }
+
+ private SyslogRecord.SyslogRecordBuilder logBuilder(String eventId, String eventName, AbstractAuthenticationEvent event) {
+ return SyslogRecord.builder()
+ .projectId(appName)
+ .eventId(eventId)
+ .eventName(eventName)
+ .eventTime(event.getTimestamp())
+ .forwardIp(DefaultSyslogResolver.forwardAddr())
+ .sourceIp(DefaultSyslogResolver.remoteAddr())
+ .clientId(clientId(event))
+ .username(DefaultSyslogResolver.username(event.getAuthentication()));
+ }
+
+ private String clientId(AbstractAuthenticationEvent event) {
+ String clientId = DefaultSyslogResolver.clientId(event.getAuthentication());
+ if(StringUtils.isNotBlank(clientId)) {
+ return clientId;
+ }
+ Object details = event.getAuthentication()
+ .getDetails();
+ if(details instanceof Map, ?>) {
+ return (String) ((Map, ?>) details).get("clientId");
+ }
+ return null;
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/LockerConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/LockerConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..5da5e5ca2ef8612682c1b53ccbdabed1838ac0bc
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/LockerConfiguration.java
@@ -0,0 +1,60 @@
+package cn.seqdata.oauth2;
+
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+import org.joda.time.DateTime;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
+import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
+import org.springframework.security.core.Authentication;
+
+import cn.seqdata.oauth2.policy.PasswordPolicy;
+import cn.seqdata.oauth2.service.AccountLockService;
+
+/**
+ * @author jrxian
+ * @date 2020/12/13 23:44
+ * 密码连续错误超过3次,锁定5分钟
+ */
+@Configuration
+@lombok.AllArgsConstructor
+public class LockerConfiguration {
+ private final StringRedisTemplate redisTemplate;
+ private final PasswordPolicy passwordPolicy;
+ private final AccountLockService lockService;
+
+ @Bean
+ public ApplicationListener lockAuthcSuccessListener() {
+ return event -> {
+ Authentication authentication = event.getAuthentication();
+ String username = authentication.getName();
+ redisTemplate.delete(lockName(username));
+ lockService.unlock(username);
+ };
+ }
+
+ @Bean
+ public ApplicationListener lockAuthcBadCredentialsListener() {
+ return event -> {
+ Authentication authentication = event.getAuthentication();
+ String username = authentication.getName();
+ String lockName = lockName(username);
+ ValueOperations opsForValue = redisTemplate.opsForValue();
+ Long hits = opsForValue.increment(lockName);
+ if(Objects.nonNull(hits) && hits > passwordPolicy.lockHits) {
+ redisTemplate.expire(lockName, passwordPolicy.lockExpire, TimeUnit.SECONDS);
+ DateTime occur = DateTime.now();
+ lockService.lock(username, occur.plusMinutes(5));
+ }
+ };
+ }
+
+ public static String lockName(String username) {
+ return "lock:" + username;
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/NonceConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/NonceConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..94844433b466d2e7ed59dd1de036e8332db775ef
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/NonceConfiguration.java
@@ -0,0 +1,74 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.mail.MailProperties;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.core.AuthenticationException;
+import cn.seqdata.oauth2.nonce.AliyunSmsService;
+import cn.seqdata.oauth2.nonce.EmailNonceHandler;
+import cn.seqdata.oauth2.nonce.MobileNonceHandler;
+import cn.seqdata.oauth2.nonce.NonceHandler;
+import cn.seqdata.oauth2.repos.rbac.UserAccountRepo;
+import cn.seqdata.oauth2.service.UserService;
+
+/**
+ * Author: jrxian
+ * Date: 2020-04-07 10:17
+ */
+@Configuration
+@EnableConfigurationProperties({AliyunProperties.class, AliyunSmsProperties.class})
+public class NonceConfiguration {
+
+ @Bean
+ @ConditionalOnProperty(value = "aliyun.sms.enabled")
+ public AliyunSmsService smsService(AliyunProperties properties, AliyunSmsProperties smsProperties) {
+ return new AliyunSmsService(properties, smsProperties);
+ }
+
+ @Bean
+ @ConditionalOnBean(AliyunSmsService.class)
+ public NonceHandler mobileNonceService(UserAccountRepo accountRepo, UserService userService,
+ AliyunSmsService smsService, StringRedisTemplate redisTemplate) {
+ return new MobileNonceHandler(accountRepo, userService, smsService, redisTemplate);
+ }
+
+ @Bean
+ @ConditionalOnProperty(value = "spring.mail.host")
+ public NonceHandler emailNonceService(UserAccountRepo accountRepo, JavaMailSender mailSender,
+ MailProperties mailProperties, StringRedisTemplate redisTemplate) {
+ return new EmailNonceHandler(accountRepo, mailSender, mailProperties, redisTemplate);
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(name = {"smsService", "emailNonceService"})
+ public NonceHandler anonymousMobileNonceHandler() {
+ return new NonceHandler() {
+ @Override
+ public boolean support(String username) {
+ return true;
+ }
+
+ @Override
+ public void nonce(String mobile, int expire, String clientId) {
+ throw new AuthenticationServiceException("未实现验证码登录功能");
+ }
+
+ @Override
+ public void nonceWithRedis(String mobile, int expire) {
+ throw new AuthenticationServiceException("未实现验证码登录功能");
+ }
+
+ @Override
+ public void check(String mobile, int nonce) throws AuthenticationException {
+ throw new AuthenticationServiceException("未实现验证码登录功能");
+ }
+ };
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/SwaggerConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/SwaggerConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4e75b7fe777cd0cfc1f0c6a3598625fefab3fa1
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/SwaggerConfiguration.java
@@ -0,0 +1,42 @@
+package cn.seqdata.oauth2;
+
+import java.util.Collections;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.OAuthBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.service.AuthorizationScope;
+import springfox.documentation.service.ResourceOwnerPasswordCredentialsGrant;
+import springfox.documentation.service.SecurityReference;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spi.service.contexts.SecurityContext;
+import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+
+/**
+ * Author: jrxian
+ * Date: 2020-02-04 01:36
+ */
+@Configuration
+@ConditionalOnProperty(value = "swagger.enabled", matchIfMissing = true)
+@EnableSwagger2
+public class SwaggerConfiguration {
+
+ @Bean
+ public Docket apiDocker() {
+ return new Docket(DocumentationType.SWAGGER_2)
+ .securitySchemes(Collections.singletonList(new OAuthBuilder()
+ .name("OAuth2")
+ .grantTypes(Collections.singletonList(new ResourceOwnerPasswordCredentialsGrant("/authz/oauth/token")))
+ .build()))
+ .securityContexts(Collections.singletonList(SecurityContext.builder()
+ .securityReferences(Collections.singletonList(SecurityReference.builder()
+ .reference("OAuth2")
+ .scopes(new AuthorizationScope[]{})
+ .build()))
+ .forPaths(PathSelectors.any())
+ .build()));
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/WebSecurityConfiguration.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/WebSecurityConfiguration.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf2fd5b57ba8c7d5b9b577fd49df4cde031ed4b6
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/WebSecurityConfiguration.java
@@ -0,0 +1,30 @@
+package cn.seqdata.oauth2;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * Author: jrxian
+ * Date: 2019-10-02 19:27
+ */
+@Configuration
+@EnableWebSecurity
+public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
+
+ @Override
+ public void configure(WebSecurity web) {
+ web
+ .ignoring()
+ .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/v2/api-docs/**", "/webjars/**", "/plugins/**");
+ }
+
+ @Bean
+ @Override
+ public AuthenticationManager authenticationManagerBean() throws Exception {
+ return super.authenticationManagerBean();
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/ConnectController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/ConnectController.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b8c65d90541e40e32f8b781ce6515ab3a32b827
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/ConnectController.java
@@ -0,0 +1,249 @@
+package cn.seqdata.oauth2.controller;
+
+import java.net.URI;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import javax.annotation.security.PermitAll;
+import javax.servlet.http.HttpServletRequest;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.joda.time.DateTimeConstants;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import cn.seqdata.core.error.ErrorAttributes;
+import cn.seqdata.crypto.exchanger.KeyExchanger;
+import cn.seqdata.oauth2.OAuth2Constants;
+import cn.seqdata.oauth2.jpa.oauth.UserDetail;
+import cn.seqdata.oauth2.jpa.rbac.UserAccount;
+import cn.seqdata.oauth2.nonce.NonceHandler;
+import cn.seqdata.oauth2.provider.OAuth2Provider;
+import cn.seqdata.oauth2.provider.OAuth2Template;
+import cn.seqdata.oauth2.repos.oauth.UserDetailRepo;
+import cn.seqdata.oauth2.repos.rbac.UserAccountRepo;
+import cn.seqdata.oauth2.service.UserService;
+import cn.seqdata.oauth2.util.SecurityUtils;
+import cn.seqdata.syslog.Syslog;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-24 01:13
+ */
+@Api("第三方账号绑定")
+@RestController
+@RequestMapping("/connect")
+@lombok.RequiredArgsConstructor
+public class ConnectController {
+ private final ClientRegistrationRepository repository;
+ private final UserService userService;
+ private final PasswordEncoder passwordEncoder;
+ private final UserAccountRepo accountRepo;
+ private final UserDetailRepo userDetailRepo;
+ private final List nonceHandlers;
+ private final KeyExchanger keyExchanger;
+
+ @GetMapping
+ public Map list(Principal principal) {
+ Map connectors = new HashMap<>();
+
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ userService.loadUser(grantType, username)
+ .ifPresent(account -> {
+ connectors.put(OAuth2Constants.USERNAME, account.getUsername());
+ connectors.put(OAuth2Constants.MOBILE, account.getMobile());
+ connectors.put(OAuth2Constants.EMAIL, account.getEmail());
+
+ userDetailRepo.findByUserId(Objects.requireNonNull(account.getId()))
+ .stream()
+ .map(UserDetail::getId)
+ .filter(Objects::nonNull)
+ .forEach(x -> connectors.put(x.getClient(), x.getPrincipal()));
+ });
+
+ return connectors;
+ }
+
+ /**
+ * 向手机或邮箱发送一次性验证码
+ */
+ @Syslog
+ @ApiOperation("发送验证码")
+ @RequestMapping(value = "/nonce", method = {RequestMethod.GET, RequestMethod.POST})
+ @PermitAll
+ public ResponseEntity> nonce(String username, @RequestParam(required = false) String clientId) {
+ boolean isSend = false;
+ for(NonceHandler nonceHandler : nonceHandlers) {
+ if(nonceHandler.support(username)) {
+ nonceHandler.nonce(username, 2 * DateTimeConstants.SECONDS_PER_MINUTE, clientId);
+ isSend = true;
+ }
+ }
+ if(isSend) {
+ return ResponseEntity.ok("验证码已经发送,2分钟内输入有效!");
+ } else {
+ throw new AuthenticationServiceException("暂未实现验证码发送功能");
+ }
+ }
+
+ /**
+ * 向手机或邮箱发送一次性验证码
+ */
+ @Deprecated
+ @Syslog
+ @ApiOperation("发送验证码")
+ @RequestMapping(value = "/v2/nonce", method = {RequestMethod.GET, RequestMethod.POST})
+ @PermitAll
+ public ResponseEntity> nonce2(String username) {
+ boolean isSend = false;
+ for(NonceHandler nonceHandler : nonceHandlers) {
+ if(nonceHandler.support(username)) {
+ nonceHandler.nonceWithRedis(username, 2 * DateTimeConstants.SECONDS_PER_MINUTE);
+ isSend = true;
+ }
+ }
+ if(isSend) {
+ return ResponseEntity.ok("验证码已经发送,2分钟内输入有效!");
+ } else {
+ throw new AuthenticationServiceException("暂未实现验证码发送功能");
+ }
+ }
+
+ /**
+ * 手机邮箱验证码校验
+ *
+ * @author liang chenglong
+ * @date 2022-03-03
+ */
+ @Syslog
+ @ApiOperation("验证码校验")
+ @RequestMapping(value = "/verify",method = {RequestMethod.GET, RequestMethod.POST})
+ public ResponseEntity> verify(String username, int nonce){
+ try {
+ for(NonceHandler nonceHandler : nonceHandlers) {
+ if(nonceHandler.support(username)) {
+ nonceHandler.check(username, nonce);
+ }
+ }
+ return ResponseEntity.ok("验证成功!");
+ } catch(Exception e) {
+ throw new AuthenticationServiceException("验证失败,请填检查验证码是否正确!");
+ }
+ }
+
+ @Syslog
+ @ApiOperation("通过密码绑定")
+ @GetMapping(value = "/password")
+ public void bindPassword(String username, String password, Principal principal) {
+ UserAccount user = userService.loadUser(OAuth2Constants.PASSWORD, username)
+ .orElseThrow(() -> new UsernameNotFoundException("用户名不存在"));
+
+ password = keyExchanger.decrypt(password);
+ if(!passwordEncoder.matches(password, user.getPassword())) {
+ throw new BadCredentialsException("用户名或密码不正确");
+ }
+
+ userService.bindUser(Objects.requireNonNull(user.getId()),
+ SecurityUtils.grantType(principal),
+ SecurityUtils.username(principal));
+ }
+
+ @Syslog
+ @ApiOperation("通过手机号绑定")
+ @GetMapping(value = "/mobile")
+ public void bindMobile(String mobile, int nonce, Principal principal) {
+ bindNonce(OAuth2Constants.MOBILE, mobile, nonce, principal);
+ }
+
+ @Syslog
+ @ApiOperation("通过邮箱绑定")
+ @GetMapping(value = "/email")
+ public void bindEmail(String email, int nonce, Principal principal) {
+ bindNonce(OAuth2Constants.EMAIL, email, nonce, principal);
+ }
+
+ @Syslog
+ @ApiOperation("第三方账号绑定")
+ @GetMapping(value = "/{registrationId}")
+ public ResponseEntity> bind(@PathVariable String registrationId, HttpServletRequest request, Principal principal) {
+ ClientRegistration registration = repository.findByRegistrationId(registrationId);
+ OAuth2Provider provider = OAuth2Provider.provider(registration);
+ OAuth2Template oAuth2Template = provider.createOAuth2Template(registration);
+
+ //从 request 中分离出 accessToken
+ String accessToken = SecurityUtils.accessToken(principal);
+
+ //重定向 URI,返回信息由 OAuth2CodeController 负责接收
+ String redirectUri = registration.getRedirectUriTemplate();
+ if(Objects.isNull(redirectUri)) {
+ redirectUri = String.valueOf((request.getRequestURL()));
+ redirectUri = redirectUri.replace("/connect", "/oauth/code");
+ }
+
+ //将 accessToken 以 state 的形式传送出去,返回后用来判断被绑定的用户
+ URI authorizeUri = oAuth2Template.buildAuthorizeURI(accessToken, redirectUri);
+
+ return ResponseEntity.status(HttpStatus.SEE_OTHER)
+ .location(authorizeUri)
+ .build();
+ }
+
+ @Syslog
+ @ApiOperation("第三方账号解绑")
+ @Transactional
+ @DeleteMapping(value = "/{registrationId}")
+ public void unbind(@PathVariable String registrationId, Principal principal) {
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ Long userId = userService.loadUser(grantType, username)
+ .map(UserAccount::getId)
+ .orElseThrow(() -> new UsernameNotFoundException("要解绑的账号不存在"));
+ userDetailRepo.deleteByIdClientAndUserId(registrationId, userId);
+ }
+
+ @Syslog
+ @ApiOperation("邮箱解绑")
+ @Transactional
+ @DeleteMapping("/email")
+ public void unbindEmail(Principal principal) {
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ UserAccount userAccount = userService.loadUser(grantType, username)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
+ userAccount.setEmail(null);
+ accountRepo.save(userAccount);
+ }
+
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(AuthenticationException.class)
+ public ErrorAttributes authenticationException(AuthenticationException ex) {
+ return new ErrorAttributes(HttpStatus.BAD_REQUEST, ex.getMessage(), null);
+ }
+
+ private void bindNonce(String grantType, String username, int nonce, Principal principal) {
+ for(NonceHandler nonceHandler : nonceHandlers) {
+ if(nonceHandler.support(username)) {
+ nonceHandler.check(username, nonce);
+ userService.loadUser(grantType, username)
+ .map(UserAccount::getId)
+ .ifPresent(userId -> userService.bindUser(userId,
+ SecurityUtils.grantType(principal),
+ SecurityUtils.username(principal)));
+ }
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/CurrentController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/CurrentController.java
new file mode 100644
index 0000000000000000000000000000000000000000..241d0c9b9b108907b77afc9fd9cf045957d2210a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/CurrentController.java
@@ -0,0 +1,246 @@
+package cn.seqdata.oauth2.controller;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
+import org.springframework.security.oauth2.provider.token.ConsumerTokenServices;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.validation.BindException;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import cn.seqdata.oauth2.OAuth2Constants;
+import cn.seqdata.oauth2.jpa.rbac.User;
+import cn.seqdata.oauth2.jpa.rbac.UserAccount;
+import cn.seqdata.oauth2.params.UserAccountParam;
+import cn.seqdata.oauth2.repos.rbac.UserAccountRepo;
+import cn.seqdata.oauth2.repos.rbac.UserRepo;
+import cn.seqdata.oauth2.service.AuthorityService;
+import cn.seqdata.oauth2.service.DefaultTokenServices;
+import cn.seqdata.oauth2.service.TokenService;
+import cn.seqdata.oauth2.service.UserService;
+import cn.seqdata.oauth2.util.SecurityUtils;
+import cn.seqdata.syslog.Syslog;
+import lombok.AllArgsConstructor;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-27 17:16
+ */
+@Api("当前登录用户")
+@RestController
+@RequestMapping("/current")
+@PreAuthorize("isAuthenticated()")
+@AllArgsConstructor
+public class CurrentController {
+ private static final String objectName = UserAccount.class.getSimpleName();
+
+ private final UserAccountRepo accountRepo;
+ private final UserRepo userRepo;
+ private final TokenStore tokenStore;
+ private final UserService userService;
+ private final ConsumerTokenServices consumerTokenServices;
+ private final AuthorityService authorityService;
+ private final TokenService tokenService;
+
+ private final AuthorizationServerTokenServices tokenServices;
+
+ @ApiOperation("通行证")
+ @GetMapping("/principal")
+ public Principal principal(Principal principal) {
+ return principal;
+ }
+
+ @PreAuthorize("permitAll()")
+ @ApiOperation("token换取通行证")
+ @GetMapping("/token/{token}/principal")
+ public Principal principal(@PathVariable String token) {
+ return tokenStore.readAuthentication(token);
+ }
+
+ @ApiOperation("用户信息")
+ @GetMapping("/user")
+ public Optional info(Principal principal) {
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ return userService.loadUser(grantType, username)
+ .map(UserAccount::getId)
+ .map(id -> {
+ User user = userRepo.getOne(Objects.requireNonNull(id));
+ user.setAuthorities(authorityService.loadAuthorities(id));
+ return user;
+ });
+ }
+
+ @ApiOperation("令牌")
+ @GetMapping("/token")
+ public OAuth2AccessToken token(Principal principal) {
+ return tokenStore.readAccessToken(SecurityUtils.accessToken(principal));
+ }
+
+ @PreAuthorize("permitAll()")
+ @ApiOperation("刷新token对应的角色关系")
+ @GetMapping("/token/refresh")
+ public void tokenRefresh(Principal principal) {
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ Collection newAuthorities = tokenService.loadAuthorities(grantType, username);
+ OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
+
+ // 重新构造OAuth2Authentication
+ HashSet origAuthorities = (HashSet) oAuth2Authentication.getOAuth2Request()
+ .getAuthorities();
+ origAuthorities.clear();
+ origAuthorities.addAll(newAuthorities);
+
+ Authentication userAuthentication = oAuth2Authentication.getUserAuthentication();
+ if(userAuthentication != null) {
+ if(userAuthentication instanceof OAuth2AuthenticationToken) {
+ userAuthentication =
+ new OAuth2AuthenticationToken(((OAuth2AuthenticationToken) userAuthentication).getPrincipal(),
+ newAuthorities, ((OAuth2AuthenticationToken) userAuthentication).getAuthorizedClientRegistrationId());
+ } else {
+ Authentication finalUserAuthentication = userAuthentication;
+ userAuthentication = new AbstractAuthenticationToken(newAuthorities) {
+ private static final long serialVersionUID = -1786107511865438515L;
+
+ @Override
+ public Object getCredentials() {
+ return finalUserAuthentication.getCredentials();
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return finalUserAuthentication.getPrincipal();
+ }
+
+ @Override
+ public boolean isAuthenticated() {
+ return true;
+ }
+ };
+ }
+ }
+ OAuth2Authentication newOAuth2Authentication = new OAuth2Authentication(oAuth2Authentication.getOAuth2Request(), userAuthentication);
+ ((DefaultTokenServices) tokenServices).refreshAccessTokenAuthorities(oAuth2Authentication, newOAuth2Authentication);
+ }
+
+ @ApiOperation("角色")
+ @GetMapping("/authorities")
+ public Collection authorities(Principal principal) {
+ return SecurityUtils.authorities(principal);
+ }
+
+ @Syslog
+ @ApiOperation("注销")
+ @DeleteMapping
+ public void logout(Principal principal) {
+ consumerTokenServices.revokeToken(SecurityUtils.accessToken(principal));
+ }
+
+ @Syslog
+ @ApiOperation("修改昵称")
+ @PatchMapping(value = "/name")
+ public void updateName(Principal principal, @RequestBody UserAccountParam param) throws BindException {
+ UserAccount entity = getUserAccount(principal);
+
+ BindException ex = new BindException(param, objectName);
+ checkField(ex, param, OAuth2Constants.NAME, UserAccountParam::getName);
+ checkErrors(ex);
+
+ entity.setName(param.getName());
+ accountRepo.save(entity);
+ }
+
+ @Syslog
+ @ApiOperation("修改登录名")
+ @PatchMapping(value = "/username")
+ public void updateUsername(Principal principal, @RequestBody UserAccountParam param) throws BindException {
+ UserAccount entity = getUserAccount(principal);
+
+ BindException ex = new BindException(param, objectName);
+ checkField(ex, param, OAuth2Constants.USERNAME, UserAccountParam::getUsername);
+ checkExist(ex, param, OAuth2Constants.USERNAME, entity, UserAccountParam::getUsername, accountRepo::findByUsername);
+ checkErrors(ex);
+
+ entity.setUsername(param.getUsername());
+ accountRepo.save(entity);
+ }
+
+ @Syslog
+ @ApiOperation("修改手机号")
+ @PatchMapping(value = "/mobile")
+ public void updateMobile(Principal principal, @RequestBody UserAccountParam param) throws BindException {
+ UserAccount entity = getUserAccount(principal);
+
+ BindException ex = new BindException(param, objectName);
+ checkField(ex, param, OAuth2Constants.MOBILE, UserAccountParam::getMobile);
+ checkExist(ex, param, OAuth2Constants.MOBILE, entity, UserAccountParam::getMobile, accountRepo::findByMobile);
+ checkErrors(ex);
+
+ entity.setMobile(param.getMobile());
+ accountRepo.save(entity);
+ }
+
+ @Syslog
+ @ApiOperation("修改邮箱")
+ @PatchMapping(value = "/email")
+ public void updateEmail(Principal principal, @RequestBody UserAccountParam param) throws BindException {
+ UserAccount entity = getUserAccount(principal);
+
+ BindException ex = new BindException(param, objectName);
+ checkField(ex, param, OAuth2Constants.EMAIL, UserAccountParam::getEmail);
+ checkExist(ex, param, OAuth2Constants.EMAIL, entity, UserAccountParam::getEmail, accountRepo::findByEmail);
+ checkErrors(ex);
+
+ entity.setEmail(param.getEmail());
+ accountRepo.save(entity);
+ }
+
+ private UserAccount getUserAccount(Principal principal) {
+ String grantType = SecurityUtils.grantType(principal);
+ String username = SecurityUtils.username(principal);
+ return userService.loadUser(grantType, username)
+ .orElseThrow(() -> new ResponseStatusException(HttpStatus.UNAUTHORIZED));
+ }
+
+ private void checkField(BindException ex, UserAccountParam param, String fieldName,
+ Function function) {
+ if(StringUtils.isBlank(function.apply(param))) {
+ ex.addError(new FieldError(objectName, fieldName, fieldName + "不能为空"));
+ }
+ }
+
+ private void checkExist(BindException ex, UserAccountParam param, String fieldName, UserAccount entity,
+ Function getter, Function> querier) {
+ querier.apply(getter.apply(param))
+ .ifPresent(account -> {
+ if(!Objects.equals(entity.getId(), account.getId())) {
+ ex.addError(new FieldError(objectName, fieldName, fieldName + "已经被使用"));
+ }
+ });
+ }
+
+ private void checkErrors(BindException ex) throws BindException {
+ if(ex.hasErrors()) {
+ throw ex;
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/OAuth2CodeController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/OAuth2CodeController.java
new file mode 100644
index 0000000000000000000000000000000000000000..38fb279dc808b2ce60a34bcd2a7c7c83185f72b3
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/OAuth2CodeController.java
@@ -0,0 +1,68 @@
+package cn.seqdata.oauth2.controller;
+
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
+import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.web.bind.annotation.*;
+
+import cn.seqdata.oauth2.provider.OAuth2Provider;
+import cn.seqdata.oauth2.provider.OAuth2Template;
+import cn.seqdata.oauth2.provider.UserProfile;
+import cn.seqdata.oauth2.service.TokenService;
+import cn.seqdata.oauth2.service.UserService;
+import cn.seqdata.syslog.Syslog;
+import lombok.AllArgsConstructor;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-24 02:09
+ */
+@RestController
+@RequestMapping("/oauth/code")
+@AllArgsConstructor
+public class OAuth2CodeController {
+ private final ClientRegistrationRepository repository;
+ private final TokenStore tokenStore;
+ private final UserService userService;
+ private final TokenService tokenService;
+
+ @Syslog
+ @ApiOperation("authorization_code的回调函数,处理code和state")
+ @GetMapping(value = "/{registrationId}", params = OAuth2ParameterNames.CODE)
+ public OAuth2AccessToken bind(@PathVariable String registrationId,
+ @RequestParam(OAuth2ParameterNames.CODE) String code,
+ @RequestParam(value = OAuth2ParameterNames.STATE, required = false) String state) {
+ ClientRegistration registration = repository.findByRegistrationId(registrationId);
+ OAuth2Provider provider = OAuth2Provider.provider(registration);
+ OAuth2Template oAuth2Template = provider.createOAuth2Template(registration);
+
+ //从远程拉取用户信息,以 registrationId + remoteUser.name 作为唯一识别符
+ OAuth2AccessTokenResponse tokenResponse = oAuth2Template.exchangeForAccess(code, state);
+ OAuth2User remoteUser = oAuth2Template.loadUser(tokenResponse);
+
+ UserProfile profile = provider.profile.apply(remoteUser);
+ //这里的 state 是 accessToken
+ OAuth2Authentication authentication = tokenStore.readAuthentication(state);
+ userService.updateUser(registrationId, profile, authentication);
+ // 注意,此处将registrationId作为clientId
+ return tokenService.createToken(registrationId, remoteUser);
+ }
+
+ @ApiOperation("authorization_code的错误回调函数,处理error, error_description, error_uri")
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @GetMapping(value = "/{registrationId}", params = OAuth2ParameterNames.ERROR)
+ public OAuth2Error error(@PathVariable String registrationId,
+ @RequestParam(OAuth2ParameterNames.ERROR) String error,
+ @RequestParam(value = OAuth2ParameterNames.ERROR_DESCRIPTION, required = false) String errorDescription,
+ @RequestParam(value = OAuth2ParameterNames.ERROR_URI, required = false) String errorURI) {
+ return new OAuth2Error(error, errorDescription, errorURI);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/PasswordController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/PasswordController.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d7fdd1496343e469229e7e35973ec2aa0d60a54
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/PasswordController.java
@@ -0,0 +1,145 @@
+package cn.seqdata.oauth2.controller;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.security.PermitAll;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.CredentialsExpiredException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.UserDetailsManager;
+import org.springframework.web.bind.annotation.*;
+
+import cn.seqdata.core.error.ErrorAttributes;
+import cn.seqdata.crypto.exchanger.KeyExchanger;
+import cn.seqdata.oauth2.nonce.NonceHandler;
+import cn.seqdata.oauth2.policy.PasswordPolicy;
+import cn.seqdata.oauth2.policy.checker.PasswordChecker;
+import cn.seqdata.oauth2.repos.rbac.UserAccountRepo;
+import cn.seqdata.oauth2.service.AccountLockService;
+import cn.seqdata.oauth2.service.JpaUserDetailsPasswordService;
+import cn.seqdata.syslog.Syslog;
+import lombok.AllArgsConstructor;
+
+/**
+ * @author jrxian
+ * @date 2020/11/28 11:07
+ */
+@Api("密码管理")
+@RestController
+@RequestMapping("/password")
+@AllArgsConstructor
+public class PasswordController {
+ private final UserAccountRepo accountRepo;
+ private final AuthenticationManager authenticationManager;
+ private final UserDetailsManager userDetailsManager;
+ private final JpaUserDetailsPasswordService passwordService;
+ private final KeyExchanger keyExchanger;
+ private final PasswordPolicy passwordPolicy;
+ private final PasswordEncoder passwordEncoder;
+ private final PasswordChecker passwordChecker;
+ private final AccountLockService lockService;
+ private final List nonceHandlers;
+
+ @PermitAll
+ @GetMapping("/key")
+ public String key() {
+ return keyExchanger.getPublicKey();
+ }
+
+ @Syslog
+ @ApiOperation("密码过期修改")
+ @PermitAll
+ @PostMapping
+ public void updatePassword(String username, String oldPassword, String newPassword) {
+ String decryptOldPswd = keyExchanger.decrypt(oldPassword);
+ String decryptNewPswd = keyExchanger.decrypt(newPassword);
+ Authentication authenticate = new UsernamePasswordAuthenticationToken(username, decryptOldPswd);
+ try {
+ //验证旧密码是否正确
+ authenticate = authenticationManager.authenticate(authenticate);
+ } catch(CredentialsExpiredException ex) {
+ //密码已经过期,必须吃掉这个异常
+ authenticate = new UsernamePasswordAuthenticationToken(username, decryptOldPswd, Collections.emptyList());
+ }
+ SecurityContext context = SecurityContextHolder.getContext();
+ context.setAuthentication(authenticate);
+ userDetailsManager.changePassword(decryptOldPswd, decryptNewPswd);
+ SecurityContextHolder.clearContext();
+ }
+
+ @Syslog
+ @ApiOperation("通用验证码修改密码")
+ @PermitAll
+ @PostMapping("/nonce")
+ public void noncePassword(String mobile, int nonce, String username, String password) {
+ //验证码有效性检查
+ for(NonceHandler nonceHandler : nonceHandlers) {
+ if(nonceHandler.support(mobile)) {
+ nonceHandler.check(mobile, nonce);
+ }
+ }
+
+ //账号和手机号关联检查
+ accountRepo.findByUsername(username)
+ .ifPresent(account -> {
+ if(!Objects.equals(account.getMobile(), mobile)) {
+ throw new AuthenticationServiceException("账户名与手机号不匹配");
+ }
+ });
+
+ //检查密码强度
+ String decryptPassword = keyExchanger.decrypt(password);
+ passwordChecker.accept(username, decryptPassword);
+
+ //设置的新密码先编码,否则会触发密码升级,导致密码立即过期
+ String encodedPassword = passwordEncoder.encode(decryptPassword);
+ passwordService.updatePassword(username, encodedPassword, passwordPolicy.getCredentialsExpireDate());
+ }
+
+ @Syslog
+ @ApiOperation("自己修改密码")
+ @PostMapping("/self")
+ @PreAuthorize("isAuthenticated()")
+ public void changePassword(String oldPassword, String newPassword) {
+ String decryptOldPswd = keyExchanger.decrypt(oldPassword);
+ String decryptNewPswd = keyExchanger.decrypt(newPassword);
+ userDetailsManager.changePassword(decryptOldPswd, decryptNewPswd);
+ }
+
+ @Syslog
+ @ApiOperation("管理员修改密码")
+ @PostMapping("/admin")
+ @PreAuthorize("hasAuthority('sysadmin')")
+ public void updatePassword(String username, String password) {
+ String decryptPassword = keyExchanger.decrypt(password);
+ UserDetails userDetails = userDetailsManager.loadUserByUsername(username);
+ passwordService.updatePassword(userDetails, decryptPassword);
+ }
+
+ @Syslog
+ @ApiOperation("解锁密码")
+ @PostMapping("/unlock")
+ @PreAuthorize("hasAuthority('sysadmin')")
+ public void unlockPassword(String username) {
+ lockService.unlock(username);
+ }
+
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ @ExceptionHandler(AuthenticationException.class)
+ public ErrorAttributes authenticationException(AuthenticationException ex) {
+ return new ErrorAttributes(HttpStatus.BAD_REQUEST, ex.getMessage(), ex);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/RouterController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/RouterController.java
new file mode 100644
index 0000000000000000000000000000000000000000..5d2f7bb1ff242fcd3bf63e4bebf1f721e5d830ae
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/RouterController.java
@@ -0,0 +1,96 @@
+package cn.seqdata.oauth2.controller;
+
+import java.security.Principal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import io.swagger.annotations.Api;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.web.SortDefault;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+
+import cn.seqdata.antd.Router;
+import cn.seqdata.oauth2.domain.PermissionType;
+import cn.seqdata.oauth2.jpa.perm.ActionPermission;
+import cn.seqdata.oauth2.jpa.perm.ModulePermission;
+import cn.seqdata.oauth2.jpa.perm.ViewPermission;
+import cn.seqdata.oauth2.jpa.rbac.AuthorityPermission;
+import cn.seqdata.oauth2.jpa.rbac.Permission;
+import cn.seqdata.oauth2.repos.perm.SysPermRepo;
+import cn.seqdata.oauth2.repos.rbac.AuthorityPermissionRepo;
+import cn.seqdata.oauth2.service.RouterService;
+import cn.seqdata.oauth2.util.RouterUtils;
+import lombok.AllArgsConstructor;
+
+/**
+ * Author: jrxian
+ * Date: 2020-03-22 10:04
+ */
+@Api("路由信息")
+@RestController
+@RequestMapping("/router")
+@AllArgsConstructor
+public class RouterController {
+ private final AuthorityPermissionRepo permissionRepo;
+ private final SysPermRepo sysPermRepo;
+ private final RouterService service;
+
+ @GetMapping
+ public Collection routers(
+ @RequestParam(required = false) Long sys,
+ @RequestParam(required = false) String id,
+ Principal principal,
+ @SortDefault("orderNo") Sort sort) {
+
+ //获取有权限的全部module
+ Collection modulePerms = service.fetchModulePerms(principal, sort);
+ //获取有权限的全部action,并且按view做好分组
+ Collection actionPerms = service.fetchActionPerms(principal, sort);
+
+ //筛选指定角色下面的所有菜单
+ Set topRouterIds = new HashSet<>();
+ boolean hasId = !StringUtils.isBlank(id);
+ if(hasId) {
+ List authorityIds = Arrays.stream(StringUtils.split(id, ","))
+ .map(Long::valueOf)
+ .collect(Collectors.toList());
+ List specialAuthPerms = permissionRepo.findByAuthorityIdIn(authorityIds, Sort.unsorted());
+ Set authPerms = new HashSet<>();
+ specialAuthPerms.forEach(x -> {
+ Permission perm = x.getPermission();
+ authPerms.add(perm.getId());
+ if(Objects.equals(perm.getType(), PermissionType.sys)) {
+ topRouterIds.add(perm.getId());
+ }
+ });
+ modulePerms.removeIf(perm -> !authPerms.contains(perm.getId()));
+ actionPerms.removeIf(perm -> !authPerms.contains(perm.getId()));
+ }
+ Multimap actionPermMap = LinkedListMultimap.create();
+ actionPerms.forEach(x -> {
+ ViewPermission viewPerm = x.getViewPerm();
+ actionPermMap.put(viewPerm.getId(), x.getIdentifier());
+ });
+
+ if(Objects.isNull(sys)) {
+ Collection routers = service.topRouters(principal, sort);
+ routers.removeIf(x -> hasId && !topRouterIds.contains(x.key));
+ routers.forEach(router -> service.doRouter(router, modulePerms, actionPermMap));
+ return routers;
+ } else {
+ //如果指定了sys,只查询本sys下的module
+ Router topRouter = RouterUtils.toRouter(sysPermRepo.getOne(sys));
+ if(hasId && !topRouterIds.contains(topRouter.key)) {
+ return Collections.emptyList();
+ }
+ service.doRouter(topRouter, modulePerms, actionPermMap);
+ return topRouter.children;
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/WxAppController.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/WxAppController.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ae9608ef088a1b572a07b680ec3b6c73b668cda
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/controller/WxAppController.java
@@ -0,0 +1,177 @@
+package cn.seqdata.oauth2.controller;
+
+import java.io.IOException;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.lang.Nullable;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import cn.seqdata.oauth2.DefaultRole;
+import cn.seqdata.oauth2.OAuth2Constants;
+import cn.seqdata.oauth2.jpa.rbac.UserAccount;
+import cn.seqdata.oauth2.service.TokenService;
+import cn.seqdata.oauth2.service.UserService;
+import cn.seqdata.oauth2.wechat.WechatUserProfile;
+import cn.seqdata.oauth2.wxapp.WxAppService;
+import cn.seqdata.syslog.Syslog;
+import cn.seqdata.wxapp.message.Code2SessionResponse;
+import cn.seqdata.wxapp.message.SchemeRequest;
+import cn.seqdata.wxapp.message.UrlLinkRequest;
+import cn.seqdata.wxapp.pojo.PhoneInfo;
+import cn.seqdata.wxapp.pojo.UserInfo;
+import cn.seqdata.wxapp.util.WxAppUtils;
+import lombok.AllArgsConstructor;
+
+/**
+ * @author jrxian
+ * @date 2020-06-22 09:17
+ */
+@Api("微信小程序")
+@RestController
+@RequestMapping("/wxapp/{id}")
+@AllArgsConstructor
+public class WxAppController {
+ private final ObjectMapper objectMapper;
+ private final WxAppService wxappService;
+ private final UserService userService;
+ private final TokenService tokenService;
+
+ /**
+ * 获取用户信息并且创建rbac_user_info
+ */
+ @Syslog
+ @ApiOperation("小程序注册")
+ @GetMapping("/signup")
+ @Transactional
+ public OAuth2AccessToken signup(@PathVariable("id") String id, String code, String iv, String encryptedData, @Nullable Principal principal) throws IOException {
+ Code2SessionResponse response = wxappService.signin(id, code);
+ String decryptedData = WxAppUtils.decryptData(response.session_key, iv, encryptedData);
+ UserInfo userInfo = objectMapper.readValue(decryptedData, UserInfo.class);
+
+ Map attributes = new HashMap<>();
+ WxAppUtils.toAttributes(attributes, userInfo);
+ WxAppUtils.toAttributes(attributes, response);
+ DefaultOAuth2User remoteUser = new DefaultOAuth2User(DefaultRole.NO_ROLES, attributes, WxAppUtils.WXAPP_ATTR_KEY);
+ WechatUserProfile profile = new WechatUserProfile(remoteUser);
+ userService.updateUser(id, profile, principal);
+
+ return tokenService.createToken(id, remoteUser);
+ }
+
+ /**
+ * 获取code换取openid登录
+ */
+ @Syslog
+ @ApiOperation("小程序登录")
+ @GetMapping("/signin")
+ @Transactional
+ public OAuth2AccessToken signin(@PathVariable("id") String id, String code, @Nullable Principal principal) {
+ Code2SessionResponse response = wxappService.signin(id, code);
+
+ Map attributes = new HashMap<>();
+ WxAppUtils.toAttributes(attributes, response);
+ DefaultOAuth2User remoteUser = new DefaultOAuth2User(DefaultRole.NO_ROLES, attributes, WxAppUtils.WXAPP_ATTR_KEY);
+ WechatUserProfile profile = new WechatUserProfile(remoteUser);
+ userService.updateUser(id, profile, principal);
+
+ return tokenService.createToken(id, remoteUser);
+ }
+
+ /**
+ * 获取微信手机号,绑定到现有账号
+ */
+ @ApiOperation("小程序手机号登录")
+ @GetMapping("/phone")
+ @Transactional
+ public OAuth2AccessToken phone(@PathVariable("id") String id, String code, String iv, String encryptedData) throws IOException {
+ Map attributes = attributes(id, code, iv, encryptedData);
+
+ userService.loadUser(OAuth2Constants.MOBILE, String.valueOf(attributes.get("purePhoneNumber")))
+ .map(UserAccount::getId)
+ .ifPresent(userId -> userService.bindUser(userId, id, String.valueOf(attributes.get("openid"))));
+
+ DefaultOAuth2User remoteUser = new DefaultOAuth2User(DefaultRole.NO_ROLES, attributes, WxAppUtils.WXAPP_ATTR_KEY);
+ return tokenService.createToken(id, remoteUser);
+ }
+
+ /**
+ * 获取微信手机号,绑定到现有账号
+ */
+ @ApiOperation("小程序手机号登录注册一体化-档案猫")
+ @GetMapping("/v2/phone")
+ @Transactional
+ public OAuth2AccessToken phoneV2(@PathVariable("id") String id, String code, String iv, String encryptedData) throws IOException {
+ Map attributes = attributes(id, code, iv, encryptedData);
+ String purePhoneNumber = String.valueOf(attributes.get("purePhoneNumber"));
+ Optional user = userService.loadUser(OAuth2Constants.MOBILE, purePhoneNumber);
+ if(user.isPresent()) {
+ // 如果手机号所在用户存在,则将openId从空user(只有openId)绑定到该手机号所在用户上
+ user.map(UserAccount::getId)
+ .ifPresent(userId -> userService.bindUser(userId, id, String.valueOf(attributes.get("openid"))));
+ } else {
+ // 如果手机号所在用户不存在,则将手机号绑定到空user(只有openId)所在用户上 or 创建新用户
+ userService.bindUserPhone(purePhoneNumber, id, String.valueOf(attributes.get("openid")), attributes);
+ }
+
+ DefaultOAuth2User remoteUser = new DefaultOAuth2User(DefaultRole.NO_ROLES, attributes, WxAppUtils.WXAPP_ATTR_KEY);
+ return tokenService.createToken(id, remoteUser);
+ }
+
+ /**
+ * 获取微信手机号,绑定到现有账号
+ */
+ @ApiOperation("解密信息小程序手机号")
+ @GetMapping("/phone/info")
+ @Transactional
+ public Map decryptData(@PathVariable("id") String id, String code, String iv, String encryptedData) throws IOException {
+ return attributes(id, code, iv, encryptedData);
+ }
+
+ @GetMapping("/token")
+ public String token(@PathVariable("id") String id) {
+ return wxappService.accessToken(id);
+ }
+
+ @DeleteMapping("/token")
+ public void token(@PathVariable("id") String id, @RequestParam("token") String token) {
+ wxappService.invalidToken(id, token);
+ }
+
+ @ApiOperation("获取小程序scheme码")
+ @PostMapping("/scheme")
+ public String urlScheme(@PathVariable("id") String id, @RequestBody SchemeRequest request) {
+ return wxappService.scheme(id, request);
+ }
+
+ @ApiOperation("获取小程序ticket")
+ @GetMapping("/ticket")
+ public String ticket(@PathVariable("id") String id) {
+ return wxappService.ticket(id);
+ }
+
+ @ApiOperation("获取小程序 url_link")
+ @PostMapping("/url/link")
+ public String urlLink(@PathVariable("id") String id, @RequestBody UrlLinkRequest request) {
+ return wxappService.urlLink(id, request);
+ }
+
+ private Map attributes(String id, String code, String iv, String encryptedData) throws IOException {
+ Code2SessionResponse response = wxappService.signin(id, code);
+ String decryptedData = WxAppUtils.decryptData(response.session_key, iv, encryptedData);
+ PhoneInfo phoneInfo = objectMapper.readValue(decryptedData, PhoneInfo.class);
+
+ Map attributes = new HashMap<>();
+ WxAppUtils.toAttributes(attributes, phoneInfo);
+ WxAppUtils.toAttributes(attributes, response);
+ return attributes;
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressFilter.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..077882e8628b496404f57c7694975950d736c36b
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressFilter.java
@@ -0,0 +1,22 @@
+package cn.seqdata.oauth2.filter;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+/**
+ * @author jrxian
+ * @date 2020/12/14 22:43
+ */
+@lombok.RequiredArgsConstructor
+public class AddressFilter extends RequestFilter {
+ private final AddressProperties properties;
+
+ @Override
+ protected void internalFilter(HttpServletRequest request) throws ResponseStatusException {
+ if(!properties.test(request.getRemoteAddr())) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "不允许在此客户端地址登录");
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressProperties.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..4482a435edd5c2f011001f1821898e051ac5675a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/AddressProperties.java
@@ -0,0 +1,55 @@
+package cn.seqdata.oauth2.filter;
+
+import java.util.Objects;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author jrxian
+ * @date 2020/12/15 0:34
+ * 只能在固定的地址范围登录
+ */
+@lombok.Getter
+@lombok.Setter
+@ConfigurationProperties("spring.security.filter.address")
+public class AddressProperties implements Predicate {
+ private Pattern allow;
+ private Pattern deny;
+
+ public String getAllow() {
+ return Objects.nonNull(allow) ? allow.toString() : null;
+ }
+
+ public void setAllow(String allow) {
+ this.allow = StringUtils.isEmpty(allow) ? null : Pattern.compile(allow);
+ }
+
+ public String getDeny() {
+ return Objects.nonNull(deny) ? deny.toString() : null;
+ }
+
+ public void setDeny(String deny) {
+ this.deny = StringUtils.isEmpty(deny) ? null : Pattern.compile(deny);
+ }
+
+ @Override
+ public boolean test(String property) {
+ if(Objects.nonNull(deny) && matches(deny.matcher(property))) {
+ return false;
+ }
+
+ if(Objects.nonNull(allow) && matches(allow.matcher(property))) {
+ return true;
+ }
+
+ return Objects.isNull(deny) && Objects.isNull(allow);
+ }
+
+ private static boolean matches(Matcher matcher) {
+ return matcher.matches();
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodFilter.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..6c1bef2c1817fa74f665fd27742c83677ef22424
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodFilter.java
@@ -0,0 +1,24 @@
+package cn.seqdata.oauth2.filter;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.joda.time.LocalTime;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.server.ResponseStatusException;
+
+/**
+ * @author jrxian
+ * @date 2020/12/15 0:56
+ * 只能在固定的时间段登录
+ */
+@lombok.RequiredArgsConstructor
+public class PeriodFilter extends RequestFilter {
+ private final PeriodProperties properties;
+
+ @Override
+ protected void internalFilter(HttpServletRequest request) throws ResponseStatusException {
+ if(!properties.test(LocalTime.now())) {
+ throw new ResponseStatusException(HttpStatus.FORBIDDEN, "不允许在此时段登录");
+ }
+ }
+}
\ No newline at end of file
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodProperties.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodProperties.java
new file mode 100644
index 0000000000000000000000000000000000000000..7acfd11bd8466a7b3f74e4e7f3a14e4ee0f8ba93
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/PeriodProperties.java
@@ -0,0 +1,56 @@
+package cn.seqdata.oauth2.filter;
+
+import java.util.LinkedList;
+import java.util.Objects;
+import java.util.function.Predicate;
+
+import org.apache.commons.lang3.ObjectUtils;
+import org.joda.time.DateTimeConstants;
+import org.joda.time.LocalTime;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * @author jrxian
+ * @date 2020/12/15 0:49
+ */
+@ConfigurationProperties("spring.security.filter.period")
+public class PeriodProperties extends LinkedList implements Predicate {
+
+ @Override
+ public boolean test(LocalTime time) {
+ if(isEmpty()) {
+ return true;
+ }
+
+ for(PeriodProperties.Item i : this) {
+ LocalTime start = ObjectUtils.defaultIfNull(i.start, LocalTime.MIDNIGHT);
+ LocalTime end = ObjectUtils.defaultIfNull(i.end, LocalTime.fromMillisOfDay(DateTimeConstants.MILLIS_PER_DAY - 1));
+ if(time.isAfter(start) && time.isBefore(end)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public static class Item {
+ private LocalTime start;
+ private LocalTime end;
+
+ public String getStart() {
+ return Objects.nonNull(start) ? start.toString() : null;
+ }
+
+ public void setStart(String start) {
+ this.start = Objects.nonNull(start) ? LocalTime.parse(start) : null;
+ }
+
+ public String getEnd() {
+ return Objects.nonNull(end) ? end.toString() : null;
+ }
+
+ public void setEnd(String end) {
+ this.end = Objects.nonNull(end) ? LocalTime.parse(end) : null;
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/RequestFilter.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/RequestFilter.java
new file mode 100644
index 0000000000000000000000000000000000000000..59aa511630d67efc068281d086a5052d5e33007a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/filter/RequestFilter.java
@@ -0,0 +1,62 @@
+package cn.seqdata.oauth2.filter;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.web.server.ResponseStatusException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * @author jrxian
+ * @date 2020/12/15 0:17
+ */
+public abstract class RequestFilter implements Filter {
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ protected abstract void internalFilter(HttpServletRequest request) throws ResponseStatusException;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+ try {
+ if(request instanceof HttpServletRequest) {
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ String requestURI = httpRequest.getRequestURI();
+ if(requestURI.startsWith("/oauth/token") || requestURI.startsWith("/connect")) {
+ internalFilter(httpRequest);
+ }
+ }
+ chain.doFilter(request, response);
+ } catch(ResponseStatusException ex) {
+ if(response instanceof HttpServletResponse) {
+ HttpServletResponse httpResponse = (HttpServletResponse) response;
+
+ HttpStatus httpStatus = ex.getStatus();
+ httpResponse.setStatus(httpStatus.value());
+
+ Map headers = ex.getHeaders();
+ headers.forEach(httpResponse::setHeader);
+
+ httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE);
+ objectMapper.writeValue(httpResponse.getOutputStream(), createErrorAttributes(ex));
+ }
+ }
+ }
+
+ private Map createErrorAttributes(ResponseStatusException rse) {
+ Map errorAttributes = new HashMap<>();
+
+ HttpStatus httpStatus = rse.getStatus();
+ errorAttributes.put("timestamp", System.currentTimeMillis());
+ errorAttributes.put("status", httpStatus.value());
+ errorAttributes.put("error", httpStatus.getReasonPhrase());
+ errorAttributes.put("message", rse.getReason());
+
+ return errorAttributes;
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/github/GithubUserProfile.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/github/GithubUserProfile.java
new file mode 100644
index 0000000000000000000000000000000000000000..fbad90ca1f4d019f8fddf2c5805b52db9c7c7b80
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/github/GithubUserProfile.java
@@ -0,0 +1,31 @@
+package cn.seqdata.oauth2.github;
+
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+import cn.seqdata.oauth2.provider.UserProfile;
+
+/**
+ * Author: jrxian
+ * Date: 2020-01-25 08:36
+ */
+public class GithubUserProfile extends UserProfile {
+
+ public GithubUserProfile(OAuth2User delegate) {
+ super(delegate);
+ }
+
+ @Override
+ public String getNickname() {
+ return getAttribute("name");
+ }
+
+ @Override
+ public String getUsername() {
+ return getAttribute("login");
+ }
+
+ @Override
+ public String getAvatar() {
+ return getAttribute("avatar_url");
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthenticationBean.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthenticationBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..588672a684bd408a45d1701002c112b93f11a3e7
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthenticationBean.java
@@ -0,0 +1,43 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.util.*;
+import java.util.function.Consumer;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 17:48
+ */
+@lombok.Getter
+@lombok.Setter
+@lombok.NoArgsConstructor
+public class AuthenticationBean implements Authentication, Consumer {
+ private String name;
+ private final UserBean principal = new UserBean();
+ private Object credentials;
+ private final Set roles = new HashSet<>();
+ private Map details = new HashMap<>();
+ private boolean authenticated;
+
+ @JsonIgnore
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return AuthorityConverter.toAuthorities(roles);
+ }
+
+ @Override
+ public void accept(Authentication authentication) {
+ this.name = authentication.getName();
+ this.principal.accept(authentication.getPrincipal());
+ this.credentials = authentication.getCredentials();
+ this.roles.addAll(AuthorityConverter.toRoles(authentication.getAuthorities()));
+ Object details = authentication.getDetails();
+ if(details instanceof Map) {
+ ((Map, ?>) details).forEach((k, v) -> this.details.put(String.valueOf(k), String.valueOf(v)));
+ }
+ this.authenticated = authentication.isAuthenticated();
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthorityConverter.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthorityConverter.java
new file mode 100644
index 0000000000000000000000000000000000000000..5fd1ddf3bee5cbe93622fd85db0638d2f70f820a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/AuthorityConverter.java
@@ -0,0 +1,28 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 17:56
+ */
+public final class AuthorityConverter {
+ private AuthorityConverter() {
+ }
+
+ public static Collection toAuthorities(Collection roles) {
+ return roles.stream()
+ .map(SimpleGrantedAuthority::new)
+ .collect(Collectors.toList());
+ }
+
+ public static Collection toRoles(Collection extends GrantedAuthority> authorities) {
+ return authorities.stream()
+ .map(GrantedAuthority::getAuthority)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/JsonSerializationStrategy.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/JsonSerializationStrategy.java
new file mode 100644
index 0000000000000000000000000000000000000000..3d1557c85148ff9cbb6c8bc465cd613767145d51
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/JsonSerializationStrategy.java
@@ -0,0 +1,38 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.io.IOException;
+
+import org.springframework.security.oauth2.provider.token.store.redis.StandardStringSerializationStrategy;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Author: jrxian
+ * Date: 2020-02-15 13:37
+ */
+public class JsonSerializationStrategy extends StandardStringSerializationStrategy {
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ public JsonSerializationStrategy() {
+ objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+ objectMapper.registerModule(new OAuth2Module());
+ }
+
+ @Override
+ protected byte[] serializeInternal(Object object) {
+ try {
+ return objectMapper.writeValueAsBytes(object);
+ } catch(IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+
+ @Override
+ protected T deserializeInternal(byte[] bytes, Class clazz) {
+ try {
+ return objectMapper.readValue(bytes, clazz);
+ } catch(IOException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationBean.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..2d57ab9ecb6d3178c8472fd952cfa533714703fc
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationBean.java
@@ -0,0 +1,29 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 18:04
+ */
+@lombok.Getter
+@lombok.Setter
+@lombok.NoArgsConstructor
+public class OAuth2AuthenticationBean implements Consumer, Supplier {
+ private final OAuth2RequestBean storedRequest = new OAuth2RequestBean();
+ private final AuthenticationBean userAuthentication = new AuthenticationBean();
+
+ @Override
+ public void accept(OAuth2Authentication wrapper) {
+ storedRequest.accept(wrapper.getOAuth2Request());
+ userAuthentication.accept(wrapper.getUserAuthentication());
+ }
+
+ @Override
+ public OAuth2Authentication get() {
+ return new OAuth2Authentication(storedRequest.get(), userAuthentication);
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationMixIn.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationMixIn.java
new file mode 100644
index 0000000000000000000000000000000000000000..66818be1c011e00baa76243ef56a993600ec868a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2AuthenticationMixIn.java
@@ -0,0 +1,17 @@
+package cn.seqdata.oauth2.jackson;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 00:21
+ */
+public class OAuth2AuthenticationMixIn {
+ @JsonCreator
+ public OAuth2AuthenticationMixIn(@JsonProperty("oauth2Request") OAuth2Request storedRequest,
+ @JsonProperty("userAuthentication") Authentication userAuthentication) {
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2Module.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2Module.java
new file mode 100644
index 0000000000000000000000000000000000000000..64f8d201a59bba279d41f9006ed412d2d3d1a157
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2Module.java
@@ -0,0 +1,39 @@
+package cn.seqdata.oauth2.jackson;
+
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Deserializer;
+import org.springframework.security.oauth2.common.OAuth2AccessTokenJackson2Serializer;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 00:16
+ */
+public class OAuth2Module extends SimpleModule {
+
+ public OAuth2Module() {
+ super(OAuth2Module.class.getSimpleName(), Version.unknownVersion());
+
+ setMixInAnnotation(SimpleGrantedAuthority.class, SimpleGrantedAuthorityMixIn.class);
+ setMixInAnnotation(User.class, UserMixIn.class);
+ setMixInAnnotation(UsernamePasswordAuthenticationToken.class, UsernamePasswordAuthenticationTokenMinIn.class);
+ setMixInAnnotation(OAuth2Request.class, OAuth2RequestMixIn.class);
+ setMixInAnnotation(OAuth2Authentication.class, OAuth2AuthenticationMixIn.class);
+
+ addAbstractTypeMapping(GrantedAuthority.class, SimpleGrantedAuthority.class);
+ addAbstractTypeMapping(UserDetails.class, UserBean.class);
+ addAbstractTypeMapping(Authentication.class, UsernamePasswordAuthenticationToken.class);
+
+ addSerializer(OAuth2AccessToken.class, new OAuth2AccessTokenJackson2Serializer());
+ addDeserializer(OAuth2AccessToken.class, new OAuth2AccessTokenJackson2Deserializer());
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestBean.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..4022c757075082f0a010f66afb457e499a55d9f1
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestBean.java
@@ -0,0 +1,57 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.springframework.security.oauth2.provider.OAuth2Request;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 17:38
+ */
+@lombok.Getter
+@lombok.Setter
+@lombok.NoArgsConstructor
+public class OAuth2RequestBean implements Consumer, Supplier {
+ private String clientId;
+ private final Set scope = new HashSet<>();
+ private final Map requestParameters = new HashMap<>();
+ private final Set resourceIds = new HashSet<>();
+ private final Set roles = new HashSet<>();
+ private boolean approved;
+ private String redirectUri;
+ private final Set responseTypes = new HashSet<>();
+ private final Map extensions = new HashMap<>();
+
+ @Override
+ public void accept(OAuth2Request wrapper) {
+ this.clientId = wrapper.getClientId();
+ this.scope.addAll(wrapper.getScope());
+ this.requestParameters.putAll(wrapper.getRequestParameters());
+ this.resourceIds.addAll(wrapper.getResourceIds());
+ this.roles.addAll(AuthorityConverter.toRoles(wrapper.getAuthorities()));
+ this.approved = wrapper.isApproved();
+ this.redirectUri = wrapper.getRedirectUri();
+ this.responseTypes.addAll(wrapper.getResponseTypes());
+ this.extensions.putAll(wrapper.getExtensions());
+ }
+
+ @Override
+ public OAuth2Request get() {
+ return new OAuth2Request(requestParameters,
+ clientId,
+ AuthorityConverter.toAuthorities(roles),
+ approved,
+ scope,
+ resourceIds,
+ redirectUri,
+ responseTypes,
+ extensions
+ );
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestMixIn.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestMixIn.java
new file mode 100644
index 0000000000000000000000000000000000000000..7557a144f3c58217840ae36ad9459fb7bb3a700e
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/OAuth2RequestMixIn.java
@@ -0,0 +1,31 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.security.core.GrantedAuthority;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 00:22
+ */
+@JsonIgnoreProperties("refresh")
+public class OAuth2RequestMixIn {
+ @JsonCreator
+ public OAuth2RequestMixIn(
+ @JsonProperty("requestParameters") Map requestParameters,
+ @JsonProperty("clientId") String clientId,
+ @JsonProperty("authorities") Collection extends GrantedAuthority> authorities,
+ @JsonProperty("approved") boolean approved,
+ @JsonProperty("scope") Set scope,
+ @JsonProperty("resourceIds") Set resourceIds,
+ @JsonProperty("redirectUri") String redirectUri,
+ @JsonProperty("responseTypes") Set responseTypes,
+ @JsonProperty("extensions") Map extensionProperties) {
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/SimpleGrantedAuthorityMixIn.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/SimpleGrantedAuthorityMixIn.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ec829fc3abe9487c3f41e223ca752d0b2d05f5f
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/SimpleGrantedAuthorityMixIn.java
@@ -0,0 +1,14 @@
+package cn.seqdata.oauth2.jackson;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 11:06
+ */
+public class SimpleGrantedAuthorityMixIn {
+ @JsonCreator
+ public SimpleGrantedAuthorityMixIn(@JsonProperty("authority") String authority) {
+ }
+}
diff --git a/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/UserBean.java b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/UserBean.java
new file mode 100644
index 0000000000000000000000000000000000000000..5f47f951881e11eb4fbf68d54b34fb0cb722360a
--- /dev/null
+++ b/seqdata-cloud-authz/src/main/java/cn/seqdata/oauth2/jackson/UserBean.java
@@ -0,0 +1,70 @@
+package cn.seqdata.oauth2.jackson;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.security.core.AuthenticatedPrincipal;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * @author jrxian
+ * @date 2020-11-15 23:42
+ */
+@lombok.Getter
+@lombok.Setter
+@lombok.NoArgsConstructor
+public class UserBean implements UserDetails, Consumer