From 1b89fdc1b6a5724c2f2bbc3b1f93e3dcc1d13f7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=B7=E8=A8=80?= <2439534736@qq.com> Date: Fri, 5 Jun 2026 14:53:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E5=AE=8C=E5=96=84=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E9=85=8D=E7=BD=AE=E5=92=8C=E5=BC=82=E5=B8=B8=E5=A4=84?= =?UTF-8?q?=E7=90=86=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现全局异常处理器,统一处理登录、业务、参数校验和系统异常 - 修复CustomUserDetails中用户名获取逻辑,支持手机号或用户ID - 配置Spring Security允许登录接口和Swagger文档接口免认证访问 - 添加认证失败和权限不足的自定义异常处理机制 - 使用RequestHeaderUtil工具类统一获取token,优化token验证流程 - 移除ES同步相关废弃接口,精简用户控制器功能 --- .../exception/GlobalExceptionHandler.java | 82 +++++++++++++++++++ .../common/model/CustomUserDetails.java | 2 +- .../controller/business/UserController.java | 11 --- .../interceptor/SpringSecurityConfig.java | 23 +++++- .../TokenAuthenticationFilter.java | 17 ++-- 5 files changed, 112 insertions(+), 23 deletions(-) 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 e69de29..42bbd7a 100644 --- a/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/cn/xf/basedemo/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,82 @@ +package cn.xf.basedemo.common.exception; + +import cn.xf.basedemo.common.enums.SystemStatus; +import cn.xf.basedemo.common.model.RetObj; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import jakarta.servlet.http.HttpServletRequest; + +/** + * @Description: 全局异常处理类 (使用 @RestControllerAdvice 替代旧版 + * HandlerExceptionResolver) + * @Author: xiongfeng + * @Date: 2025/1/9 + * @Version: 2.0 + */ +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理登录/认证异常 + */ + @ExceptionHandler(LoginException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public RetObj handleLoginException(LoginException e, HttpServletRequest request) { + log.warn("认证失败 [URL:{}]: code={}, message={}", request.getRequestURI(), e.getStatus().getCode(), e.getMessage()); + return new RetObj<>(e.getStatus().getCode(), e.getMessage()); + } + + /** + * 处理业务逻辑异常 + */ + @ExceptionHandler(BusinessException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) // 或者使用 HttpStatus.OK,根据前端约定 + public RetObj handleBusinessException(BusinessException e, HttpServletRequest request) { + log.warn("业务异常 [URL:{}]: code={}, message={}", request.getRequestURI(), e.getStatus().getCode(), e.getMessage()); + return new RetObj<>(e.getStatus().getCode(), e.getMessage()); + } + + /** + * 处理参数校验异常 (处理 @Valid / @Validated 触发的异常) + */ + @ExceptionHandler({ MethodArgumentNotValidException.class, BindException.class }) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public RetObj handleValidationException(Exception e, HttpServletRequest request) { + BindingResult bindingResult = null; + if (e instanceof MethodArgumentNotValidException) { + bindingResult = ((MethodArgumentNotValidException) e).getBindingResult(); + } else if (e instanceof BindException) { + bindingResult = ((BindException) e).getBindingResult(); + } + + String errorMsg = "参数校验失败"; + if (bindingResult != null && bindingResult.hasErrors()) { + FieldError fieldError = bindingResult.getFieldError(); + if (fieldError != null) { + errorMsg = fieldError.getDefaultMessage(); + } + } + log.warn("参数校验失败 [URL:{}]: {}", request.getRequestURI(), errorMsg); + return new RetObj<>(SystemStatus.USER_INPUT_ERROR.getCode(), errorMsg); + } + + /** + * 处理所有未知的系统异常 (兜底) + */ + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public RetObj handleSystemException(Exception e, HttpServletRequest request) { + // 生产级关键点:必须记录异常堆栈,否则无法排查 BUG + log.error("系统发生未知异常 [URL:{}]", request.getRequestURI(), e); + return new RetObj<>(SystemStatus.ERROR.getCode(), "系统内部繁忙,请稍后再试"); + } +} 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 92c33b7..81ac7d7 100644 --- a/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java +++ b/src/main/java/cn/xf/basedemo/common/model/CustomUserDetails.java @@ -44,6 +44,6 @@ public class CustomUserDetails implements UserDetails { @Override public String getUsername() { - return null; + return phone != null ? phone : (userId != null ? String.valueOf(userId) : null); } } 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 f04ba67..8a1ba45 100644 --- a/src/main/java/cn/xf/basedemo/controller/business/UserController.java +++ b/src/main/java/cn/xf/basedemo/controller/business/UserController.java @@ -47,18 +47,7 @@ public class UserController { return RetObj.success(user); } - @Operation(summary = "es同步用户信息", description = "用户信息") - @GetMapping("/syncEs") - @PreAuthorize("hasRole('admin')") // 角色控制 - public RetObj syncEs(Long userId){ - return userService.syncEs(userId); - } - @Operation(summary = "es查询用户信息", description = "用户信息") - @GetMapping("/getEsId") - public RetObj getEsId(Long userId){ - return userService.getEsId(userId); - } @Operation(summary = "获取用户权限数据", description = "用户信息") @GetMapping("/getPermission") public RetObj getPermission(){ diff --git a/src/main/java/cn/xf/basedemo/interceptor/SpringSecurityConfig.java b/src/main/java/cn/xf/basedemo/interceptor/SpringSecurityConfig.java index 13ed1e6..b10da62 100644 --- a/src/main/java/cn/xf/basedemo/interceptor/SpringSecurityConfig.java +++ b/src/main/java/cn/xf/basedemo/interceptor/SpringSecurityConfig.java @@ -12,6 +12,10 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import cn.xf.basedemo.common.enums.SystemStatus; +import cn.xf.basedemo.common.model.RetObj; +import com.alibaba.fastjson.JSONObject; +import jakarta.servlet.http.HttpServletResponse; /** * Description: spring security体系 全局跨域配置 @@ -28,9 +32,26 @@ public class SpringSecurityConfig { .cors(Customizer.withDefaults()) // 开启 CORS .csrf(AbstractHttpConfigurer::disable) //前后端分离 禁用csrf .authorizeHttpRequests(auth -> auth + .requestMatchers("/user/login", "/web/login").permitAll() + .requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/doc.html", "/webjars/**", "/swagger-resources/**").permitAll() .anyRequest().authenticated() ) // 把自定义过滤器插入 Spring Security 过滤器链 - .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .exceptionHandling(exceptions -> exceptions + .authenticationEntryPoint((request, response, authException) -> { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setContentType("application/json;charset=UTF-8"); + RetObj retObj = new RetObj<>(SystemStatus.UNAUTHORIZED.getCode(), "未登录或Token失效"); + response.getWriter().write(JSONObject.toJSONString(retObj)); + }) + .accessDeniedHandler((request, response, accessDeniedException) -> { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.setContentType("application/json;charset=UTF-8"); + RetObj retObj = new RetObj<>(SystemStatus.FORBIDDEN.getCode(), "没有权限访问该资源"); + response.getWriter().write(JSONObject.toJSONString(retObj)); + }) + ); return http.build(); } diff --git a/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java b/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java index 3cbd8ac..a6ce42c 100644 --- a/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java +++ b/src/main/java/cn/xf/basedemo/interceptor/TokenAuthenticationFilter.java @@ -1,10 +1,12 @@ package cn.xf.basedemo.interceptor; +import cn.xf.basedemo.common.enums.SystemStatus; 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.model.RetObj; import cn.xf.basedemo.common.utils.ApplicationContextUtils; +import cn.xf.basedemo.common.utils.RequestHeaderUtil; import cn.xf.basedemo.mappers.SysPermissionMapper; import cn.xf.basedemo.mappers.SysRoleMapper; import com.alibaba.fastjson.JSONObject; @@ -63,12 +65,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { 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 token = RequestHeaderUtil.getToken(request); String value = (String) redisTemplate.opsForValue().get("token:" + token); if (StringUtils.isEmpty(value)) { throw new LoginException(); @@ -77,9 +74,9 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { //JSON对象转换成Java对象 LoginUser loginUserInfo = JSONObject.toJavaObject(jsonObject, LoginUser.class); if (loginUserInfo == null || loginUserInfo.getId() <= 0) { - throw new LoginException(ResponseCode.USER_INPUT_ERROR); + throw new LoginException(SystemStatus.USER_INPUT_ERROR); } - redisTemplate.expire(token, 86700, TimeUnit.SECONDS); + redisTemplate.expire("token:" + token, 86700, TimeUnit.SECONDS); //用户信息设置到上下文(如果使用Spring security 也可设置登录用户上下文数据,下面就可不用自定义设置) SessionContext.getInstance().set(loginUserInfo); //设置用户权限角色 @@ -88,7 +85,7 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { }catch (LoginException e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType("application/json;charset=UTF-8"); - response.getWriter().write("{\"message\":\"" + e.getMessage() + "\"}"); + response.getWriter().write(JSONObject.toJSONString(new RetObj<>(e.getStatus().getCode(), e.getMessage()))); }finally { // 无论请求是否异常,最后一定清理,避免 ThreadLocal 泄漏 SessionContext.getInstance().clear();