mirror of
https://github.com/RemainderTime/spring-boot-base-demo.git
synced 2026-07-23 11:30:34 +08:00
feat(security): 添加用户角色权限缓存支持无状态会话
- 在LoginUser模型中添加roles和permissions字段用于缓存用户权限数据 - 配置Spring Security使用无状态会话管理策略 - 移除TokenAuthenticationFilter中的数据库查询依赖改用缓存数据 - 在UserServiceImpl中将角色权限数据预加载到Redis缓存中 - 优化安全上下文设置逻辑提高性能减少重复查询 - 清理ThreadLocal和SecurityContext避免内存泄漏
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) -> {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user