diff --git a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java index b8dd7fb58..af3765a09 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java +++ b/youlai-auth/src/main/java/com/youlai/auth/authentication/password/PasswordAuthenticationProvider.java @@ -2,7 +2,6 @@ package com.youlai.auth.authentication.password; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ReflectUtil; import com.youlai.auth.util.OAuth2AuthenticationProviderUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.AuthenticationManager; @@ -29,7 +28,10 @@ import org.springframework.security.oauth2.server.authorization.token.OAuth2Toke import org.springframework.util.CollectionUtils; import java.security.Principal; -import java.util.*; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -44,6 +46,7 @@ import java.util.stream.Collectors; public class PasswordAuthenticationProvider 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; @@ -133,9 +136,6 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider { generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); - // 权限数据(perms)比较多通过反射移除,不随令牌一起持久化至数据库 - ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null); - OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) .principalName(usernamePasswordAuthentication.getName()) .authorizationGrantType(AuthorizationGrantType.PASSWORD) @@ -166,11 +166,41 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider { authorizationBuilder.refreshToken(refreshToken); } - OAuth2Authorization authorization = authorizationBuilder.build(); + // ----- 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); + } + + if (log.isTraceEnabled()) { + log.trace("Generated id token"); + } + + 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(); + + additionalParameters = (idToken != null) + ? Collections.singletonMap(OidcParameterNames.ID_TOKEN, idToken.getTokenValue()) + : Collections.emptyMap(); 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 a07fe1181..7323d984a 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 @@ -38,6 +38,7 @@ 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.config.Customizer; 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; @@ -105,6 +106,7 @@ public class AuthorizationServerConfig { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); authorizationServerConfigurer + .oidc(Customizer.withDefaults()) .tokenEndpoint(tokenEndpoint -> tokenEndpoint .accessTokenRequestConverters( @@ -200,11 +202,17 @@ public class AuthorizationServerConfig { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } + /** + * 授权服务器配置(令牌签发者、获取令牌等端点) + */ @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } + /** + * 密码加密器 + */ @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); diff --git a/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java b/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java index b4726fbe9..8d63f9240 100644 --- a/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java +++ b/youlai-auth/src/main/java/com/youlai/auth/config/JwtTokenClaimsConfig.java @@ -2,6 +2,7 @@ package com.youlai.auth.config; import com.youlai.auth.model.MemberDetails; import com.youlai.auth.model.SysUserDetails; +import com.youlai.common.constant.JwtClaimConstants; import com.youlai.common.constant.SecurityConstants; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -28,11 +29,8 @@ import java.util.stream.Collectors; * @since 3.0.0 */ @Configuration -@RequiredArgsConstructor public class JwtTokenClaimsConfig { - private final RedisTemplate redisTemplate; - /** * JWT 自定义字段 */ @@ -45,28 +43,19 @@ public class JwtTokenClaimsConfig { JwtClaimsSet.Builder claims = context.getClaims(); if (principal instanceof SysUserDetails userDetails) { // 系统用户添加自定义字段 - Long userId = userDetails.getUserId(); - claims.claim("userId", userDetails.getUserId()); - claims.claim("username", userDetails.getUsername()); - claims.claim("deptId", userDetails.getDeptId()); - claims.claim("dataScope", userDetails.getDataScope()); - - Set roles = userDetails.getAuthorities().stream() - .map(GrantedAuthority::getAuthority).collect(Collectors.toSet()); - claims.claim("authorities", roles); + claims.claim(JwtClaimConstants.USER_ID, userDetails.getUserId()); + claims.claim(JwtClaimConstants.USERNAME, userDetails.getUsername()); + claims.claim(JwtClaimConstants.DEPT_ID, userDetails.getDeptId()); + claims.claim(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope()); // 这里存入角色至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_KEY_PREFIX + userId, perms); + claims.claim(JwtClaimConstants.AUTHORITIES, authorities); } else if (principal instanceof MemberDetails userDetails) { // 商城会员添加自定义字段 - claims.claim("member_id", String.valueOf(userDetails.getId())); + claims.claim(JwtClaimConstants.MEMBER_ID, String.valueOf(userDetails.getId())); } }); } diff --git a/youlai-common/common-core/src/main/java/com/youlai/common/constant/JwtClaimConstants.java b/youlai-common/common-core/src/main/java/com/youlai/common/constant/JwtClaimConstants.java new file mode 100644 index 000000000..e1930eb28 --- /dev/null +++ b/youlai-common/common-core/src/main/java/com/youlai/common/constant/JwtClaimConstants.java @@ -0,0 +1,34 @@ +package com.youlai.common.constant; + +public interface JwtClaimConstants { + + /** + * 用户ID + */ + String USER_ID = "userId"; + + /** + * 用户名 + */ + String USERNAME = "username"; + + /** + * 部门ID + */ + String DEPT_ID = "deptId"; + + /** + * 数据权限 + */ + String DATA_SCOPE = "dataScope"; + + /** + * 权限(角色Code)集合 + */ + String AUTHORITIES = "authorities"; + + /** + * 会员ID + */ + String MEMBER_ID = "memberId"; +} diff --git a/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java b/youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java similarity index 97% rename from youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java rename to youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java index a279cea41..ee47fab85 100644 --- a/youlai-common/common-security/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java +++ b/youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/config/ResourceServerConfig.java @@ -2,6 +2,7 @@ package com.youlai.common.security.config; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.json.JSONUtil; +import com.youlai.common.constant.JwtClaimConstants; import com.youlai.common.constant.SecurityConstants; import lombok.RequiredArgsConstructor; import lombok.Setter; @@ -112,7 +113,7 @@ public class ResourceServerConfig { public Converter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY); - jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY); + jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JwtClaimConstants.AUTHORITIES); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); diff --git a/youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/service/PermissionService.java b/youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/service/PermissionService.java new file mode 100644 index 000000000..a42bda645 --- /dev/null +++ b/youlai-common/common-resourceserver/src/main/java/com/youlai/common/security/service/PermissionService.java @@ -0,0 +1,96 @@ +package com.youlai.common.security.service; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.youlai.common.constant.SecurityConstants; +import com.youlai.common.security.util.SecurityUtils; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.util.PatternMatchUtils; + +import java.util.*; + +/** + * SpringSecurity 权限校验 + * + * @author haoxr + * @since 2022/2/22 + */ +@Service("ss") +@RequiredArgsConstructor +@Slf4j +public class PermissionService { + + private final RedisTemplate redisTemplate; + + /** + * 判断当前登录用户是否拥有操作权限 + * + * @param requiredPerm 所需权限 + * @return 是否有权限 + */ + public boolean hasPerm(String requiredPerm) { + + if (StrUtil.isBlank(requiredPerm)) { + return false; + } + // 超级管理员放行 + if (SecurityUtils.isRoot()) { + return true; + } + + // 获取当前登录用户的角色编码集合 + Set roleCodes = SecurityUtils.getRoles(); + if (CollectionUtil.isEmpty(roleCodes)) { + return false; + } + + // 获取当前登录用户的所有角色的权限列表 + Set rolePerms = this.getRolePermsFormCache(roleCodes); + if (CollectionUtil.isEmpty(rolePerms)) { + return false; + } + // 判断当前登录用户的所有角色的权限列表中是否包含所需权限 + boolean hasPermission = rolePerms.stream() + .anyMatch(rolePerm -> + // 匹配权限,支持通配符(* 等) + PatternMatchUtils.simpleMatch(rolePerm, requiredPerm) + ); + + if (!hasPermission) { + log.error("用户无操作权限"); + } + return hasPermission; + } + + + /** + * 从缓存中获取角色权限列表 + * + * @param roleCodes 角色编码集合 + * @return 角色权限列表 + */ + public Set getRolePermsFormCache(Set roleCodes) { + // 检查输入是否为空 + if (CollectionUtil.isEmpty(roleCodes)) { + return Collections.emptySet(); + } + + Set perms = new HashSet<>(); + // 从缓存中一次性获取所有角色的权限 + Collection roleCodesAsObjects = new ArrayList<>(roleCodes); + List rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects); + + for (Object rolePermsObj : rolePermsList) { + if (rolePermsObj instanceof Set) { + @SuppressWarnings("unchecked") + Set rolePerms = (Set) rolePermsObj; + perms.addAll(rolePerms); + } + } + + return perms; + } +} diff --git a/youlai-common/common-security/src/main/java/com/youlai/common/security/service/PermissionService.java b/youlai-common/common-security/src/main/java/com/youlai/common/security/service/PermissionService.java deleted file mode 100644 index a71432d3b..000000000 --- a/youlai-common/common-security/src/main/java/com/youlai/common/security/service/PermissionService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.youlai.common.security.service; - -import cn.hutool.core.collection.CollectionUtil; -import cn.hutool.core.util.StrUtil; -import com.youlai.common.constant.SecurityConstants; -import com.youlai.common.security.util.SecurityUtils; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; -import org.springframework.util.PatternMatchUtils; - -import java.util.Set; - -/** - * SpringSecurity权限校验 - * - * @author haoxr - * @since 2022/2/22 - */ -@Service("ss") -@RequiredArgsConstructor -@Slf4j -public class PermissionService { - - private final RedisTemplate redisTemplate; - - /** - * 判断当前登录用户是否拥有操作权限 - * - * @param perm 权限标识(eg: sys:user:add) - * @return - */ - public boolean hasPerm(String perm) { - - if (StrUtil.isBlank(perm)) { - return false; - } - // 超级管理员放行 - if (SecurityUtils.isRoot()) { - return true; - } - - Long userId = SecurityUtils.getUserId(); - - Set perms = (Set) redisTemplate.opsForValue().get(SecurityConstants.USER_PERMS_CACHE_KEY_PREFIX + userId); // 权限数据用户登录成功节点存入redis,详见 JwtTokenManager#createToken() - - if (CollectionUtil.isEmpty(perms)) { - return false; - } - boolean hasPermission = perms.stream() - .anyMatch(item -> PatternMatchUtils.simpleMatch(perm, item)); // *号匹配任意字符 - - if (!hasPermission) { - log.error("用户无访问权限"); - } - return hasPermission; - } - - -} diff --git a/youlai-system/system-boot/src/main/java/com/youlai/system/model/bo/RolePermsBO.java b/youlai-system/system-boot/src/main/java/com/youlai/system/model/bo/RolePermsBO.java new file mode 100644 index 000000000..6e8c764a0 --- /dev/null +++ b/youlai-system/system-boot/src/main/java/com/youlai/system/model/bo/RolePermsBO.java @@ -0,0 +1,26 @@ +package com.youlai.system.model.bo; + +import lombok.Data; + +import java.util.Set; + +/** + * 角色权限业务对象 + * + * @author haoxr + * @since 2023/11/29 + */ +@Data +public class RolePermsBO { + + /** + * 角色编码 + */ + private String roleCode; + + /** + * 权限标识集合 + */ + private Set perms; + +}