From: 刘洪青 Date: Tue, 10 Sep 2019 10:57:49 +0000 (+0800) Subject: feat: 实现jwt认证接口 X-Git-Tag: v0.0.1^2~41 X-Git-Url: https://source.supwisdom.com/gerrit/gitweb?a=commitdiff_plain;h=7d5d22137f983a377c3a41fe1557742684a06648;p=institute%2Fsw-backend.git 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 attributes; + + public Map 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 authorities) { + this(principal, token, authorities, Collections.emptyMap()); + } + + public JwtAuthenticationToken(Object principal, String token, + Collection authorities, + Map 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 authorities, + Map 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 authorities, + Map attributes) { + super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); + + this.attributes = Collections.unmodifiableMap(attributes); + } + + private final Map attributes; + public Map getAttributes() { + return this.attributes; + } + + public List getRoles() { + List 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.SecurityWebFilterChain; 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 @@ public class BasicWebFluxSecurityConfiguration { //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 save(ServerWebExchange exchange, SecurityContext context) { + return Mono.empty(); + } + + @Override + public Mono 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 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 getAuthoritiesFromToken(String token) { + List collAuthorities = new ArrayList(); + 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(); + } + 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 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 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 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 mapTokenExpiration = new ConcurrentHashMap(); + + @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 to Map"); + mapTokenExpiration.put(token, expiration); // FIXME: 存储到 redis 或 数据库 + + if (redisTokenStore != null) { + logger.debug("store to Redis"); + redisTokenStore.storeTokenExpiration(token, expiration); + } + } + + private Long loadTokenExpiration(String token) { + if (!kickoutEnabled) { + return Long.MAX_VALUE; + } + + if (redisTokenStore != null) { + logger.debug("load from Redis"); + return redisTokenStore.loadTokenExpiration(token, -1L); + } + + logger.debug("load from Map"); + return mapTokenExpiration.getOrDefault(token, -1L); // FIXME: 存储到 redis 或 数据库 + } + + /** + * 从数据声明生成令牌 + * + * @param claims 数据声明 + * @return 令牌 + */ + public String generateToken(Map 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