mirror of
https://github.com/RemainderTime/spring-boot-base-demo.git
synced 2026-07-24 03:50:36 +08:00
feat(security): 完善安全配置和异常处理机制
- 实现全局异常处理器,统一处理登录、业务、参数校验和系统异常 - 修复CustomUserDetails中用户名获取逻辑,支持手机号或用户ID - 配置Spring Security允许登录接口和Swagger文档接口免认证访问 - 添加认证失败和权限不足的自定义异常处理机制 - 使用RequestHeaderUtil工具类统一获取token,优化token验证流程 - 移除ES同步相关废弃接口,精简用户控制器功能
This commit is contained in:
@@ -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(), "系统内部繁忙,请稍后再试");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user