feat(security): 完善安全配置和异常处理机制

- 实现全局异常处理器,统一处理登录、业务、参数校验和系统异常
- 修复CustomUserDetails中用户名获取逻辑,支持手机号或用户ID
- 配置Spring Security允许登录接口和Swagger文档接口免认证访问
- 添加认证失败和权限不足的自定义异常处理机制
- 使用RequestHeaderUtil工具类统一获取token,优化token验证流程
- 移除ES同步相关废弃接口,精简用户控制器功能
This commit is contained in:
海言
2026-06-05 14:53:06 +08:00
parent 2e6e75c61a
commit 1b89fdc1b6
5 changed files with 112 additions and 23 deletions

View File

@@ -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<Void> 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<Void> 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<Void> 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<Void> handleSystemException(Exception e, HttpServletRequest request) {
// 生产级关键点:必须记录异常堆栈,否则无法排查 BUG
log.error("系统发生未知异常 [URL:{}]", request.getRequestURI(), e);
return new RetObj<>(SystemStatus.ERROR.getCode(), "系统内部繁忙,请稍后再试");
}
}

View File

@@ -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);
}
}

View File

@@ -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(){

View File

@@ -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<Void> 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<Void> retObj = new RetObj<>(SystemStatus.FORBIDDEN.getCode(), "没有权限访问该资源");
response.getWriter().write(JSONObject.toJSONString(retObj));
})
);
return http.build();
}

View File

@@ -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();