diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java b/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java new file mode 100644 index 0000000..761ac25 --- /dev/null +++ b/flyfish-user/src/main/java/com/flyfish/framework/config/RSAAuthenticationManager.java @@ -0,0 +1,68 @@ +package com.flyfish.framework.config; + +import com.flyfish.framework.config.properties.SecurityProperties; +import com.flyfish.framework.utils.RSAUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.AbstractUserDetailsReactiveAuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + * 支持rsa的认证管理器 + * + * @author wangyu + */ +@Slf4j +public class RSAAuthenticationManager extends AbstractUserDetailsReactiveAuthenticationManager { + + private final ReactiveUserDetailsService userDetailsService; + + private final Boolean rsa; + + public RSAAuthenticationManager(SecurityProperties securityProperties, ReactiveUserDetailsService userDetailsService) { + Assert.notNull(userDetailsService, "userDetailsService cannot be null"); + this.rsa = securityProperties.isRsa(); + this.userDetailsService = userDetailsService; + } + + @Override + public Mono authenticate(Authentication authentication) { + if (rsa && !authentication.isAuthenticated()) { + Object credentials = authentication.getCredentials(); + if (credentials instanceof String) { + Authentication mapped = createAuthentication(authentication); + return super.authenticate(mapped); + } + } + return super.authenticate(authentication); + } + + private UsernamePasswordAuthenticationToken createAuthentication(Authentication authentication) throws IllegalArgumentException { + String password = (String) authentication.getCredentials(); + try { + password = RSAUtils.decrypt(password, RSAKeys.PRIVATE_KEY); + } catch (IllegalBlockSizeException | InvalidKeyException | BadPaddingException | NoSuchAlgorithmException | NoSuchPaddingException e) { + log.error("尝试解密密码出错", e); + throw new IllegalArgumentException("非法请求!密码格式校验失败!"); + } catch (IllegalArgumentException e) { + log.error("抛出参数异常", e); + throw new IllegalArgumentException("密码未加密,请求无效!" + e.getMessage()); + } + return new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), password, authentication.getAuthorities()); + } + + @Override + protected Mono retrieveUser(String username) { + return this.userDetailsService.findByUsername(username); + } +} diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/RSAKeys.java b/flyfish-user/src/main/java/com/flyfish/framework/config/RSAKeys.java new file mode 100644 index 0000000..891374f --- /dev/null +++ b/flyfish-user/src/main/java/com/flyfish/framework/config/RSAKeys.java @@ -0,0 +1,13 @@ +package com.flyfish.framework.config; + +/** + * rsa密钥,使用PKCS8的私钥格式 + * @author wangyu + */ +public interface RSAKeys { + + String PUBLIC_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDp4mkmGRzPqM/ilnmNiVzZVVnWvRQRZ/u45xeCwPowYBa+dJWby26CtApy9bz0szCPWZkgj3AGGaepdlwRyl8c1cVXGBsewf7rRZyMTWasW7YxNCDjAnlVo+NsZUqumK1UcK4EkEtsVI32EQMe+4BrkNEFJeEbeZWfRfEU20utBQIDAQAB"; + + String PRIVATE_KEY = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAOniaSYZHM+oz+KWeY2JXNlVWda9FBFn+7jnF4LA+jBgFr50lZvLboK0CnL1vPSzMI9ZmSCPcAYZp6l2XBHKXxzVxVcYGx7B/utFnIxNZqxbtjE0IOMCeVWj42xlSq6YrVRwrgSQS2xUjfYRAx77gGuQ0QUl4Rt5lZ9F8RTbS60FAgMBAAECgYA9aS2iy0U9YV30aP5Ro5e1mSuVrt/uEheOXV9W/cSznwdZytLdrXS+7PcxaETNG5/MhAIpAoCvmVzkqidSpIiEUSRjtQVehOmXBL7SsiVrsLO7FtCwWhgPY8xAOrorVgM1awNiDTz1GI7yVEx8ahL1i2jky6AcFXM9VmuC6GaIoQJBAP5kSqH/TkiuSLk2Pq4dczWldpgndxYykgl4XFbIzxDg+M8a7nDtFAf74QYMiJYepdoL6fnoidk2Waw3Pt3tFK8CQQDrXO4XeebvBMOowdVnOn9lJs8pMC47ajVS1h2jtzhueEhglK1hb6+xUNHAW9uvD6h17im+SSVcEWUdV94GRi6LAkBOIzwRWjwPcwj/arwB+yXCGpq8zZJ0jP/yTLgAKBoCEzgZVSpcIVX/xcwucXcatTs4KGSr0FNXFqygiy+SNxodAkAE1mUiYGQJt5xZ5JoBRCeIKbMKUq4wlG9CI5p6WfKz+o7qRUQHA6elylZ1UK4EPBsnSVzcR16YNNdSMhMjJ+AfAkBnQQQGj+Cg5ZcU/EM3/Gh5/DQ4D6OnyLR/hekQhoKmj4B18jMtuB0IMe4lHhY1raOPsxm/6y758ULv9yHj1yaP"; + +} diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java b/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java index 0d081c4..ad21266 100644 --- a/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java +++ b/flyfish-user/src/main/java/com/flyfish/framework/config/WebSecurityConfig.java @@ -21,8 +21,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.annotation.Order; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.ReactiveAuthenticationManager; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; @@ -31,7 +33,8 @@ import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.HttpStatusServerEntryPoint; import org.springframework.security.web.server.context.ServerSecurityContextRepository; import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository; -import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; + +import static org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers.pathMatchers; /** * @author wangyu @@ -85,21 +88,26 @@ public class WebSecurityConfig { * @return 结果 */ @Bean - public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ResultDataTransformer dataTransformer, - TokenProvider tokenProvider, SecurityProperties properties) { + @SuppressWarnings("all") + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, + ResultDataTransformer dataTransformer, + TokenProvider tokenProvider, + SecurityProperties properties, + ReactiveUserDetailsService userDetailsService) { http .securityContextRepository(contextRepository()) .authorizeExchange() .pathMatchers(properties.getAllowUris()).permitAll() - .pathMatchers("/api/logout").permitAll() + .pathMatchers("/api/logout", "/api/login").permitAll() .pathMatchers("/api/users/**").authenticated() .anyExchange().authenticated() .and() .formLogin() // 配置登录节点 + .authenticationManager(authenticationManager(properties, userDetailsService)) .authenticationEntryPoint(new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED)) .authenticationFailureHandler(new JsonAuthenticationFailureHandler(dataTransformer)) .authenticationSuccessHandler(new JsonAuthenticationSuccessHandler(dataTransformer)) - .requiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers(HttpMethod.POST, "/login", "/api/login")) + .requiresAuthenticationMatcher(pathMatchers(HttpMethod.POST, "/login", "/api/login")) .and() .logout() .logoutUrl("/api/logout") @@ -109,10 +117,23 @@ public class WebSecurityConfig { return http.build(); } + /** + * 构建鉴权管理器 + * + * @param userDetailsService 用户详情服务 + * @return 结果 + */ + public ReactiveAuthenticationManager authenticationManager(SecurityProperties securityProperties, + ReactiveUserDetailsService userDetailsService) { + RSAAuthenticationManager authenticationManager = new RSAAuthenticationManager(securityProperties, userDetailsService); + authenticationManager.setPasswordEncoder(passwordEncoder()); + return authenticationManager; + } + /** * 默认的初始化 * - * @param userService 用户服务 + * @param userService 用户服务 * @return 结果 */ @Bean diff --git a/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java b/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java index 93b7125..f3d3df6 100644 --- a/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java +++ b/flyfish-user/src/main/java/com/flyfish/framework/config/properties/SecurityProperties.java @@ -3,8 +3,6 @@ package com.flyfish.framework.config.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; -import java.util.List; - /** * 安全配置类 * @@ -16,4 +14,7 @@ public class SecurityProperties { // 允许的uris private String[] allowUris = new String[0]; + + // 启用rsa + private boolean rsa; } diff --git a/flyfish-user/src/main/java/com/flyfish/framework/controller/LoginController.java b/flyfish-user/src/main/java/com/flyfish/framework/controller/LoginController.java index 8fb0abd..056c007 100644 --- a/flyfish-user/src/main/java/com/flyfish/framework/controller/LoginController.java +++ b/flyfish-user/src/main/java/com/flyfish/framework/controller/LoginController.java @@ -1,15 +1,36 @@ package com.flyfish.framework.controller; +import com.flyfish.framework.bean.Result; +import com.flyfish.framework.config.RSAKeys; +import com.flyfish.framework.config.properties.SecurityProperties; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import javax.annotation.Resource; + /** * 登录controller + * * @author wangyu */ @RestController @RequestMapping("/login") public class LoginController { + @Resource + private SecurityProperties securityProperties; + /** + * 获取公钥,如果有的话 + * + * @return 结果 + */ + @GetMapping("") + public Result getPublicKey() { + if (securityProperties.isRsa()) { + return Result.ok(RSAKeys.PUBLIC_KEY); + } + return Result.error("尚未配置加密密钥!"); + } } diff --git a/flyfish-user/src/main/java/com/flyfish/framework/utils/RSAUtils.java b/flyfish-user/src/main/java/com/flyfish/framework/utils/RSAUtils.java new file mode 100644 index 0000000..f4447f9 --- /dev/null +++ b/flyfish-user/src/main/java/com/flyfish/framework/utils/RSAUtils.java @@ -0,0 +1,67 @@ +package com.flyfish.framework.utils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.lang.Nullable; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * 缺省的RSA,提供固定的公钥和私钥 + * + * @author wangyu + */ +@Slf4j +public class RSAUtils { + + @Nullable + private static PublicKey getPublicKey(String base64PublicKey) { + try { + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(base64PublicKey.getBytes())); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + log.error("获取公钥时出错,未找到加密算法:", e); + } catch (InvalidKeySpecException e) { + log.error("获取公钥时出错,不可用的密钥特征:", e); + } + return null; + } + + @Nullable + private static PrivateKey getPrivateKey(String base64PrivateKey) { + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(base64PrivateKey.getBytes())); + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + log.error("获取私钥时出错,未找到加密算法:", e); + } catch (InvalidKeySpecException e) { + log.error("获取私钥时出错,不可用的密钥特征:", e); + } + return null; + } + + public static byte[] encrypt(String data, String publicKey) throws BadPaddingException, IllegalBlockSizeException, InvalidKeyException, NoSuchPaddingException, NoSuchAlgorithmException { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, getPublicKey(publicKey)); + return cipher.doFinal(data.getBytes()); + } + + private static String decrypt(byte[] data, PrivateKey privateKey) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + return new String(cipher.doFinal(data)); + } + + public static String decrypt(String data, String base64PrivateKey) throws IllegalBlockSizeException, InvalidKeyException, BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException { + return decrypt(Base64.getDecoder().decode(data.getBytes()), getPrivateKey(base64PrivateKey)); + } +} diff --git a/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/JwtSecurityContextRepository.java b/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/JwtSecurityContextRepository.java index cd11055..1e02849 100644 --- a/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/JwtSecurityContextRepository.java +++ b/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/JwtSecurityContextRepository.java @@ -31,6 +31,8 @@ public class JwtSecurityContextRepository implements ServerSecurityContextReposi URI requestURI = request.getURI(); // 存在jwt时,校验jwt。redis也需要存在 if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt) && redisOperations.hasKey(jwt)) { + // token即将过期,续租 + Authentication authentication = tokenProvider.getAuthentication(jwt); log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), requestURI); return userDetailsService.findByUsername(authentication.getName()) diff --git a/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/TokenProvider.java b/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/TokenProvider.java index f58dd1b..fdc4186 100644 --- a/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/TokenProvider.java +++ b/flyfish-web/src/main/java/com/flyfish/framework/configuration/jwt/TokenProvider.java @@ -2,7 +2,6 @@ package com.flyfish.framework.configuration.jwt; import com.flyfish.framework.domain.base.IUser; import com.flyfish.framework.utils.RedisOperations; -import com.sun.org.apache.xml.internal.security.algorithms.SignatureAlgorithm; import io.jsonwebtoken.*; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.DecodingException; @@ -89,7 +88,8 @@ public class TokenProvider implements InitializingBean { HttpHeaders headers = exchange.getResponse().getHeaders(); // app用户从头部返回,方便获取 headers.add("Token", token); - headers.add("Token-Valid-Time", String.valueOf(tokenValidityInMilliseconds)); + headers.add("Token-Valid-Time", String.valueOf(remember ? tokenValidityInMillisecondsForRememberMe : + tokenValidityInMilliseconds)); // token在web端的时间较短,不允许记住,所以使用短期 // exchange.getResponse().addCookie(ResponseCookie.from(AUTHORIZATION_HEADER, "Bearer-" + token). // httpOnly(true).maxAge(tokenValidityInMilliseconds).build());