feat: 实现jwt认证接口
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java b/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java
new file mode 100644
index 0000000..26eea40
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/api/InfrasApiUserController.java
@@ -0,0 +1,55 @@
+package com.supwisdom.infras.security.api;
+
+import java.security.Principal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.userdetails.User;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.supwisdom.infras.security.authentication.JwtAuthenticationToken;
+import com.supwisdom.infras.security.core.userdetails.InfrasUser;
+
+@RestController
+@RequestMapping("/api/user")
+public class InfrasApiUserController {
+
+ private static final Logger logger = LoggerFactory.getLogger(InfrasApiUserController.class);
+
+ @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @ResponseBody
+ public InfrasUser current(Principal principal) {
+
+ logger.debug("ApiUserController.current(Principal) is {}", principal);
+
+ if (principal == null) {
+ throw new RuntimeException("exception.principal.is.null");
+ }
+
+ if (principal instanceof UsernamePasswordAuthenticationToken) {
+
+ UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) principal;
+
+ if (token.isAuthenticated()) {
+ if (token.getPrincipal() instanceof User) {
+ return (InfrasUser) token.getPrincipal();
+ }
+ }
+ } else if (principal instanceof JwtAuthenticationToken) {
+ JwtAuthenticationToken token = (JwtAuthenticationToken) principal;
+
+ InfrasUser user = new InfrasUser((String) token.getPrincipal(), (String) token.getToken(), token.getAuthorities(), token.getAttributes());
+ user.eraseCredentials();
+
+ return user;
+ }
+
+ throw new RuntimeException("exception.principal.not.correct");
+ }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java b/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java
new file mode 100644
index 0000000..73a1944
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/authentication/JwtAuthenticationToken.java
@@ -0,0 +1,74 @@
+package com.supwisdom.infras.security.authentication;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+import org.springframework.security.authentication.AbstractAuthenticationToken;
+import org.springframework.security.core.GrantedAuthority;
+
+public class JwtAuthenticationToken extends AbstractAuthenticationToken {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 4407107653933017523L;
+
+ private Object principal;
+ private String credentials;
+ private String token;
+
+ private final Map<String, Object> attributes;
+
+ public Map<String, Object> getAttributes() {
+ return this.attributes;
+ }
+
+ public JwtAuthenticationToken(String token) {
+ super(Collections.emptyList());
+ this.token = token;
+
+ this.attributes = Collections.emptyMap();
+
+ setAuthenticated(false);
+ }
+
+ public JwtAuthenticationToken(Object principal, String token,
+ Collection<? extends GrantedAuthority> authorities) {
+ this(principal, token, authorities, Collections.emptyMap());
+ }
+
+ public JwtAuthenticationToken(Object principal, String token,
+ Collection<? extends GrantedAuthority> authorities,
+ Map<? extends String, ? extends Object> attributes) {
+ super(authorities);
+ this.principal = principal;
+ this.token = token;
+
+ this.attributes = Collections.unmodifiableMap(attributes);
+
+ setAuthenticated(true);
+ }
+
+ @Override
+ public void setDetails(Object details) {
+ super.setDetails(details);
+
+ this.setAuthenticated(true);
+ }
+
+ @Override
+ public Object getCredentials() {
+ return credentials;
+ }
+
+ @Override
+ public Object getPrincipal() {
+ return principal;
+ }
+
+ public String getToken() {
+ return token;
+ }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java b/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java
new file mode 100644
index 0000000..8764974
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/core/userdetails/InfrasUser.java
@@ -0,0 +1,49 @@
+package com.supwisdom.infras.security.core.userdetails;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.User;
+
+public class InfrasUser extends User {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 6535845256804630918L;
+
+ public InfrasUser(String username, String password,
+ Collection<? extends GrantedAuthority> authorities,
+ Map<? extends String, ? extends Object> attributes) {
+ this(username, password, true, true, true, true, authorities, attributes);
+
+ }
+
+ public InfrasUser(String username, String password,
+ boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked,
+ Collection<? extends GrantedAuthority> authorities,
+ Map<? extends String, ? extends Object> attributes) {
+ super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
+
+ this.attributes = Collections.unmodifiableMap(attributes);
+ }
+
+ private final Map<String, Object> attributes;
+ public Map<String, Object> getAttributes() {
+ return this.attributes;
+ }
+
+ public List<String> getRoles() {
+ List<String> roles = new ArrayList<>();
+ for (GrantedAuthority grantedAuthority : this.getAuthorities()) {
+ roles.add(grantedAuthority.getAuthority());
+ }
+
+ return roles;
+ }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
index aa792e1..76cc927 100644
--- a/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/basic/BasicWebFluxSecurityConfiguration.java
@@ -10,7 +10,7 @@
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
@Configuration
-@ConditionalOnProperty(name="infras.security.basic.enabled", havingValue="true")
+@ConditionalOnProperty(name="infras.security.basic.reactive.enabled", havingValue="true")
public class BasicWebFluxSecurityConfiguration {
@Bean
@@ -28,10 +28,9 @@
//http.addFilterAt(webFilter, SecurityWebFiltersOrder.LAST);
http.httpBasic();
-
- http.csrf().disable();
-
http.formLogin().disable();
+
+ http.csrf().disable();
return http.build();
}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java
new file mode 100644
index 0000000..51062b2
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/EnableInfrasJWTWebFluxApi.java
@@ -0,0 +1,17 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(JWTWebFluxConfiguration.class)
+public @interface EnableInfrasJWTWebFluxApi {
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java
new file mode 100644
index 0000000..3c4b036
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTKeyController.java
@@ -0,0 +1,25 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@RestController
+public class JWTKeyController {
+
+ @Autowired
+ private JWTTokenUtil jwtTokenUtil;
+
+ /**
+ * curl -i -s -X GET 'http://localhost:8080/jwt/publicKey'
+ * @return
+ */
+ @GetMapping(value = "/jwt/publicKey")
+ public String publicKey() {
+
+ return jwtTokenUtil.getPublicKeyPem();
+ }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java
new file mode 100644
index 0000000..bf2f6a7
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTSecurityContextRepository.java
@@ -0,0 +1,118 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import io.jsonwebtoken.Claims;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.server.reactive.ServerHttpRequest;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextImpl;
+import org.springframework.security.web.server.context.ServerSecurityContextRepository;
+import org.springframework.web.server.ServerWebExchange;
+
+import com.supwisdom.infras.security.authentication.JwtAuthenticationToken;
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+import reactor.core.publisher.Mono;
+
+public class JWTSecurityContextRepository implements ServerSecurityContextRepository {
+
+ private static final Logger logger = LoggerFactory.getLogger(JWTSecurityContextRepository.class);
+
+ @Value("${infras.security.jwt.token.authorization.prefix:Bearer}")
+ private String authorizationPrefix;
+
+ private JWTTokenUtil jwtTokenUtil;
+
+ @Autowired
+ public JWTSecurityContextRepository(JWTTokenUtil jwtTokenUtil) {
+ this.jwtTokenUtil = jwtTokenUtil;
+ }
+
+ @Override
+ public Mono<Void> save(ServerWebExchange exchange, SecurityContext context) {
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono<SecurityContext> load(ServerWebExchange exchange) {
+
+ ServerHttpRequest request = exchange.getRequest();
+
+ String authToken = null;
+
+ String authParamter = request.getQueryParams().getFirst("token"); logger.debug("authParamter is [{}]", authParamter);
+ if (authParamter != null && !authParamter.isEmpty()) {
+ authToken = authParamter;
+ }
+
+ if (authToken == null) {
+ String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION); logger.debug("authHeader is [{}]", authHeader);
+ if (authHeader != null && authHeader.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+ authToken = authHeader.substring(authorizationPrefix.length() + 1);
+ }
+ }
+
+ logger.debug("authToken is [{}]", authToken);
+
+ if (authToken != null && !authToken.isEmpty()) {
+
+ String username = getUsernameFromToken(authToken);
+
+ if (username != null) {
+ List<GrantedAuthority> authorities = getAuthoritiesFromToken(authToken);
+
+ Authentication authentication = new JwtAuthenticationToken(username, authToken, authorities);
+
+ return Mono.justOrEmpty(new SecurityContextImpl(authentication));
+ }
+
+ }
+
+ return Mono.empty();
+ }
+
+
+ /**
+ * 从令牌中获取用户名
+ *
+ * @param token 令牌
+ * @return 用户名
+ */
+ private String getUsernameFromToken(String token) {
+ String username;
+ try {
+ Claims claims = jwtTokenUtil.getClaimsFromToken(token);
+ username = claims.getSubject();
+ } catch (Exception e) {
+ username = null;
+ }
+ return username;
+ }
+
+ private List<GrantedAuthority> getAuthoritiesFromToken(String token) {
+ List<GrantedAuthority> collAuthorities = new ArrayList<GrantedAuthority>();
+ try {
+ Claims claims = jwtTokenUtil.getClaimsFromToken(token);
+ String roles = claims.get("roles", String.class);
+
+ for (String role : roles.split(",")) {
+ collAuthorities.add(new SimpleGrantedAuthority(role));
+ }
+ } catch (Exception e) {
+ collAuthorities = new ArrayList<GrantedAuthority>();
+ }
+ return collAuthorities;
+ }
+
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java
new file mode 100644
index 0000000..9a357f6
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTTokenController.java
@@ -0,0 +1,232 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.util.MimeTypeUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RestController;
+
+import reactor.core.publisher.Mono;
+
+import com.supwisdom.infras.security.configure.jwt.util.SignUtil;
+import com.supwisdom.infras.security.core.userdetails.InfrasUser;
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@RestController
+public class JWTTokenController {
+
+ private static final Logger logger = LoggerFactory.getLogger(JWTTokenController.class);
+
+ @Value("${infras.security.jwt.token.sign.enabled:false}")
+ private boolean signEnabled;
+ @Value("${infras.security.jwt.token.sign.key:}")
+ private String signKey;
+
+ @Value("${infras.security.jwt.token.authorization.prefix:Bearer}")
+ private String authorizationPrefix;
+
+ @Autowired
+ private ReactiveAuthenticationManager reactiveAuthenticationManager;
+ @Autowired
+ private ReactiveUserDetailsService reactiveUserDetailsService;
+ @Autowired
+ private JWTTokenUtil jwtTokenUtil;
+
+ /**
+ * 用户登录
+ *
+ * curl -i -s -X POST 'http://localhost:8080/jwt/token/login' -d 'username=user&password=user' -H 'Content-Type: application/json'
+ *
+ * @param username 用户名
+ * @param password 密码
+ * @return 操作结果
+ * @throws AuthenticationException 错误信息
+ */
+ @PostMapping(path = "/jwt/token/login", consumes = MimeTypeUtils.APPLICATION_JSON_VALUE)
+ public Mono<String> login(
+ @RequestBody LoginRequest loginRequest
+ ) throws AuthenticationException {
+
+ return Mono.just(loginRequest).flatMap(req -> {
+ try {
+ String username = req.getUsername();
+ String password = req.getPassword();
+
+ // TODO:判断用户的登录频次
+
+ UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
+
+ return reactiveAuthenticationManager.authenticate(upToken);
+ } catch (AuthenticationException e) {
+ // TODO: 记录失败
+ throw e;
+ }
+ }).filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof InfrasUser)
+ .map(Authentication::getPrincipal)
+ .cast(InfrasUser.class)
+ .map(userDetails -> {
+ String jwtToken = generateToken(userDetails);
+ return jwtToken;
+ })
+ ;
+
+ // 判断签名
+// if (signEnabled) {
+// String timestamp = request.queryParam("timestamp").get();
+// String sign = request.queryParam("sign").get();
+//
+// if (timestamp == null || timestamp.isEmpty()) {
+// throw new BadCredentialsException("timestamp error");
+// }
+//
+// if (sign == null || sign.isEmpty()) {
+// throw new BadCredentialsException("sign error");
+// }
+//
+// String data = timestamp + username + password;
+//
+// if (!SignUtil.checkIntervalTime(timestamp, 10000)) {
+// throw new BadCredentialsException("timestamp error");
+// }
+//
+// String currentSign = SignUtil.HMACSHA1(data, signKey);
+// if (!currentSign.equals(sign)) {
+// throw new BadCredentialsException("sign error");
+// }
+// }
+
+// String username = loginRequest.getUsername();
+// String password = loginRequest.getPassword();
+//
+// // TODO:判断用户的登录频次
+//
+// try {
+// UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken(username, password);
+//
+// Mono<String> token =
+// reactiveAuthenticationManager.authenticate(upToken)
+// .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof UserDetails)
+// .map(Authentication::getPrincipal)
+// .cast(UserDetails.class)
+// .map(userDetails -> {
+// String jwtToken = generateToken(userDetails);
+// return jwtToken;
+// });
+//
+//// reactiveAuthenticationManager.authenticate(upToken)
+//// .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof String)
+//// .map(Authentication::getPrincipal)
+//// .cast(String.class)
+//// .map(principal -> {
+//// return reactiveUserDetailsService.findByUsername(principal)
+//// .map(userDetails -> {
+//// String jwtToken = generateToken(userDetails);
+//// return jwtToken;
+//// });
+//// });
+//
+// return token.block();
+//
+//// reactiveAuthenticationManager.authenticate(upToken)
+//// .filter(a -> a.isAuthenticated() && a.getPrincipal() instanceof UserDetails)
+//// .map(Authentication::getPrincipal)
+//// .cast(UserDetails.class)
+//// .map(userDetails -> {
+//// String jwtToken = generateToken(userDetails);
+//// return jwtToken;
+//// })
+//// ;
+//
+// } catch (AuthenticationException e) {
+// // TODO: 记录失败
+// throw e;
+// }
+//
+// //throw new UsernameNotFoundException(String.format("%s not found", username));
+ }
+
+
+ /**
+ * 生成令牌
+ *
+ * @param userDetails 用户
+ * @return 令牌
+ */
+ private String generateToken(InfrasUser userDetails) {
+ String roles = "";
+ for (GrantedAuthority ga : userDetails.getAuthorities()) {
+ String role = ga.getAuthority();
+ roles += (roles.length() > 0 ? "," : "") + role;
+ }
+
+ Map<String, Object> claims = new HashMap<>();
+ claims.put("sub", userDetails.getUsername());
+ claims.put("roles", roles);
+ claims.put("created", new Date());
+ return jwtTokenUtil.generateToken(claims);
+ }
+
+
+ /**
+ * 刷新密钥
+ *
+ * curl -i -s -X GET 'http://localhost:8080/jwt/token/refreshToken' -H 'Authorization: Bearer JWTToken'
+ *
+ * @param authorization 原密钥
+ * @return 新密钥
+ * @throws AuthenticationException 错误信息
+ */
+ @GetMapping(path = "/jwt/token/refreshToken")
+ public String refreshToken(@RequestHeader("Authorization") String authorization) throws AuthenticationException {
+
+ if (authorization == null || !authorization.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+ return "authorization error";
+ }
+
+ String token = authorization.substring(authorizationPrefix.length() + 1);
+ //if (!jwtTokenUtil.isTokenExpired(token)) {
+ return jwtTokenUtil.refreshToken(token);
+ //}
+
+ //return "error";
+ }
+
+ /**
+ * 注销密钥
+ *
+ * curl -i -s -X GET 'http://localhost:8080/jwt/token/logout' -H 'Authorization: Bearer JWTToken'
+ *
+ * @param authorization 原密钥
+ * @return
+ * @throws AuthenticationException
+ */
+ @GetMapping(path = "/jwt/token/logout")
+ public String expireToken(@RequestHeader("Authorization") String authorization) throws AuthenticationException {
+
+ if (authorization == null || !authorization.toLowerCase().startsWith(authorizationPrefix.toLowerCase())) {
+ return "authorization error";
+ }
+
+ String token = authorization.substring(authorizationPrefix.length() + 1);
+
+ jwtTokenUtil.expireToken(token);
+
+ return "success";
+ }
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java
new file mode 100644
index 0000000..014e483
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/JWTWebFluxConfiguration.java
@@ -0,0 +1,141 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.security.authentication.ReactiveAuthenticationManager;
+import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
+import org.springframework.security.config.web.server.ServerHttpSecurity;
+import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.server.SecurityWebFilterChain;
+import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers;
+
+import com.supwisdom.infras.security.utils.JWTTokenUtil;
+
+@Configuration
+@ConditionalOnProperty(name="infras.security.jwt.reactive.enabled", havingValue="true")
+public class JWTWebFluxConfiguration {
+
+ private static final Logger logger = LoggerFactory.getLogger(JWTWebFluxConfiguration.class);
+
+// @ConditionalOnClass(RedisConnectionFactory.class)
+// @Configuration
+// public static class RedisStoreConfiguration {
+//
+// @Bean
+// public JWTTokenRedisStore jwtTokenRedisStore(RedisConnectionFactory connectionFactory) {
+//
+// JWTTokenRedisStore jwtTokenRedisStore = new JWTTokenRedisStore(connectionFactory);
+//
+// return jwtTokenRedisStore;
+// }
+//
+// }
+
+ @Autowired(required = false)
+ private ReactiveUserDetailsService reactiveUserDetailsService;
+
+ @Autowired(required = false)
+ private PasswordEncoder passwordEncoder;
+
+ @Bean
+ public ReactiveAuthenticationManager reactiveAuthenticationManager() {
+ if(this.reactiveUserDetailsService != null) {
+ UserDetailsRepositoryReactiveAuthenticationManager manager =
+ new UserDetailsRepositoryReactiveAuthenticationManager(this.reactiveUserDetailsService);
+ if(this.passwordEncoder != null) {
+ manager.setPasswordEncoder(this.passwordEncoder);
+ }
+ return manager;
+ }
+ return null;
+ }
+
+ @Bean
+ public JWTTokenUtil jwtTokenUtil() {
+
+ return new JWTTokenUtil();
+ }
+
+ @Bean
+ JWTKeyController jwtKeyController() {
+
+ JWTKeyController jwtKeyController = new JWTKeyController();
+ logger.debug("JWTWebFluxConfiguration jwtKeyController is {}", jwtKeyController);
+
+ return jwtKeyController;
+ }
+
+
+
+ @Bean
+ public JWTTokenController jwtTokenController() {
+
+ JWTTokenController jwtTokenController = new JWTTokenController();
+ logger.debug("JWTWebFluxConfiguration jwtTokenController is {}", jwtTokenController);
+
+ return jwtTokenController;
+ }
+
+ @Bean
+ public JWTSecurityContextRepository jwtSecurityContextRepository(JWTTokenUtil jwtTokenUtil) {
+ JWTSecurityContextRepository jwtSecurityContextRepository = new JWTSecurityContextRepository(jwtTokenUtil);
+
+ return jwtSecurityContextRepository;
+ }
+
+
+ @Bean
+ public SecurityWebFilterChain jwtTokenSpringSecurityFilterChain(ServerHttpSecurity http) {
+
+ logger.debug("jwtTokenSpringSecurityFilterChain(ServerHttpSecurity)");
+
+ http
+ .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/jwt/**"))
+ .authorizeExchange()
+ .pathMatchers(HttpMethod.OPTIONS).permitAll()
+ .pathMatchers("/jwt/publicKey").permitAll()
+ .pathMatchers("/jwt/**").permitAll()
+ .anyExchange().authenticated();
+
+ http.httpBasic().disable();
+ http.formLogin().disable();
+
+ http.csrf().disable();
+
+ return http.build();
+ }
+
+
+
+ @Bean
+ public SecurityWebFilterChain jwtApiSpringSecurityFilterChain(ServerHttpSecurity http) {
+ http
+ .securityMatcher(ServerWebExchangeMatchers.pathMatchers("/api/**"))
+ .authorizeExchange()
+ .pathMatchers(HttpMethod.OPTIONS).permitAll()
+ .pathMatchers("/api/public/**", "/api/open/**").permitAll()
+ .pathMatchers("/api/v*/public/**", "/api/v*/open/**").permitAll()
+ .pathMatchers("/api/*/v*/public/**", "/api/*/v*/open/**").permitAll()
+ .pathMatchers("/api/**").authenticated()
+ .anyExchange().authenticated();
+
+ //http.addFilterAt(webFilter, SecurityWebFiltersOrder.LAST);
+
+ http.securityContextRepository(jwtSecurityContextRepository(jwtTokenUtil()));
+
+ http.httpBasic().disable();
+ http.formLogin().disable();
+
+ http.csrf().disable();
+
+ return http.build();
+ }
+
+
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java
new file mode 100644
index 0000000..2e47c2c
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/reactive/jwt/LoginRequest.java
@@ -0,0 +1,13 @@
+package com.supwisdom.infras.security.reactive.jwt;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class LoginRequest {
+ private String username;
+ private String password;
+}
diff --git a/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java b/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java
new file mode 100644
index 0000000..abbf378
--- /dev/null
+++ b/gateway/src/main/java/com/supwisdom/infras/security/utils/JWTTokenUtil.java
@@ -0,0 +1,261 @@
+package com.supwisdom.infras.security.utils;
+
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.SignatureAlgorithm;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+//import org.springframework.security.core.GrantedAuthority;
+//import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.stereotype.Component;
+
+import com.supwisdom.infras.security.cert.CertUtil;
+import com.supwisdom.infras.security.token.store.redis.JWTTokenRedisStore;
+
+@Component
+public class JWTTokenUtil implements InitializingBean {
+
+ private static final Logger logger = LoggerFactory.getLogger(JWTTokenUtil.class);
+
+ private static ConcurrentMap<String, Long> mapTokenExpiration = new ConcurrentHashMap<String, Long>();
+
+ @Autowired(required = false)
+ private JWTTokenRedisStore redisTokenStore;
+
+ /**
+ * 密钥
+ */
+ //@Value("${infras.security.jwt.secret:MyJwtSecret}")
+ //private String secret;
+
+
+ @Value("${infras.security.jwt.iss:supwisdom}")
+ private String issuer;
+ @Value("${infras.security.jwt.jti:supwisdom-jwt}")
+ private String jti;
+
+
+ @Value("${infras.security.jwt.expiration:2592000}")
+ private Long expiration;
+
+ @Value("${infras.security.jwt.kickout.enabled:false}")
+ private boolean kickoutEnabled;
+
+
+ @Value("${infras.security.jwt.key-alias:supwisdom-jwt-key}")
+ private String keyAlias;
+ @Value("${infras.security.jwt.key-password:kingstar}")
+ private String keyPassword;
+
+ @Value("${infras.security.jwt.key-store:}")
+ private String keyStore;
+ @Value("${infras.security.jwt.key-store-password:kingstar}")
+ private String keyStorePassword;
+
+
+ @Value("${infras.security.jwt.public-key-pem:}")
+ private String publicKeyPem;
+ @Value("${infras.security.jwt.private-key-pem-pkcs8:}")
+ private String privateKeyPemPKCS8;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ this.initKey();
+ }
+
+ private KeyPair keyPair;
+
+ public void initKey() {
+
+ try {
+ this.keyPair = CertUtil.initKeyFromPem(publicKeyPem, privateKeyPemPKCS8);
+ logger.debug("init keyPair from pem");
+ return;
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+
+ try {
+ this.keyPair = CertUtil.initKeyFromKeyStore(keyStore, keyStorePassword, keyAlias, keyPassword);
+ logger.debug("init keyPair from keyStore");
+ } catch (UnrecoverableKeyException e) {
+ e.printStackTrace();
+ } catch (KeyStoreException e) {
+ e.printStackTrace();
+ } catch (CertificateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ public RSAPublicKey getPublicKey() {
+ return (RSAPublicKey) this.keyPair.getPublic();
+ }
+
+ public RSAPrivateKey getPrivateKey() {
+ return (RSAPrivateKey) this.keyPair.getPrivate();
+ }
+
+ public String getPublicKeyPem() {
+ return CertUtil.publicKeyToPem(getPublicKey());
+ }
+
+ private void storeTokenExpiration(String token, Long expiration) {
+ if (!kickoutEnabled) {
+ return;
+ }
+
+ logger.debug("store <token, expiration> to Map");
+ mapTokenExpiration.put(token, expiration); // FIXME: 存储到 redis 或 数据库
+
+ if (redisTokenStore != null) {
+ logger.debug("store <token, expiration> to Redis");
+ redisTokenStore.storeTokenExpiration(token, expiration);
+ }
+ }
+
+ private Long loadTokenExpiration(String token) {
+ if (!kickoutEnabled) {
+ return Long.MAX_VALUE;
+ }
+
+ if (redisTokenStore != null) {
+ logger.debug("load <token, expiration> from Redis");
+ return redisTokenStore.loadTokenExpiration(token, -1L);
+ }
+
+ logger.debug("load <token, expiration> from Map");
+ return mapTokenExpiration.getOrDefault(token, -1L); // FIXME: 存储到 redis 或 数据库
+ }
+
+ /**
+ * 从数据声明生成令牌
+ *
+ * @param claims 数据声明
+ * @return 令牌
+ */
+ public String generateToken(Map<String, Object> claims) {
+ Date expirationDate = new Date(System.currentTimeMillis() + expiration * 1000);
+ String token = Jwts.builder()
+ .setClaims(claims)
+ .setIssuer(issuer)
+ .setId(jti)
+ .setIssuedAt(new Date(System.currentTimeMillis()))
+ .setExpiration(expirationDate)
+ .signWith(SignatureAlgorithm.RS512, this.getPrivateKey())
+ .compact();
+
+ storeTokenExpiration(token, expirationDate.getTime());
+
+ return token;
+ }
+
+ /**
+ * 从令牌中获取数据声明
+ *
+ * @param token 令牌
+ * @return 数据声明
+ */
+ public Claims getClaimsFromToken(String token) {
+ Claims claims;
+ try {
+ claims = Jwts.parser().setSigningKey(this.getPublicKey()).parseClaimsJws(token).getBody();
+ } catch (Exception e) {
+ claims = null;
+ }
+ return claims;
+ }
+
+ /**
+ * 判断令牌是否过期
+ *
+ * @param token 令牌
+ * @return 是否过期
+ */
+ public Boolean isTokenExpired(String token) {
+ try {
+ Claims claims = getClaimsFromToken(token);
+ Date expiration = claims.getExpiration();
+
+ Date now = new Date();
+
+ // 判断存储中的 token 是否过期
+ Long expirationTimeMillis = loadTokenExpiration(token);
+ if (expirationTimeMillis < now.getTime()) {
+ return true;
+ }
+
+ return expiration.before(now);
+ } catch (Exception e) {
+ return false; // FIXME: ?
+ }
+ }
+
+ /**
+ * 刷新令牌
+ *
+ * @param token 原令牌
+ * @return 新令牌
+ */
+ public String refreshToken(String token) {
+ String refreshedToken;
+ try {
+ Claims claims = getClaimsFromToken(token);
+ claims.put("created", new Date());
+ refreshedToken = generateToken(claims);
+ } catch (Exception e) {
+ refreshedToken = null;
+ }
+ return refreshedToken;
+ }
+
+ /**
+ * 验证令牌
+ *
+ * @param token 令牌
+ * @param userDetails 用户
+ * @return 是否有效
+ */
+ public Boolean validateToken(String token, String username) {
+ Claims claims = getClaimsFromToken(token);
+ String sub = claims.getSubject();
+
+ return (sub.equals(username) && !isTokenExpired(token));
+ }
+
+ /**
+ * 踢出令牌
+ * @param token
+ */
+ public void expireToken(String token) {
+ if (!isTokenExpired(token)) {
+ storeTokenExpiration(token, -1L);
+ }
+ }
+
+}
diff --git a/gateway/src/main/resources/META-INF/spring.factories b/gateway/src/main/resources/META-INF/spring.factories
new file mode 100644
index 0000000..9a872fd
--- /dev/null
+++ b/gateway/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,3 @@
+# Auto Configure
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+com.supwisdom.infras.security.reactive.InfrasSecurityReactiveAutoConfiguration
\ No newline at end of file