refactor: 登录用户权限校验逻辑调整

This commit is contained in:
haoxr 2024-01-30 13:52:55 +08:00
parent 369f2e6b04
commit 524e34d0bc
8 changed files with 210 additions and 87 deletions

View File

@ -2,7 +2,6 @@ package com.youlai.auth.authentication.password;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import com.youlai.auth.util.OAuth2AuthenticationProviderUtils; import com.youlai.auth.util.OAuth2AuthenticationProviderUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager; 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 org.springframework.util.CollectionUtils;
import java.security.Principal; 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; import java.util.stream.Collectors;
/** /**
@ -44,6 +46,7 @@ import java.util.stream.Collectors;
public class PasswordAuthenticationProvider implements AuthenticationProvider { public class PasswordAuthenticationProvider implements AuthenticationProvider {
private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; 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 AuthenticationManager authenticationManager;
private final OAuth2AuthorizationService authorizationService; private final OAuth2AuthorizationService authorizationService;
private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator; private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;
@ -133,9 +136,6 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(), generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes()); generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
// 权限数据(perms)比较多通过反射移除不随令牌一起持久化至数据库
ReflectUtil.setFieldValue(usernamePasswordAuthentication.getPrincipal(), "perms", null);
OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient) OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
.principalName(usernamePasswordAuthentication.getName()) .principalName(usernamePasswordAuthentication.getName())
.authorizationGrantType(AuthorizationGrantType.PASSWORD) .authorizationGrantType(AuthorizationGrantType.PASSWORD)
@ -166,11 +166,41 @@ public class PasswordAuthenticationProvider implements AuthenticationProvider {
authorizationBuilder.refreshToken(refreshToken); 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); 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); return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, accessToken, refreshToken, additionalParameters);
} }

View File

@ -38,6 +38,7 @@ import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.security.authentication.AuthenticationManager; 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.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.factory.PasswordEncoderFactories;
@ -105,6 +106,7 @@ public class AuthorizationServerConfig {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
authorizationServerConfigurer authorizationServerConfigurer
.oidc(Customizer.withDefaults())
.tokenEndpoint(tokenEndpoint -> .tokenEndpoint(tokenEndpoint ->
tokenEndpoint tokenEndpoint
.accessTokenRequestConverters( .accessTokenRequestConverters(
@ -200,11 +202,17 @@ public class AuthorizationServerConfig {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
} }
/**
* 授权服务器配置(令牌签发者获取令牌等端点)
*/
@Bean @Bean
public AuthorizationServerSettings authorizationServerSettings() { public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build(); return AuthorizationServerSettings.builder().build();
} }
/**
* 密码加密器
*/
@Bean @Bean
public PasswordEncoder passwordEncoder() { public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); return PasswordEncoderFactories.createDelegatingPasswordEncoder();

View File

