From a07a3c8839db7c22185a80c2b74ef17569a72361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=9D=E5=85=88=E7=91=9E?= <1490493387@qq.com> Date: Tue, 4 Jul 2023 19:16:50 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=87=AA=E5=AE=9A=E4=B9=89=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=93=8D=E5=BA=94=E6=A0=BC=E5=BC=8F=E5=92=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=88=B7=E6=96=B0=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CaptchaAuthenticationProvider.java | 61 +------------- .../config/AuthorizationServerConfig.java | 72 ++++++++-------- .../auth/config/JwtCustomizerConfig.java | 79 ++++++++++++++++++ .../auth/controller/AuthController.java | 13 +++ .../MyAuthenticationSuccessHandler.java | 4 +- .../userdetails/user/jackson/SysUseMixin.java | 19 +++++ .../user/jackson/SysUserDeserializer.java | 83 +++++++++++++++++++ .../common/constant/SecurityConstants.java | 17 ++-- .../security/config/ResourceServerConfig.java | 7 +- .../common/security/util/SecurityUtils.java | 17 ++-- .../youlai/system/mapper/SysRoleMapper.java | 2 +- .../youlai/system/service/SysRoleService.java | 2 +- .../service/impl/SysRoleServiceImpl.java | 4 +- .../service/impl/SysUserServiceImpl.java | 16 +++- .../main/resources/mapper/SysRoleMapper.xml | 2 +- 15 files changed, 268 insertions(+), 130 deletions(-) create mode 100644 youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java create mode 100644 youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java index 98af8806..b288c595 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/captcha/CaptchaAuthenticationProvider.java @@ -13,10 +13,6 @@ 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.core.oidc.OidcIdToken; -import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; -import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; @@ -27,11 +23,10 @@ import org.springframework.security.oauth2.server.authorization.context.Authoriz 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.util.CollectionUtils; import java.security.Principal; -import java.util.*; -import java.util.stream.Collectors; +import java.util.Collections; +import java.util.Map; /** * 验证码模式身份验证提供者 @@ -45,10 +40,7 @@ import java.util.stream.Collectors; @Slf4j public class CaptchaAuthenticationProvider implements AuthenticationProvider { - private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; - - private static final OAuth2TokenType ID_TOKEN_TOKEN_TYPE = new OAuth2TokenType(OidcParameterNames.ID_TOKEN); private final AuthenticationManager authenticationManager; private final OAuth2AuthorizationService authorizationService; private final OAuth2TokenGenerator tokenGenerator; @@ -93,7 +85,7 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider { String verifyCode = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE); String verifyCodeKey = (String) additionalParameters.get(CaptchaParameterNames.VERIFY_CODE_KEY); - String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_KEY_PREFIX +verifyCodeKey); + String cacheCode = redisTemplate.opsForValue().get(SecurityConstants.VERIFY_CODE_KEY_PREFIX + verifyCodeKey); if (!StrUtil.equals(verifyCode, cacheCode)) { throw new OAuth2AuthenticationException("验证码错误"); } @@ -105,25 +97,11 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider { // 用户名密码身份验证,成功后返回带有权限的认证信息 Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken); - // 验证申请访问范围(Scope) - Set authorizedScopes = registeredClient.getScopes(); - Set requestedScopes = captchaAuthenticationToken.getScopes(); - if (!CollectionUtils.isEmpty(requestedScopes)) { - Set unauthorizedScopes = requestedScopes.stream() - .filter(requestedScope -> !registeredClient.getScopes().contains(requestedScope)) - .collect(Collectors.toSet()); - if (!CollectionUtils.isEmpty(unauthorizedScopes)) { - throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_SCOPE); - } - authorizedScopes = new LinkedHashSet<>(requestedScopes); - } - // 访问令牌(Access Token) 构造器 DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder() .registeredClient(registeredClient) .principal(usernamePasswordAuthentication) // 身份验证成功的认证信息(用户名、权限等信息) .authorizationServerContext(AuthorizationServerContextHolder.getContext()) - .authorizedScopes(authorizedScopes) .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) // 授权方式 .authorizationGrant(captchaAuthenticationToken) // 授权具体对象 ; @@ -144,7 +122,6 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider { OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(usernamePasswordAuthentication.getName()) .authorizationGrantType(CaptchaAuthenticationToken.CAPTCHA) - .authorizedScopes(authorizedScopes) .attribute(Principal.class.getName(), usernamePasswordAuthentication); if (generatedAccessToken instanceof ClaimAccessor) { authorizationBuilder.token(accessToken, (metadata) -> @@ -171,39 +148,9 @@ public class CaptchaAuthenticationProvider implements AuthenticationProvider { authorizationBuilder.refreshToken(refreshToken); } - // 生成 ID token - OidcIdToken idToken; - if (requestedScopes.contains(OidcScopes.OPENID)) { - // @formatter:off - tokenContext = tokenContextBuilder - .tokenType(ID_TOKEN_TOKEN_TYPE) - .authorization(authorizationBuilder.build()) // ID token customizer may need access to the access token and/or refresh token - .build(); - // @formatter:on - OAuth2Token generatedIdToken = this.tokenGenerator.generate(tokenContext); - if (!(generatedIdToken instanceof Jwt)) { - OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, - "The token generator failed to generate the ID token.", ERROR_URI); - throw new OAuth2AuthenticationException(error); - } - - idToken = new OidcIdToken(generatedIdToken.getTokenValue(), generatedIdToken.getIssuedAt(), - generatedIdToken.getExpiresAt(), ((Jwt) generatedIdToken).getClaims()); - authorizationBuilder.token(idToken, (metadata) -> - metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, idToken.getClaims())); - } else { - idToken = null; - } - OAuth2Authorization authorization = authorizationBuilder.build(); - this.authorizationService.save(authorization); - - additionalParameters = Collections.emptyMap(); - if (idToken != null) { - additionalParameters = new HashMap<>(); - additionalParameters.put(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()); - } + additionalParameters = Collections.EMPTY_MAP; return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters); } diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java index 1d6dbfa2..ee67691f 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java +++ b/youlai-auth/src/main/java/com/youlai/auth/config/AuthorizationServerConfig.java @@ -2,13 +2,13 @@ package com.youlai.auth.config; import cn.binarywang.wx.miniapp.api.WxMaService; -import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jose.shaded.json.JSONObject; import com.youlai.auth.authentication.captcha.CaptchaAuthenticationConverter; import com.youlai.auth.authentication.captcha.CaptchaAuthenticationProvider; import com.youlai.auth.authentication.captcha.CaptchaAuthenticationToken; @@ -21,57 +21,52 @@ import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationConverter import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationProvider; import com.youlai.auth.authentication.wxminiapp.WxMiniAppAuthenticationToken; import com.youlai.auth.handler.MyAuthenticationSuccessHandler; -import com.youlai.auth.userdetails.member.MemberDetails; import com.youlai.auth.userdetails.member.MemberDetailsService; import com.youlai.auth.userdetails.user.SysUserDetails; -import com.youlai.common.result.Result; -import jakarta.servlet.http.HttpServletResponse; +import com.youlai.auth.userdetails.user.jackson.SysUseMixin; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.security.authentication.AuthenticationManager; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.jackson2.SecurityJackson2Modules; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; import org.springframework.security.oauth2.core.oidc.OidcScopes; -import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; -import org.springframework.security.oauth2.jwt.JwtClaimsSet; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; -import org.springframework.security.oauth2.server.authorization.*; -import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository; 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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; +import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; import org.springframework.security.oauth2.server.authorization.token.*; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.util.matcher.RequestMatcher; -import org.springframework.web.util.ContentCachingResponseWrapper; -import org.springframework.web.util.WebUtils; -import java.io.IOException; -import java.io.PrintWriter; -import java.io.StringWriter; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.time.Duration; import java.util.List; -import java.util.Optional; import java.util.UUID; /** @@ -88,7 +83,7 @@ public class AuthorizationServerConfig { private final WxMaService wxMaService; private final StringRedisTemplate redisTemplate; private final MemberDetailsService memberDetailsService; - + private final OAuth2TokenCustomizer jwtCustomizer; /** * 授权配置 @@ -203,7 +198,22 @@ public class AuthorizationServerConfig { @Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { - return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); + + JdbcOAuth2AuthorizationService service = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); + JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository); + rowMapper.setLobHandler(new DefaultLobHandler()); + ObjectMapper objectMapper = new ObjectMapper(); + ClassLoader classLoader = JdbcOAuth2AuthorizationService.class.getClassLoader(); + List securityModules = SecurityJackson2Modules.getModules(classLoader); + objectMapper.registerModules(securityModules); + objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); + // You will need to write the Mixin for your class so Jackson can marshall it. + objectMapper.addMixIn(SysUserDetails.class, SysUseMixin.class); + objectMapper.addMixIn(Long.class, Object.class); + rowMapper.setObjectMapper(objectMapper); + service.setAuthorizationRowMapper(rowMapper); + + return service; } @Bean @@ -217,7 +227,7 @@ public class AuthorizationServerConfig { @Bean OAuth2TokenGenerator tokenGenerator(JWKSource jwkSource) { JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource)); - jwtGenerator.setJwtCustomizer(jwtCustomizer()); + jwtGenerator.setJwtCustomizer(jwtCustomizer); OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); @@ -226,25 +236,7 @@ public class AuthorizationServerConfig { } - @Bean - public OAuth2TokenCustomizer jwtCustomizer() { - return context -> { - if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) { - // Customize headers/claims for access_token - Optional.ofNullable(context.getPrincipal().getPrincipal()).ifPresent(principal -> { - JwtClaimsSet.Builder claims = context.getClaims(); - if (principal instanceof SysUserDetails userDetails) { - claims.claim("user_id", String.valueOf(userDetails.getUserId())); - } else if (principal instanceof MemberDetails userDetails) { - claims.claim("member_id", String.valueOf(userDetails.getId())); - } - }); - } else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) { - // Customize headers/claims for id_token - - } - }; - } + @Bean @@ -284,6 +276,7 @@ public class AuthorizationServerConfig { .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) + .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); registeredClientRepository.save(mallAppClient); @@ -320,6 +313,7 @@ public class AuthorizationServerConfig { .postLogoutRedirectUri("http://127.0.0.1:8080/logged-out") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) + .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofDays(1)).build()) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); registeredClientRepository.save(mallAppClient); diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java new file mode 100644 index 00000000..bcf85d11 --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/config/JwtCustomizerConfig.java @@ -0,0 +1,79 @@ +package com.youlai.auth.config; + +import com.youlai.auth.userdetails.member.MemberDetails; +import com.youlai.auth.userdetails.user.SysUserDetails; +import com.youlai.common.constant.SecurityConstants; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; +import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 自定义 JWT 的 Claims(声明) + * + * @author haoxr + * @since 2023/7/4 + */ +@Configuration +@RequiredArgsConstructor +public class JwtCustomizerConfig { + + private final RedisTemplate redisTemplate; + + @Bean + public OAuth2TokenCustomizer jwtCustomizer() { + return context -> { + if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType()) && context.getPrincipal() instanceof UsernamePasswordAuthenticationToken) { + // Customize headers/claims for access_token + Optional.ofNullable(context.getPrincipal().getPrincipal()).ifPresent(principal -> { + JwtClaimsSet.Builder claims = context.getClaims(); + if (principal instanceof SysUserDetails userDetails) { + + Long userId = userDetails.getUserId(); + claims.claim("user_id", userId); + + // 这里存入角色至JWT,解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter + var authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities()) + .stream() + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + claims.claim(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY, authorities); + + // 权限数据比较多,缓存至redis + Set perms = userDetails.getPerms(); + redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_PREFIX + userId, perms); + + } else if (principal instanceof MemberDetails userDetails) { + claims.claim("member_id", String.valueOf(userDetails.getId())); + }else{ + User user = (User) principal; + + // 这里存入角色至JWT,解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter + var authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities()) + .stream() + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); + claims.claim(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY, authorities); + } + }); + } else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) { + // Customize headers/claims for id_token + + } + }; + } + +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java b/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java new file mode 100644 index 00000000..a6412a5c --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/controller/AuthController.java @@ -0,0 +1,13 @@ +package com.youlai.auth.controller; + +import org.springframework.web.bind.annotation.RestController; + +/** + * @author haoxr + * @since 2023/6/29 + */ + +@RestController +public class AuthController { + +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java index 88395f00..01124946 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java +++ b/youlai-auth/src/main/java/com/youlai/auth/handler/MyAuthenticationSuccessHandler.java @@ -52,8 +52,7 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand OAuth2AccessTokenResponse.Builder builder = OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue()) - .tokenType(accessToken.getTokenType()) - .scopes(accessToken.getScopes()); + .tokenType(accessToken.getTokenType()); if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) { builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt())); } @@ -68,6 +67,7 @@ public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHand Map tokenResponseParameters = this.accessTokenResponseParametersConverter .convert(accessTokenResponse); + response.setCharacterEncoding("UTF-8"); response.getWriter().write(JSONUtil.toJsonStr(Result.success(tokenResponseParameters))); diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java new file mode 100644 index 00000000..f87302df --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUseMixin.java @@ -0,0 +1,19 @@ +package com.youlai.auth.userdetails.user.jackson; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * @see org.springframework.security.jackson2.UserMixin + * + * @author haoxr + * @since 2023/7/4 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY) +@JsonDeserialize(using = SysUserDeserializer.class) +@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE) +@JsonIgnoreProperties(ignoreUnknown = true) +public class SysUseMixin { +} diff --git a/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java new file mode 100644 index 00000000..055c95ab --- /dev/null +++ b/youlai-auth/src/main/java/com/youlai/auth/userdetails/user/jackson/SysUserDeserializer.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.youlai.auth.userdetails.user.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.MissingNode; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; + +import java.io.IOException; +import java.util.Set; + +/** + * Custom Deserializer for {@link User} class. This is already registered with + * {@link UserMixin}. You can also use it directly with your mixin class. + * + * @author Jitendra Singh + * @since 4.2 + * @see UserMixin + */ +class SysUserDeserializer extends JsonDeserializer { + + private static final TypeReference> SIMPLE_GRANTED_AUTHORITY_SET = new TypeReference>() { + }; + + /** + * This method will create {@link User} object. It will ensure successful object + * creation even if password key is null in serialized json, because credentials may + * be removed from the {@link User} by invoking {@link User#eraseCredentials()}. In + * that case there won't be any password key in serialized json. + * @param jp the JsonParser + * @param ctxt the DeserializationContext + * @return the user + * @throws IOException if a exception during IO occurs + * @throws JsonProcessingException if an error during JSON processing occurs + */ + @Override + public User deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + ObjectMapper mapper = (ObjectMapper) jp.getCodec(); + JsonNode jsonNode = mapper.readTree(jp); + Set authorities = mapper.convertValue(jsonNode.get("authorities"), + SIMPLE_GRANTED_AUTHORITY_SET); + JsonNode passwordNode = readJsonNode(jsonNode, "password"); + String username = readJsonNode(jsonNode, "username").asText(); + String password = passwordNode.asText(""); + boolean enabled = readJsonNode(jsonNode, "enabled").asBoolean(); + boolean accountNonExpired = readJsonNode(jsonNode, "accountNonExpired").asBoolean(); + boolean credentialsNonExpired = readJsonNode(jsonNode, "credentialsNonExpired").asBoolean(); + boolean accountNonLocked = readJsonNode(jsonNode, "accountNonLocked").asBoolean(); + User result = new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, + authorities); + if (passwordNode.asText(null) == null) { + result.eraseCredentials(); + } + return result; + } + + private JsonNode readJsonNode(JsonNode jsonNode, String field) { + return jsonNode.has(field) ? jsonNode.get(field) : MissingNode.getInstance(); + } + +} diff --git a/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java b/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java index b3e3a2e2..8d5e6f18 100644 --- a/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java +++ b/youlai-common/common-core/src/main/java/com/youlai/common/constant/SecurityConstants.java @@ -15,12 +15,7 @@ public interface SecurityConstants { /** * 短信验证码key前缀 */ - String SMS_CODE_PREFIX = "SMS_CODE:"; - - /** - * 接口文档 Knife4j 测试客户端ID - */ - String TEST_CLIENT_ID = "client"; + String SMS_CODE_PREFIX = "AUTH:SMS_CODE:"; /** * 系统管理 web 客户端ID @@ -32,16 +27,14 @@ public interface SecurityConstants { */ String APP_CLIENT_ID = "mall-app"; - /** - * 微信小程序客户端ID - */ - String WEAPP_CLIENT_ID = "mall-weapp"; - - /** * 用户权限集合缓存前缀 */ String USER_PERMS_CACHE_PREFIX = "AUTH:USER_PERMS:"; + /** + * 授权信息中的权限或角色的key + */ + String AUTHORITIES_CLAIM_NAME_KEY="authorities"; } diff --git a/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java b/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java index 240c8b87..1d5f4b13 100644 --- a/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java +++ b/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java @@ -3,8 +3,10 @@ package com.youlai.common.security.config; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.convert.Convert; import cn.hutool.json.JSONUtil; +import com.youlai.common.constant.SecurityConstants; import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -78,10 +80,11 @@ public class ResourceServerConfig { * @return Converter * @see JwtAuthenticationProvider#setJwtAuthenticationConverter(Converter) */ + @Bean public Converter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); - jwtGrantedAuthoritiesConverter.setAuthorityPrefix("ROLE_"); - jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName("authorities"); + jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); diff --git a/youlai-common/common-security/src/main/java/com/youlai/common/security/util/SecurityUtils.java b/youlai-common/common-security/src/main/java/com/youlai/common/security/util/SecurityUtils.java index f410bf29..4d9d3eff 100644 --- a/youlai-common/common-security/src/main/java/com/youlai/common/security/util/SecurityUtils.java +++ b/youlai-common/common-security/src/main/java/com/youlai/common/security/util/SecurityUtils.java @@ -1,10 +1,9 @@ package com.youlai.common.security.util; -import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.convert.Convert; -import cn.hutool.core.util.StrUtil; import com.youlai.common.constant.GlobalConstants; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; @@ -13,6 +12,9 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +/** + * Spring Security 工具类 + */ public class SecurityUtils { public static Long getUserId() { @@ -34,15 +36,10 @@ public class SecurityUtils { public static Set getRoles() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + var roles = AuthorityUtils.authorityListToSet(authentication.getAuthorities()) + .stream() + .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); - Set roles; - if (CollectionUtil.isNotEmpty(authentication.getAuthorities())) { - roles = authentication.getAuthorities() - .stream() - .map(item -> StrUtil.removePrefix(item.getAuthority(), "ROLE_")).collect(Collectors.toSet()); - } else { - roles = Collections.EMPTY_SET; - } return roles; } diff --git a/youlai-system/system-boot/src/main/java/com/youlai/system/mapper/SysRoleMapper.java b/youlai-system/system-boot/src/main/java/com/youlai/system/mapper/SysRoleMapper.java index fba8f94c..742afe51 100644 --- a/youlai-system/system-boot/src/main/java/com/youlai/system/mapper/SysRoleMapper.java +++ b/youlai-system/system-boot/src/main/java/com/youlai/system/mapper/SysRoleMapper.java @@ -16,5 +16,5 @@ public interface SysRoleMapper extends BaseMapper { * @param roles * @return */ - Integer getMaximumDataScope(Set roles); + Integer getMaxDataRangeDataScope(Set roles); } diff --git a/youlai-system/system-boot/src/main/java/com/youlai/system/service/SysRoleService.java b/youlai-system/system-boot/src/main/java/com/youlai/system/service/SysRoleService.java index c0f261c7..636559f7 100644 --- a/youlai-system/system-boot/src/main/java/com/youlai/system/service/SysRoleService.java +++ b/youlai-system/system-boot/src/main/java/com/youlai/system/service/SysRoleService.java @@ -93,7 +93,7 @@ public interface SysRoleService extends IService { * @param roles * @return */ - Integer getMaximumDataScope(Set roles); + Integer getMaxDataRangeDataScope(Set roles); } diff --git a/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysRoleServiceImpl.java b/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysRoleServiceImpl.java index 99127b15..df7a2de8 100644 --- a/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysRoleServiceImpl.java +++ b/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysRoleServiceImpl.java @@ -206,8 +206,8 @@ public class SysRoleServiceImpl extends ServiceImpl impl * @return */ @Override - public Integer getMaximumDataScope(Set roles) { - Integer dataScope = this.baseMapper.getMaximumDataScope(roles); + public Integer getMaxDataRangeDataScope(Set roles) { + Integer dataScope = this.baseMapper.getMaxDataRangeDataScope(roles); return dataScope; } diff --git a/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java b/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java index 828531fa..f9365c2f 100644 --- a/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java +++ b/youlai-system/system-boot/src/main/java/com/youlai/system/service/impl/SysUserServiceImpl.java @@ -1,5 +1,6 @@ package com.youlai.system.service.impl; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -21,6 +22,7 @@ import com.youlai.system.model.query.UserPageQuery; import com.youlai.system.model.vo.UserExportVO; import com.youlai.system.model.vo.UserInfoVO; import com.youlai.system.model.vo.UserPageVO; +import com.youlai.system.service.SysMenuService; import com.youlai.system.service.SysRoleService; import com.youlai.system.service.SysUserRoleService; import com.youlai.system.service.SysUserService; @@ -55,6 +57,8 @@ public class SysUserServiceImpl extends ServiceImpl impl private final RedisTemplate redisTemplate; + private final SysMenuService menuService; + /** * 获取用户分页列表 * @@ -198,9 +202,15 @@ public class SysUserServiceImpl extends ServiceImpl impl UserAuthInfo userAuthInfo = this.baseMapper.getUserAuthInfo(username); if (userAuthInfo != null) { Set roles = userAuthInfo.getRoles(); - // 获取最大范围的数据权限 - Integer dataScope = roleService.getMaximumDataScope(roles); - userAuthInfo.setDataScope(dataScope); + if (CollectionUtil.isNotEmpty(roles)) { + // 根据角色编码集合获取权限标识集合 + Set perms = menuService.listRolePerms(roles); + userAuthInfo.setPerms(perms); + + // 获取最大范围的数据权限(目前设定DataScope越小,拥有的数据权限范围越大,所以获取得到角色列表中最小的DataScope) + Integer dataScope = roleService.getMaxDataRangeDataScope(roles); + userAuthInfo.setDataScope(dataScope); + } } return userAuthInfo; } diff --git a/youlai-system/system-boot/src/main/resources/mapper/SysRoleMapper.xml b/youlai-system/system-boot/src/main/resources/mapper/SysRoleMapper.xml index 7fd085c8..f5ee9813 100644 --- a/youlai-system/system-boot/src/main/resources/mapper/SysRoleMapper.xml +++ b/youlai-system/system-boot/src/main/resources/mapper/SysRoleMapper.xml @@ -5,7 +5,7 @@ - SELECT min(data_scope) FROM -- GitLab