feat(security): 添加用户角色权限缓存支持无状态会话

- 在LoginUser模型中添加roles和permissions字段用于缓存用户权限数据
- 配置Spring Security使用无状态会话管理策略
- 移除TokenAuthenticationFilter中的数据库查询依赖改用缓存数据
- 在UserServiceImpl中将角色权限数据预加载到Redis缓存中
- 优化安全上下文设置逻辑提高性能减少重复查询
- 清理ThreadLocal和SecurityContext避免内存泄漏
This commit is contained in:
海言
2026-06-05 15:10:12 +08:00
parent 1b89fdc1b6
commit 5939277fc6
4 changed files with 37 additions and 20 deletions

View File

@@ -23,4 +23,14 @@ public class LoginUser implements Serializable {
private String phone; private String phone;
private String token; private String token;
/**
* 角色列表
*/
private java.util.List<String> roles;
/**
* 权限列表
*/
private java.util.List<String> permissions;
} }

View File

@@ -9,6 +9,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@@ -37,6 +38,9 @@ public class SpringSecurityConfig {
.requestMatchers("/doc.html", "/webjars/**", "/swagger-resources/**").permitAll() .requestMatchers("/doc.html", "/webjars/**", "/swagger-resources/**").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
) // 把自定义过滤器插入 Spring Security 过滤器链 ) // 把自定义过滤器插入 Spring Security 过滤器链
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling(exceptions -> exceptions .exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint((request, response, authException) -> { .authenticationEntryPoint((request, response, authException) -> {

View File

@@ -7,8 +7,6 @@ import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.model.RetObj; import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.common.utils.ApplicationContextUtils; import cn.xf.basedemo.common.utils.ApplicationContextUtils;
import cn.xf.basedemo.common.utils.RequestHeaderUtil; import cn.xf.basedemo.common.utils.RequestHeaderUtil;
import cn.xf.basedemo.mappers.SysPermissionMapper;
import cn.xf.basedemo.mappers.SysRoleMapper;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException; import jakarta.servlet.ServletException;
@@ -48,12 +46,6 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
@Autowired @Autowired
private RedisTemplate redisTemplate; private RedisTemplate redisTemplate;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//登录处理 //登录处理
@@ -89,29 +81,28 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
}finally { }finally {
// 无论请求是否异常,最后一定清理,避免 ThreadLocal 泄漏 // 无论请求是否异常,最后一定清理,避免 ThreadLocal 泄漏
SessionContext.getInstance().clear(); SessionContext.getInstance().clear();
SecurityContextHolder.clearContext();
} }
} }
// Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); // CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
// Long userId = user.getUserId(); // 拿到登录用户 ID // Long userId = user.getUserId(); // 拿到登录用户 ID
/**
* 设置用户权限角色 Spring Security 本身的 SecurityContext 是请求级别的,每次请求都会被清理,所以每次请求都会查询权限数据并设置,
* 安全但是很慢所以可以做一些优化比如把权限数据放到redis中获取和用户信息一起放在jwt中然后登录时解析在设置到Spring security上下文中
* @param loginUserInfo
*/
private void setSpringSecurityContext(LoginUser loginUserInfo) { private void setSpringSecurityContext(LoginUser loginUserInfo) {
//获取登录用户权限数据 //直接从缓存中获取登录用户权限和角色数据
List<String> permissionList = sysPermissionMapper.getPermissionListByRoleId(loginUserInfo.getId()); List<String> permissionList = loginUserInfo.getPermissions();
//获取用户角色数据 List<String> roleList = loginUserInfo.getRoles();
List<String> roleList = sysRoleMapper.getRoleListByUserId(loginUserInfo.getId()); List<String> authoritiesList = new java.util.ArrayList<>();
if (!CollectionUtils.isEmpty(permissionList)) {
authoritiesList.addAll(permissionList);
}
if (!CollectionUtils.isEmpty(roleList)) { if (!CollectionUtils.isEmpty(roleList)) {
//为角色拼接前缀 //为角色拼接前缀
roleList = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList()); List<String> roleAuthorities = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());
authoritiesList.addAll(roleAuthorities);
} }
permissionList.addAll(roleList);
//封装用户权限角色 //封装用户权限角色
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(permissionList); List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(authoritiesList);
//设置用户信息到SpringSecurity上下文 //设置用户信息到SpringSecurity上下文
UserDetails userDetails = new CustomUserDetails(loginUserInfo.getId(), loginUserInfo.getPhone(), authorities); UserDetails userDetails = new CustomUserDetails(loginUserInfo.getId(), loginUserInfo.getPhone(), authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

View File

@@ -6,6 +6,8 @@ import cn.xf.basedemo.common.model.RetObj;
import cn.xf.basedemo.common.utils.JwtTokenUtils; import cn.xf.basedemo.common.utils.JwtTokenUtils;
import cn.xf.basedemo.common.utils.RSAUtils; import cn.xf.basedemo.common.utils.RSAUtils;
import cn.xf.basedemo.config.GlobalConfig; import cn.xf.basedemo.config.GlobalConfig;
import cn.xf.basedemo.mappers.SysPermissionMapper;
import cn.xf.basedemo.mappers.SysRoleMapper;
import cn.xf.basedemo.mappers.UserMapper; import cn.xf.basedemo.mappers.UserMapper;
import cn.xf.basedemo.model.domain.User; import cn.xf.basedemo.model.domain.User;
import cn.xf.basedemo.model.res.LoginInfoRes; import cn.xf.basedemo.model.res.LoginInfoRes;
@@ -46,6 +48,12 @@ public class UserServiceImpl implements UserService {
@Autowired @Autowired
private RedisTemplate redisTemplate; private RedisTemplate redisTemplate;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Override @Override
public RetObj login(LoginInfoRes res) { public RetObj login(LoginInfoRes res) {
@@ -84,6 +92,10 @@ public class UserServiceImpl implements UserService {
String token = JwtTokenUtils.createToken(user.getId()); String token = JwtTokenUtils.createToken(user.getId());
loginUser.setToken(token); loginUser.setToken(token);
// 获取并缓存角色与权限数据到 Redis 避免后续过滤器的重复查库性能开销
loginUser.setPermissions(sysPermissionMapper.getPermissionListByRoleId(user.getId()));
loginUser.setRoles(sysRoleMapper.getRoleListByUserId(user.getId()));
redisTemplate.opsForValue().set("token:" + token, JSONObject.toJSONString(loginUser), 3600, TimeUnit.SECONDS); redisTemplate.opsForValue().set("token:" + token, JSONObject.toJSONString(loginUser), 3600, TimeUnit.SECONDS);
redisTemplate.opsForValue().set("user_login_token:" + user.getId(), token, 3600, TimeUnit.SECONDS); redisTemplate.opsForValue().set("user_login_token:" + user.getId(), token, 3600, TimeUnit.SECONDS);
return RetObj.success(loginUser); return RetObj.success(loginUser);