@ -2,6 +2,7 @@ package com.youlai.auth.config;
import com.youlai.auth.model.MemberDetails; import com.youlai.auth.model.MemberDetails;
import com.youlai.auth.model.SysUserDetails; import com.youlai.auth.model.SysUserDetails;
import com.youlai.common.constant.JwtClaimConstants;
import com.youlai.common.constant.SecurityConstants; import com.youlai.common.constant.SecurityConstants;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -28,11 +29,8 @@ import java.util.stream.Collectors;
* @since 3.0.0 * @since 3.0.0
*/ */
@Configuration @Configuration
@RequiredArgsConstructor
public class JwtTokenClaimsConfig { public class JwtTokenClaimsConfig {
private final RedisTemplate redisTemplate;
/** /**
* JWT 自定义字段 * JWT 自定义字段
*/ */
@ -45,28 +43,19 @@ public class JwtTokenClaimsConfig {
JwtClaimsSet.Builder claims = context.getClaims(); JwtClaimsSet.Builder claims = context.getClaims();
if (principal instanceof SysUserDetails userDetails) { // 系统用户添加自定义字段 if (principal instanceof SysUserDetails userDetails) { // 系统用户添加自定义字段
Long userId = userDetails.getUserId(); claims.claim(JwtClaimConstants.USER_ID, userDetails.getUserId());
claims.claim("userId", userDetails.getUserId()); claims.claim(JwtClaimConstants.USERNAME, userDetails.getUsername());
claims.claim("username", userDetails.getUsername()); claims.claim(JwtClaimConstants.DEPT_ID, userDetails.getDeptId());
claims.claim("deptId", userDetails.getDeptId()); claims.claim(JwtClaimConstants.DATA_SCOPE, userDetails.getDataScope());
claims.claim("dataScope", userDetails.getDataScope());
Set<String> roles = userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
claims.claim("authorities", roles);
// 这里存入角色至JWT解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter // 这里存入角色至JWT解析JWT的角色用于鉴权的位置: ResourceServerConfig#jwtAuthenticationConverter
var authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities()) var authorities = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
.stream() .stream()
.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); .collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet));
claims.claim(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY, authorities); claims.claim(JwtClaimConstants.AUTHORITIES, authorities);
// 权限数据比较多缓存至redis
Set<String> perms = userDetails.getPerms();
redisTemplate.opsForValue().set(SecurityConstants.USER_PERMS_CACHE_KEY_PREFIX + userId, perms);
} else if (principal instanceof MemberDetails userDetails) { // 商城会员添加自定义字段 } else if (principal instanceof MemberDetails userDetails) { // 商城会员添加自定义字段
claims.claim("member_id", String.valueOf(userDetails.getId())); claims.claim(JwtClaimConstants.MEMBER_ID, String.valueOf(userDetails.getId()));
} }
}); });
} }

View File

@ -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";
}

View File

@ -2,6 +2,7 @@ package com.youlai.common.security.config;
import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.json.JSONUtil; import cn.hutool.json.JSONUtil;
import com.youlai.common.constant.JwtClaimConstants;
import com.youlai.common.constant.SecurityConstants; import com.youlai.common.constant.SecurityConstants;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
@ -112,7 +113,7 @@ public class ResourceServerConfig {
public Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() { public Converter<Jwt, ? extends AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY); jwtGrantedAuthoritiesConverter.setAuthorityPrefix(Strings.EMPTY);
jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(SecurityConstants.AUTHORITIES_CLAIM_NAME_KEY); jwtGrantedAuthoritiesConverter.setAuthoritiesClaimName(JwtClaimConstants.AUTHORITIES);
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter);

View File

@ -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<String, Object> redisTemplate;
/**
* 判断当前登录用户是否拥有操作权限
*
* @param requiredPerm 所需权限
* @return 是否有权限
*/
public boolean hasPerm(String requiredPerm) {
if (StrUtil.isBlank(requiredPerm)) {
return false;
}
// 超级管理员放行
if (SecurityUtils.isRoot()) {
return true;
}
// 获取当前登录用户的角色编码集合
Set<String> roleCodes = SecurityUtils.getRoles();
if (CollectionUtil.isEmpty(roleCodes)) {
return false;
}
// 获取当前登录用户的所有角色的权限列表
Set<String> 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<String> getRolePermsFormCache(Set<String> roleCodes) {
// 检查输入是否为空
if (CollectionUtil.isEmpty(roleCodes)) {
return Collections.emptySet();
}
Set<String> perms = new HashSet<>();
// 从缓存中一次性获取所有角色的权限
Collection<Object> roleCodesAsObjects = new ArrayList<>(roleCodes);
List<Object> rolePermsList = redisTemplate.opsForHash().multiGet(SecurityConstants.ROLE_PERMS_PREFIX, roleCodesAsObjects);
for (Object rolePermsObj : rolePermsList) {
if (rolePermsObj instanceof Set) {
@SuppressWarnings("unchecked")
Set<String> rolePerms = (Set<String>) rolePermsObj;
perms.addAll(rolePerms);
}
}
return perms;
}
}

View File

@ -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<String> perms = (Set<String>) 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;
}
}

View File

@ -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<String> perms;
}