登录时查询用户权限和角色并加载到Spring Security的上下文中

This commit is contained in:
xiongfeng
2025-08-31 22:51:35 +08:00
parent f5cc0345a7
commit 1d4ba9f841
7 changed files with 221 additions and 118 deletions

View File

@@ -6,6 +6,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
@@ -84,6 +85,19 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler{
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
/**
* 访问权限不足异常捕获
* @param e
* @return
*/
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity handleAccessDenied(AccessDeniedException e) {
Map<String, Object> result = new HashMap<>();
result.put("code", 403);
result.put("msg", "权限不足,请联系管理员");
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(result);
}
/**
* 其他异常捕获
* @param request

View File

@@ -37,11 +37,6 @@ public class CustomUserDetails implements UserDetails {
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword() {
return null;

View File

@@ -1,29 +1,32 @@
package cn.xf.basedemo.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
* Description: 全局跨域配置
*
*/
@Slf4j
@Configuration
public class GlobalCorsConfig {
//package cn.xf.basedemo.config;
//
//import cn.xf.basedemo.interceptor.CustomAccessDeniedHandler;
//import lombok.extern.slf4j.Slf4j;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
//import org.springframework.boot.web.servlet.FilterRegistrationBean;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.security.config.Customizer;
//import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
//import org.springframework.security.config.annotation.web.builders.HttpSecurity;
//import org.springframework.security.web.SecurityFilterChain;
//import org.springframework.web.cors.CorsConfiguration;
//import org.springframework.web.cors.CorsConfigurationSource;
//import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
//import org.springframework.web.filter.CorsFilter;
//
///**
// * Description: 全局跨域配置
// *
// */
//@Slf4j
//@Configuration
//@EnableMethodSecurity(prePostEnabled = true) // 开启 @PreAuthorize/@PostAuthorize
//public class GlobalCorsConfig {
//
// @ConditionalOnMissingBean
// @Bean
// public CorsFilter corsFilter() {
// public FilterRegistrationBean<CorsFilter> corsFilter() {
// CorsConfiguration config = new CorsConfiguration();
// // 放行哪些原始域
// //config.addAllowedOrigin("*");
@@ -39,36 +42,10 @@ public class GlobalCorsConfig {
// UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
// configSource.registerCorsConfiguration("/**", config);
//
//// FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(configSource));
// FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(configSource));
// // 这个顺序很重要哦,为避免麻烦请设置在最前
//// bean.setOrder(0);
// return new CorsFilter(configSource);
// bean.setOrder(0);
// return bean;
// }
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // 开启 CORS
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user/login", "/web/**").permitAll() // 放行登录、注册接口
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
//
//}

View File

@@ -0,0 +1,48 @@
package cn.xf.basedemo.config.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
/**
* Description: spring security体系 全局跨域配置
*/
@Slf4j
@Configuration
@EnableMethodSecurity(prePostEnabled = true) // 开启 @PreAuthorize/@PostAuthorize
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(Customizer.withDefaults()) // 开启 CORS
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/**", "/web/**").permitAll() // 放行登录、注册接口
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}

View File

@@ -40,8 +40,10 @@ public class UserController {
@PostMapping("/info")
@PreAuthorize("hasAuthority('user:add')") // 权限控制
public RetObj info(){
LoginUser loginUser = SessionContext.getInstance().get();
return RetObj.success(loginUser);
// LoginUser loginUser = SessionContext.getInstance().get();
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
return RetObj.success(user);
}
@Operation(summary = "es同步用户信息", description = "用户信息")
@@ -56,7 +58,6 @@ public class UserController {
public RetObj getEsId(Long userId){
return userService.getEsId(userId);
}
@Operation(summary = "获取用户权限数据", description = "用户信息")
@GetMapping("/getPermission")
public RetObj getPermission(){
@@ -65,4 +66,5 @@ public class UserController {
return RetObj.success(user.getAuthorities());
}
}

View File

@@ -0,0 +1,120 @@
package cn.xf.basedemo.interceptor;
import cn.xf.basedemo.common.exception.LoginException;
import cn.xf.basedemo.common.exception.ResponseCode;
import cn.xf.basedemo.common.model.CustomUserDetails;
import cn.xf.basedemo.common.model.LoginUser;
import cn.xf.basedemo.common.utils.ApplicationContextUtils;
import cn.xf.basedemo.mappers.SysPermissionMapper;
import cn.xf.basedemo.mappers.SysRoleMapper;
import com.alibaba.fastjson.JSONObject;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
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.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @Description: 登录权限校验过滤器(过滤器职责:登录认证和权限恢复)
* @ClassName: TokenAuthenticationFilter
* @Author: xiongfeng
* @Date: 2025/8/28 22:41
* @Version: 1.0
*/
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
//不拦截的请求列表
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login", "/swagger-ui.html", "/v3/api-docs", "/swagger-ui/index.html");
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SysPermissionMapper sysPermissionMapper;
@Autowired
private SysRoleMapper sysRoleMapper;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//登录处理
try {
String requestURI = request.getRequestURI();
if (EXCLUDE_PATH_LIST.contains(requestURI) ||
requestURI.contains("/swagger-ui") ||
requestURI.contains("/v3/api-docs")) {
filterChain.doFilter(request, response);
return;
}
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token))
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
throw new LoginException("请先登录");
}
String value = (String) redisTemplate.opsForValue().get("token:" + token);
if (StringUtils.isEmpty(value)) {
throw new LoginException();
}
JSONObject jsonObject = JSONObject.parseObject(value);
//JSON对象转换成Java对象
LoginUser loginUserInfo = JSONObject.toJavaObject(jsonObject, LoginUser.class);
if (loginUserInfo == null || loginUserInfo.getId() <= 0) {
throw new LoginException(ResponseCode.USER_INPUT_ERROR);
}
redisTemplate.expire(token, 86700, TimeUnit.SECONDS);
//用户信息设置到上下文(如果使用Spring security 也可设置登录用户上下文数据,下面就可不用自定义设置)
SessionContext.getInstance().set(loginUserInfo);
//设置用户权限角色
this.setSpringSecurityContext(loginUserInfo);
filterChain.doFilter(request, response);
}catch (LoginException e) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}");
}
}
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
// Long userId = user.getUserId(); // 拿到登录用户 ID
/**
* 设置用户权限角色 Spring Security 本身的 SecurityContext 是请求级别的,每次请求都会被清理,所以每次请求都会查询权限数据并设置,
* 安全但是很慢所以可以做一些优化比如把权限数据放到redis中获取和用户信息一起放在jwt中然后登录时解析在设置到Spring security上下文中
* @param loginUserInfo
*/
private void setSpringSecurityContext(LoginUser loginUserInfo) {
//获取登录用户权限数据
List<String> permissionList = sysPermissionMapper.getPermissionListByRoleId(loginUserInfo.getId());
//获取用户角色数据
List<String> roleList = sysRoleMapper.getRoleListByUserId(loginUserInfo.getId());
if (!CollectionUtils.isEmpty(roleList)) {
//为角色拼接前缀
roleList = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());
}
permissionList.addAll(roleList);
//封装用户权限角色
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(permissionList);
//设置用户信息到SpringSecurity上下文
UserDetails userDetails = new CustomUserDetails(loginUserInfo.getId(), loginUserInfo.getPhone(), authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

View File

@@ -35,20 +35,13 @@ import java.util.stream.Collectors;
/**
* @program: spring-boot-base-demo
* @ClassName TokenInterceptor
* @description:
* @description: 拦截器职责(日志、请求校验、限流)
* @author: xiongfeng
* @create: 2022-06-16 14:17
**/
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate redisTemplate;
private SysPermissionMapper sysPermissionMapper = ApplicationContextUtils.getBean(SysPermissionMapper.class);
private SysRoleMapper sysRoleMapper = ApplicationContextUtils.getBean(SysRoleMapper.class);
//不拦截的请求列表
private static final List<String> EXCLUDE_PATH_LIST = Arrays.asList("/user/login", "/web/login", "/swagger-ui.html", "/v3/api-docs", "/swagger-ui/index.html");
@@ -61,33 +54,8 @@ public class TokenInterceptor implements HandlerInterceptor {
requestURI.contains("/v3/api-docs")) {
return true;
}
//登录处理
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token))
token = request.getParameter("token");
if (StringUtils.isEmpty(token)) {
throw new LoginException("请先登录");
}
String value = (String) redisTemplate.opsForValue().get("token:" + token);
if (StringUtils.isEmpty(value)) {
throw new LoginException();
}
JSONObject jsonObject = JSONObject.parseObject(value);
//JSON对象转换成Java对象
LoginUser loginUserInfo = JSONObject.toJavaObject(jsonObject, LoginUser.class);
if (loginUserInfo == null || loginUserInfo.getId() <= 0) {
throw new LoginException(ResponseCode.USER_INPUT_ERROR);
}
redisTemplate.expire(token, 86700, TimeUnit.SECONDS);
//设置用户权限角色
this.setSpringSecurityContext(loginUserInfo);
//用户信息设置到上下文(如果使用Spring security 也可设置登录用户上下文数据,下面就可不用自定义设置)
SessionContext.getInstance().set(loginUserInfo);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
// Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// CustomUserDetails user = (CustomUserDetails) auth.getPrincipal();
// Long userId = user.getUserId(); // 拿到登录用户 ID
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
@@ -95,26 +63,5 @@ public class TokenInterceptor implements HandlerInterceptor {
SessionContext.getInstance().clear();
}
/**
* 设置用户权限角色 Spring Security 本身的 SecurityContext 是请求级别的,每次请求都会被清理,所以每次请求都会查询权限数据并设置,
* 安全但是很慢所以可以做一些优化比如把权限数据放到redis中获取和用户信息一起放在jwt中然后登录时解析在设置到Spring security上下文中
* @param loginUserInfo
*/
private void setSpringSecurityContext(LoginUser loginUserInfo) {
//获取登录用户权限数据
List<String> permissionList = sysPermissionMapper.getPermissionListByRoleId(loginUserInfo.getId());
//获取用户角色数据
List<String> roleList = sysRoleMapper.getRoleListByUserId(loginUserInfo.getId());
if (!CollectionUtils.isEmpty(roleList)) {
//为角色拼接前缀
roleList = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList());
}
permissionList.addAll(roleList);
//封装用户权限角色
List<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(permissionList);
//设置用户信息到SpringSecurity上下文
UserDetails userDetails = new CustomUserDetails(loginUserInfo.getId(), loginUserInfo.getPhone(), authorities);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}