[JWT]登录认证授权使用SpringSecurity
一、预备内容
1. 统一响应结果的封装
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Integer code;//业务状态码 0-成功 1-失败 401-token过期 403-权限不足
private String message;//提示信息
private T data;//响应数据
//快速返回操作成功响应结果(带响应数据)
public static <E> Result<E> success(E data) {
return new Result<>(0, "操作成功", data);
}
//快速返回操作成功响应结果
public static Result success() {
return new Result(0, "操作成功", null);
}
public static Result error(String message) {
return new Result(1, message, null);
}
public static Result error(String message, Integer code) {
return new Result(code, message, null);
}
}
2. 用户实体类
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Long id;
private String username;
private String email;
private String phone;
private String password;
private String nickname;
private Integer role;
private String wechatOpenid;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserLoginDTO {
private String sessionId; // 会话id,全局唯一
private Long id;
private String username;
private String email;
private String phone;
private String nickname;
private Integer role;
private String wechatOpenid;
private Long expiredTime; // 过期时间
}
3. 异常处理
BaseException
public class BaseException extends RuntimeException {
private final HttpStatus httpStatus;
@Setter
private Integer code; // 自定义一个全局唯一的code,
public BaseException() {
httpStatus = HttpStatus.BAD_REQUEST;
}
public BaseException(HttpStatus httpStatus) {
this.httpStatus = httpStatus;
}
public BaseException(String msg, HttpStatus httpStatus) {
super(msg);
this.httpStatus = httpStatus;
}
public HttpStatus getHttpStatus() {
return httpStatus;
}
public Integer getCode() {
return code;
}
}
ExceptionTool
public class ExceptionTool {
private static final HttpStatus defaultHttpStatus = HttpStatus.BAD_REQUEST;
public static void throwException(String message) {
BaseException baseException = new BaseException(message, defaultHttpStatus);
baseException.setCode(1);
throw baseException;
}
public static void throwException(String message, Integer code) {
BaseException baseException = new BaseException(message, defaultHttpStatus);
baseException.setCode(code);
throw baseException;
}
public static void throwException(String message, HttpStatus httpStatus, Integer errorCode) {
BaseException baseException = new BaseException(message, httpStatus);
baseException.setCode(errorCode);
throw baseException;
}
}
GlobalExceptionHandler
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BaseException.class)
public Result exceptionHandler(HttpServletResponse response, BaseException e) {
response.setStatus(e.getHttpStatus().value());
return createResult(e);
}
private Result createResult(BaseException e) {
return Result.error(e.getMessage(), e.getCode() == null ? 1 : e.getCode());
}
// 处理认证异常
@ExceptionHandler(AuthenticationException.class)
public Result<Void> handleAuthenticationException(AuthenticationException e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "认证失败!", 401);
}
// 处理授权异常
@ExceptionHandler(AccessDeniedException.class)
public Result<Void> handleAccessDeniedException(AccessDeniedException e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "权限不足!");
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result handleVaildException(MethodArgumentNotValidException e){
BindingResult bindingResult = e.getBindingResult();
StringBuffer stringBuffer = new StringBuffer();
bindingResult.getFieldErrors().forEach(item ->{
//获取错误信息
String message = item.getDefaultMessage();
//获取错误的属性名字
String field = item.getField();
stringBuffer.append(field + ":" + message + " ");
});
return Result.error(stringBuffer + "");
}
@ExceptionHandler(Exception.class)
public Result handleException(Exception e){
e.printStackTrace();
return Result.error(StringUtils.hasLength(e.getMessage())?e.getMessage():"操作失败!");
}
}
二、SpringSecurity基本配置
1. 异常处理
CustomAccessDeniedHandler
:处理授权失败(即用户权限不足)时的异常。
CustomAuthenticationExceptionHandler
:处理认证失败时的异常。
CustomSecurityExceptionHandler
:全局过滤器,用于统一处理各种安全相关的异常。
CustomAccessDeniedHandler
实现了Spring Security的 AccessDeniedHandler
接口,用于处理用户在尝试访问受保护资源时,因权限不足而被拒绝访问的情况。
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
public CustomAccessDeniedHandler(ObjectMapper objectMapper){
this.objectMapper = objectMapper;
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = Result.error("权限不足,拒绝访问!");
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.getWriter().write(objectMapper.writeValueAsString(result));
}
}
CustomAuthenticationExceptionHandler
实现了Spring Security的 AuthenticationEntryPoint
接口,用于处理用户在认证过程中失败的情况,如未登录或认证失败。
@Component
public class CustomAuthenticationExceptionHandler implements AuthenticationEntryPoint {
// private final ObjectMapper objectMapper;
// public CustomAuthenticationExceptionHandler(ObjectMapper objectMapper){
// this.objectMapper = objectMapper;
// }
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
PrintWriter writer = response.getWriter();
writer.print(JSONUtil.stringify(Result.error("${authentication.fail:认证失败}", 1)));
writer.flush();
writer.close();
}
}
CustomSecurityExceptionHandler
继承自 OncePerRequestFilter
,是一个自定义的全局安全异常处理过滤器。它用于捕获和处理过滤器链中抛出的各种安全相关异常,确保系统能够返回统一的错误响应。
public class CustomSecurityExceptionHandler extends OncePerRequestFilter {
public static final Logger logger = LoggerFactory.getLogger(CustomSecurityExceptionHandler.class);
@Override
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (BaseException e) {
// 自定义异常
Result result = Result.error(e.getMessage(), e.getCode());
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(e.getHttpStatus().value());
PrintWriter writer = response.getWriter();
writer.write(JSONUtil.stringify(result));
writer.flush();
writer.close();
} catch (AuthenticationException | AccessDeniedException e) {
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.FORBIDDEN.value());
PrintWriter writer = response.getWriter();
writer.print(JSONUtil.stringify(Result.error(e.getMessage())));
writer.flush();
writer.close();
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 未知异常
Result result = Result.error("系统异常");
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
PrintWriter writer = response.getWriter();
writer.write(JSONUtil.stringify(result));
writer.flush();
writer.close();
}
}
}
2. SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final ApplicationContext applicationContext;
private final CustomAuthenticationExceptionHandler customAuthenticationExceptionHandler;
private final AuthenticationEntryPoint authenticationExceptionHandler = new CustomAuthenticationExceptionHandler();
private final Filter globalSpringSecurityExceptionHandler = new CustomSecurityExceptionHandler();
private final CustomAccessDeniedHandler customAccessDeniedHandler;
@Autowired
public SecurityConfig(
ApplicationContext applicationContext,
CustomAuthenticationExceptionHandler customAuthenticationExceptionHandler,
CustomAccessDeniedHandler customAccessDeniedHandler
){
this.applicationContext = applicationContext;
this.customAuthenticationExceptionHandler = customAuthenticationExceptionHandler;
this.customAccessDeniedHandler = customAccessDeniedHandler;
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
private void commonHttpSetting(HttpSecurity http) throws Exception {
// 禁用SpringSecurity默认filter。这些filter都是非前后端分离项目的产物,用不上.
// yml配置文件将日志设置DEBUG模式,就能看到加载了哪些filter
// logging:
// level:
// org.springframework.security: DEBUG
// 表单登录/登出、session管理、csrf防护等默认配置,如果不disable。会默认创建默认filter
http.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.sessionManagement(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
// requestCache用于重定向,前后端分析项目无需重定向,requestCache也用不上
.requestCache(cache -> cache
.requestCache(new NullRequestCache())
)
// 无需给用户一个匿名身份
.anonymous(AbstractHttpConfigurer::disable);
// 处理 SpringSecurity 异常响应结果。响应数据的结构,改成业务统一的JSON结构。不要框架默认的响应结构
http.exceptionHandling(exceptionHandling ->
exceptionHandling
// 认证失败异常
.authenticationEntryPoint(authenticationExceptionHandler)
// 鉴权失败异常
// .accessDeniedHandler(authorizationExceptionHandler)
);
// 其他未知异常. 尽量提前加载。
http.addFilterBefore(globalSpringSecurityExceptionHandler, SecurityContextHolderFilter.class);
}
@Bean
public SecurityFilterChain loginFilterChain(HttpSecurity http) throws Exception {
commonHttpSetting(http);
// 使用securityMatcher限定当前配置作用的路径
http.securityMatcher("/public/user/login/*")
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
// 异常处理
http
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationExceptionHandler)
.accessDeniedHandler(customAccessDeniedHandler));
return http.build();
}
/** 其余路径,走这个默认过滤链 */
@Bean
@Order(Integer.MAX_VALUE) // 这个过滤链最后加载
public SecurityFilterChain defaultApiFilterChain(HttpSecurity http) throws Exception {
commonHttpSetting(http);
http // 不用securityMatcher表示缺省值,匹配不上其他过滤链时,都走这个过滤链
.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated());
http.addFilterBefore(new PublicApiAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
3. PublicApiAuthenticationFilter
public class PublicApiAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
System.out.println("这里是默认过滤链...");
// 随便给个默认身份
Authentication authentication = new TestingAuthenticationToken("username", "password", "ROLE_TOURIST");
SecurityContextHolder.getContext().setAuthentication(authentication);
// 放行
filterChain.doFilter(request, response);
}
}
三、登录认证流程
获取当前用户的信息
public class CurrentUserUtil {
/**
* 获取当前认证的用户信息
*
* @return 当前用户的 UserPrincipal 对象,若未认证则返回 null
*/
public static UserLoginDTO getCurrentUser() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()
&& !(authentication.getPrincipal() instanceof String)) { // 避免匿名用户
Object principal = authentication.getPrincipal();
if (principal instanceof UserLoginDTO) {
return (UserLoginDTO) principal;
}
}
return null;
}
}