From 1d4ba9f841414dd37780d31dfedb42ddf27b422d Mon Sep 17 00:00:00 2001 From: xiongfeng Date: Sun, 31 Aug 2025 22:51:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=99=BB=E5=BD=95=E6=97=B6=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=9D=83=E9=99=90=E5=92=8C=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E5=B9=B6=E5=8A=A0=E8=BD=BD=E5=88=B0Spring=20Security=E7=9A=84?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=87=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 14 ++ .../common/model/CustomUserDetails.java | 5 - .../xf/basedemo/config/GlobalCorsConfig.java | 87 +++++-------- .../config/security/SpringSecurityConfig.java | 48 +++++++ .../controller/business/UserController.java | 8 +- .../TokenAuthenticationFilter.java | 120 ++++++++++++++++++ .../interceptor/TokenInterceptor.java | 57 +-------- 7 files changed, 221 insertions(+), 118 deletions(-) create mode 100644 src/main/java/cn/xf/basedemo/config/security/SpringSecurityConfig.java create mode 100644 src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java diff --git a/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java b/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java index 45ef3df..d1b8d0a 100644 --- a/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java @@ -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 result = new HashMap<>(); + result.put("code", 403); + result.put("msg", "权限不足,请联系管理员"); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(result); + } + /** * 其他异常捕获 * @param request diff --git a/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java b/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java index f380858..92c33b7 100644 --- a/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java +++ b/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java @@ -37,11 +37,6 @@ public class CustomUserDetails implements UserDetails { this.authorities = authorities; } - @Override - public Collection getAuthorities() { - return null; - } - @Override public String getPassword() { return null; diff --git a/src/main/java/cn/xf/basedemo/config/GlobalCorsConfig.java b/src/main/java/cn/xf/basedemo/config/GlobalCorsConfig.java index cf9999d..b599c9a 100644 --- a/src/main/java/cn/xf/basedemo/config/GlobalCorsConfig.java +++ b/src/main/java/cn/xf/basedemo/config/GlobalCorsConfig.java @@ -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() { // 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; - } - -} +// +//} diff --git a/src/main/java/cn/xf/basedemo/config/security/SpringSecurityConfig.java b/src/main/java/cn/xf/basedemo/config/security/SpringSecurityConfig.java new file mode 100644 index 0000000..c503793 --- /dev/null +++ b/src/main/java/cn/xf/basedemo/config/security/SpringSecurityConfig.java @@ -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; + } + +} diff --git a/src/main/java/cn/xf/basedemo/controller/business/UserController.java b/src/main/java/cn/xf/basedemo/controller/business/UserController.java index 00df484..6cb9802 100644 --- a/src/main/java/cn/xf/basedemo/controller/business/UserController.java +++ b/src/main/java/cn/xf/basedemo/controller/business/UserController.java @@ -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()); } + } diff --git a/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java b/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java new file mode 100644 index 0000000..595bea4 --- /dev/null +++ b/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java @@ -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 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 permissionList = sysPermissionMapper.getPermissionListByRoleId(loginUserInfo.getId()); + //获取用户角色数据 + List roleList = sysRoleMapper.getRoleListByUserId(loginUserInfo.getId()); + if (!CollectionUtils.isEmpty(roleList)) { + //为角色拼接前缀 + roleList = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList()); + } + permissionList.addAll(roleList); + //封装用户权限角色 + List 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); + } +} diff --git a/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java b/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java index 0a5b52e..740e1c6 100644 --- a/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java +++ b/src/main/java/cn/xf/basedemo/interceptor/TokenInterceptor.java @@ -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 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 permissionList = sysPermissionMapper.getPermissionListByRoleId(loginUserInfo.getId()); - //获取用户角色数据 - List roleList = sysRoleMapper.getRoleListByUserId(loginUserInfo.getId()); - if (!CollectionUtils.isEmpty(roleList)) { - //为角色拼接前缀 - roleList = roleList.stream().map(role -> "ROLE_" + role).collect(Collectors.toList()); - } - permissionList.addAll(roleList); - //封装用户权限角色 - List 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); - } + }