From c88f9d95d45096fcce970f8d404a22a0b8865eb6 Mon Sep 17 00:00:00 2001 From: EightMonth Date: Tue, 19 Mar 2024 17:16:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=89=8B=E5=8A=A8=E7=94=9F?= =?UTF-8?q?=E6=88=90token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/jeecg/common/system/util/JwtUtil.java | 27 ++- .../org/jeecg/config/security/LoginType.java | 2 + .../jeecg/config/security/SecurityConfig.java | 14 +- .../self/SelfAuthenticationProvider.java | 217 ++++++++++++++++++ .../self/SelfAuthenticationToken.java | 19 ++ .../modules/system/test/InsertDemoTest.java | 16 ++ 6 files changed, 282 insertions(+), 13 deletions(-) create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationProvider.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationToken.java diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java index 6958e855..2a2d9359 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/system/util/JwtUtil.java @@ -12,10 +12,7 @@ import com.google.common.base.Joiner; import java.io.IOException; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -37,12 +34,16 @@ import org.jeecg.common.system.vo.SysUserCacheInfo; import org.jeecg.common.util.DateUtils; import org.jeecg.common.util.SpringContextUtils; import org.jeecg.common.util.oConvertUtils; +import org.jeecg.config.security.self.SelfAuthenticationProvider; +import org.jeecg.config.security.self.SelfAuthenticationToken; import org.jeecg.config.security.utils.SecureUtil; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.*; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder; @@ -124,17 +125,25 @@ public class JwtUtil { } /** - * 生成签名,5min后过期(暂未实现) + * 生成token * * @param username 用户名 * @param secret 用户的密码 * @return 加密的token */ public static String sign(String username, String secret) { - Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME); - Algorithm algorithm = Algorithm.HMAC256(secret); - // 附带username信息 - return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm); + Map additionalParameter = new HashMap<>(); + additionalParameter.put("username", username); + + RegisteredClientRepository registeredClientRepository = SpringContextUtils.getBean(RegisteredClientRepository.class); + SelfAuthenticationProvider selfAuthenticationProvider = SpringContextUtils.getBean(SelfAuthenticationProvider.class); + + OAuth2ClientAuthenticationToken client = new OAuth2ClientAuthenticationToken(Objects.requireNonNull(registeredClientRepository.findByClientId("jeecg-client")), ClientAuthenticationMethod.CLIENT_SECRET_BASIC, null); + client.setAuthenticated(true); + SelfAuthenticationToken selfAuthenticationToken = new SelfAuthenticationToken(client, additionalParameter); + selfAuthenticationToken.setAuthenticated(true); + OAuth2AccessTokenAuthenticationToken accessToken = (OAuth2AccessTokenAuthenticationToken) selfAuthenticationProvider.authenticate(selfAuthenticationToken); + return accessToken.getAccessToken().getTokenValue(); } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java index c5ee9620..26f39a2e 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/LoginType.java @@ -33,4 +33,6 @@ public class LoginType { * 所有联合登录,比如github\钉钉\企业微信\微信 */ public static final String SOCIAL = "social"; + + public static final String SELF = "self"; } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java index f4dd531d..d5e3195e 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/SecurityConfig.java @@ -37,17 +37,16 @@ import org.springframework.security.oauth2.server.authorization.settings.Authori import org.springframework.security.oauth2.server.authorization.token.*; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; -import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; import org.springframework.web.cors.CorsConfiguration; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.security.SecureRandom; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.util.Arrays; -import java.util.UUID; /** * spring authorization server核心配置 @@ -193,7 +192,8 @@ public class SecurityConfig { RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) - .keyID(UUID.randomUUID().toString()) + // 重要!生产环境需要修改! + .keyID("jeecg") .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); @@ -211,7 +211,13 @@ public class SecurityConfig { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(2048); + + // 生产环境不应该设置secureRandom,seed如果被泄露,jwt容易被伪造 + // 如果不设置secureRandom,会存在一个问题,当应用重启后,原有的token将会全部失效,因为重启的keyPair与之前已经不同 + SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG"); + // 重要!生产环境需要修改! + secureRandom.setSeed("jeecg".getBytes()); + keyPairGenerator.initialize(2048, secureRandom); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationProvider.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationProvider.java new file mode 100644 index 00000000..5936d9b2 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationProvider.java @@ -0,0 +1,217 @@ +package org.jeecg.config.security.self; + +import com.alibaba.fastjson.JSONObject; +import org.jeecg.common.api.CommonAPI; +import org.jeecg.common.constant.CommonConstant; +import org.jeecg.common.exception.JeecgBootException; +import org.jeecg.common.system.vo.LoginUser; +import org.jeecg.common.system.vo.SysDepartModel; +import org.jeecg.common.util.RedisUtil; +import org.jeecg.common.util.oConvertUtils; +import org.jeecg.config.JeecgBaseConfig; +import org.jeecg.modules.base.service.BaseCommonService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.oauth2.core.*; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; +import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; + +import java.security.Principal; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 自用生成token处理器,不对外开放,外部请求无法通过该方式生成token + * @author eightmonth@qq.com + * @date 2024/3/19 11:40 + */ +@Component +public class SelfAuthenticationProvider implements AuthenticationProvider { + + private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; + + private final OAuth2AuthorizationService authorizationService; + private final OAuth2TokenGenerator tokenGenerator; + @Autowired + private CommonAPI commonAPI; + @Autowired + private RedisUtil redisUtil; + @Autowired + private JeecgBaseConfig jeecgBaseConfig; + @Autowired + private BaseCommonService baseCommonService; + + public SelfAuthenticationProvider(OAuth2AuthorizationService authorizationService, OAuth2TokenGenerator tokenGenerator) { + Assert.notNull(authorizationService, "authorizationService cannot be null"); + Assert.notNull(tokenGenerator, "tokenGenerator cannot be null"); + this.authorizationService = authorizationService; + this.tokenGenerator = tokenGenerator; + } + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + SelfAuthenticationToken passwordGrantAuthenticationToken = (SelfAuthenticationToken) authentication; + Map additionalParameter = passwordGrantAuthenticationToken.getAdditionalParameters(); + + // 授权类型 + AuthorizationGrantType authorizationGrantType = passwordGrantAuthenticationToken.getGrantType(); + // 用户名 + String username = (String) additionalParameter.get(OAuth2ParameterNames.USERNAME); + //请求参数权限范围 + String requestScopesStr = "*"; + //请求参数权限范围专场集合 + Set requestScopeSet = Stream.of(requestScopesStr.split(" ")).collect(Collectors.toSet()); + + OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(passwordGrantAuthenticationToken); + RegisteredClient registeredClient = clientPrincipal.getRegisteredClient(); + + if (!registeredClient.getAuthorizationGrantTypes().contains(authorizationGrantType)) { + throw new JeecgBootException("非法登录"); + } + + // 通过用户名获取用户信息 + LoginUser loginUser = commonAPI.getUserByName(username); + // 检查用户可行性 + checkUserIsEffective(loginUser); + + //由于在上面已验证过用户名、密码,现在构建一个已认证的对象UsernamePasswordAuthenticationToken + UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = UsernamePasswordAuthenticationToken.authenticated(loginUser,clientPrincipal,new ArrayList<>()); + + DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() + .registeredClient(registeredClient) + .principal(usernamePasswordAuthenticationToken) + .authorizationGrantType(authorizationGrantType) + .authorizedScopes(requestScopeSet) + .authorizationGrant(passwordGrantAuthenticationToken); + + OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) + .principalName(clientPrincipal.getName()) + .authorizedScopes(requestScopeSet) + .attribute(Principal.class.getName(), username) + .authorizationGrantType(authorizationGrantType); + + + // ----- Access token ----- + OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build(); + OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext); + if (generatedAccessToken == null) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "无法生成访问token,请联系管理系。", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, + generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), + generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); + if (generatedAccessToken instanceof ClaimAccessor) { + authorizationBuilder.token(accessToken, (metadata) -> { + metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()); + }); + } else { + authorizationBuilder.accessToken(accessToken); + } + + // ----- Refresh token ----- + OAuth2RefreshToken refreshToken = null; + if (registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) && + // 不向公共客户端颁发刷新令牌 + !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) { + + tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build(); + OAuth2Token generatedRefreshToken = this.tokenGenerator.generate(tokenContext); + if (!(generatedRefreshToken instanceof OAuth2RefreshToken)) { + OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, + "无法生成刷新token,请联系管理员。", ERROR_URI); + throw new OAuth2AuthenticationException(error); + } + + refreshToken = (OAuth2RefreshToken) generatedRefreshToken; + authorizationBuilder.refreshToken(refreshToken); + } + + OAuth2Authorization authorization = authorizationBuilder.build(); + + // 保存认证信息至redis + authorizationService.save(authorization); + + JSONObject addition = new JSONObject(new LinkedHashMap<>()); + + // 设置租户 + JSONObject jsonObject = commonAPI.setLoginTenant(username); + addition.putAll(jsonObject.getInnerMap()); + + // 设置登录用户信息 + addition.put("userInfo", loginUser); + addition.put("sysAllDictItems", commonAPI.queryAllDictItems()); + + List departs = commonAPI.queryUserDeparts(loginUser.getId()); + addition.put("departs", departs); + if (departs == null || departs.size() == 0) { + addition.put("multi_depart", 0); + } else if (departs.size() == 1) { + commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null); + addition.put("multi_depart", 1); + } else { + //查询当前是否有登录部门 + if(oConvertUtils.isEmpty(loginUser.getOrgCode())){ + commonAPI.updateUserDepart(username, departs.get(0).getOrgCode(),null); + } + addition.put("multi_depart", 2); + } + + // 返回access_token、refresh_token以及其它信息给到前端 + return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, addition); + } + + @Override + public boolean supports(Class authentication) { + return SelfAuthenticationToken.class.isAssignableFrom(authentication); + } + + private static OAuth2ClientAuthenticationToken getAuthenticatedClientElseThrowInvalidClient(Authentication authentication) { + OAuth2ClientAuthenticationToken clientPrincipal = null; + if (OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication.getPrincipal().getClass())) { + clientPrincipal = (OAuth2ClientAuthenticationToken) authentication.getPrincipal(); + } + if (clientPrincipal != null && clientPrincipal.isAuthenticated()) { + return clientPrincipal; + } + throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT); + } + + /** + * 校验用户是否有效 + */ + private void checkUserIsEffective(LoginUser loginUser) { + //情况1:根据用户信息查询,该用户不存在 + if (Objects.isNull(loginUser)) { + baseCommonService.addLog("用户登录失败,用户不存在!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户不存在,请注册"); + } + //情况2:根据用户信息查询,该用户已注销 + //update-begin---author:王帅 Date:20200601 for:if条件永远为falsebug------------ + if (CommonConstant.DEL_FLAG_1.equals(loginUser.getDelFlag())) { + //update-end---author:王帅 Date:20200601 for:if条件永远为falsebug------------ + baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已注销!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户已注销"); + } + //情况3:根据用户信息查询,该用户已冻结 + if (CommonConstant.USER_FREEZE.equals(loginUser.getStatus())) { + baseCommonService.addLog("用户登录失败,用户名:" + loginUser.getUsername() + "已冻结!", CommonConstant.LOG_TYPE_1, null); + throw new JeecgBootException("该用户已冻结"); + } + } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationToken.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationToken.java new file mode 100644 index 00000000..7a036ddf --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/self/SelfAuthenticationToken.java @@ -0,0 +1,19 @@ +package org.jeecg.config.security.self; + +import org.jeecg.config.security.LoginType; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationGrantAuthenticationToken; + +import java.util.Map; + +/** + * 自用生成token,不支持对外请求,仅为程序内部生成token + * @author eightmonth + * @date 2024/3/19 11:37 + */ +public class SelfAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken { + public SelfAuthenticationToken(Authentication clientPrincipal, Map additionalParameters) { + super(new AuthorizationGrantType(LoginType.SELF), clientPrincipal, additionalParameters); + } +} diff --git a/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/InsertDemoTest.java b/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/InsertDemoTest.java index f458ecd5..ad54c99d 100644 --- a/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/InsertDemoTest.java +++ b/jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/system/test/InsertDemoTest.java @@ -6,6 +6,9 @@ import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.util.RedisUtil; import org.jeecg.common.util.RestUtil; +import org.jeecg.common.util.SpringContextUtils; +import org.jeecg.config.security.self.SelfAuthenticationProvider; +import org.jeecg.config.security.self.SelfAuthenticationToken; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; @@ -14,8 +17,21 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; import org.springframework.test.context.junit4.SpringRunner; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + /** * 系统用户单元测试 */