增加登录的异常,修复重复调用error

This commit is contained in:
dengqichen 2024-11-29 18:39:46 +08:00
parent e3aa3d1aca
commit 527b7563c0
7 changed files with 142 additions and 41 deletions

View File

@ -32,7 +32,12 @@ public enum ResponseCode {
DEPENDENCY_INJECTION_SERVICE_NOT_FOUND(1100, "dependency.injection.service.not.found"), DEPENDENCY_INJECTION_SERVICE_NOT_FOUND(1100, "dependency.injection.service.not.found"),
DEPENDENCY_INJECTION_REPOSITORY_NOT_FOUND(1101, "dependency.injection.repository.not.found"), DEPENDENCY_INJECTION_REPOSITORY_NOT_FOUND(1101, "dependency.injection.repository.not.found"),
DEPENDENCY_INJECTION_CONVERTER_NOT_FOUND(1102, "dependency.injection.converter.not.found"), DEPENDENCY_INJECTION_CONVERTER_NOT_FOUND(1102, "dependency.injection.converter.not.found"),
DEPENDENCY_INJECTION_ENTITYPATH_FAILED(1103, "dependency.injection.entitypath.failed"); DEPENDENCY_INJECTION_ENTITYPATH_FAILED(1103, "dependency.injection.entitypath.failed"),
// JWT相关错误码 (2100-2199)
JWT_EXPIRED(2100, "jwt.token.expired"),
JWT_INVALID(2101, "jwt.token.invalid"),
JWT_MISSING(2102, "jwt.token.missing");
private final int code; private final int code;

View File

@ -2,6 +2,8 @@ package com.qqchen.deploy.backend.framework.exception;
import com.qqchen.deploy.backend.framework.api.Response; import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.enums.ResponseCode; import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; import org.springframework.context.MessageSource;
@ -9,6 +11,8 @@ import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.security.SignatureException;
@Slf4j @Slf4j
@RestControllerAdvice @RestControllerAdvice
public class GlobalExceptionHandler { public class GlobalExceptionHandler {
@ -37,4 +41,16 @@ public class GlobalExceptionHandler {
log.error("Unexpected error occurred", e); log.error("Unexpected error occurred", e);
return Response.error(ResponseCode.ERROR); return Response.error(ResponseCode.ERROR);
} }
@ExceptionHandler(ExpiredJwtException.class)
public Response<?> handleExpiredJwtException(ExpiredJwtException e) {
log.warn("JWT token expired", e);
return Response.error(ResponseCode.JWT_EXPIRED);
}
@ExceptionHandler({SignatureException.class, MalformedJwtException.class})
public Response<?> handleInvalidJwtException(Exception e) {
log.warn("Invalid JWT token", e);
return Response.error(ResponseCode.JWT_INVALID);
}
} }

View File

@ -0,0 +1,29 @@
package com.qqchen.deploy.backend.framework.exception;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
/**
* JWT认证异常
*/
public class JwtAuthenticationException extends BusinessException {
public JwtAuthenticationException(ResponseCode errorCode) {
super(errorCode);
}
public JwtAuthenticationException(ResponseCode errorCode, Object[] args) {
super(errorCode, args);
}
public static JwtAuthenticationException expired() {
return new JwtAuthenticationException(ResponseCode.JWT_EXPIRED);
}
public static JwtAuthenticationException invalid() {
return new JwtAuthenticationException(ResponseCode.JWT_INVALID);
}
public static JwtAuthenticationException missing() {
return new JwtAuthenticationException(ResponseCode.JWT_MISSING);
}
}

View File

@ -1,19 +1,32 @@
package com.qqchen.deploy.backend.framework.security.filter; package com.qqchen.deploy.backend.framework.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qqchen.deploy.backend.framework.api.Response;
import com.qqchen.deploy.backend.framework.context.TenantContext; import com.qqchen.deploy.backend.framework.context.TenantContext;
import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.exception.JwtAuthenticationException;
import com.qqchen.deploy.backend.framework.exception.SystemException;
import com.qqchen.deploy.backend.framework.security.util.JwtTokenUtil; import com.qqchen.deploy.backend.framework.security.util.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.SignatureException;
import jakarta.servlet.FilterChain; import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -43,37 +56,58 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
try { try {
// String tenantId = request.getHeader("X-Tenant-ID"); String jwt = getJwtFromRequest(request);
// if (tenantId != null && !tenantId.isEmpty()) { if (StringUtils.hasText(jwt)) {
// TenantContext.setTenantId(tenantId); String username = jwtTokenUtil.getUsernameFromToken(jwt);
// }
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authHeader.substring(7); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
String username = jwtTokenUtil.getUsernameFromToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
if (jwtTokenUtil.validateToken(token, userDetails)) { authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( SecurityContextHolder.getContext().setAuthentication(authentication);
userDetails, null, userDetails.getAuthorities()); }
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
} }
} }
chain.doFilter(request, response); chain.doFilter(request, response);
} catch (ExpiredJwtException e) {
handleAuthenticationException(response, JwtAuthenticationException.expired());
} catch (SignatureException | MalformedJwtException e) {
handleAuthenticationException(response, JwtAuthenticationException.invalid());
} catch (Exception e) { } catch (Exception e) {
log.error("JWT authentication failed", e); log.error("JWT authentication failed", e);
SecurityContextHolder.clearContext(); handleAuthenticationException(response, new SystemException("JWT authentication failed", e));
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
} finally { } finally {
TenantContext.clear(); TenantContext.clear();
} }
} }
}
private void handleAuthenticationException(HttpServletResponse response, Exception e) {
try {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Response<?> errorResponse;
if (e instanceof BusinessException be) {
errorResponse = Response.error(be.getErrorCode());
} else {
errorResponse = Response.error(ResponseCode.UNAUTHORIZED);
}
new ObjectMapper().writeValue(response.getOutputStream(), errorResponse);
} catch (IOException ex) {
log.error("Failed to write error response", ex);
}
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (!StringUtils.hasText(bearerToken) || !bearerToken.startsWith("Bearer ")) {
return null;
}
return bearerToken.substring(7);
}
}

View File

@ -18,27 +18,34 @@ import org.springframework.stereotype.Component;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
@Component
@Slf4j @Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper;
public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override @Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
if (!isErrorDispatch(request)) {
log.error("Custom authentication entry point error", authException);
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name()); response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Response<?> result;
if (authException instanceof BadCredentialsException) { Response<?> errorResponse = Response.error(ResponseCode.UNAUTHORIZED);
result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.UNAUTHORIZED.getMessageKey())); objectMapper.writeValue(response.getOutputStream(), errorResponse);
} else if (authException instanceof InternalAuthenticationServiceException) { }
result = Response.error(ResponseCode.USER_NOT_FOUND, MessageUtils.getMessage(ResponseCode.USER_NOT_FOUND.getMessageKey()));
} else if (authException instanceof InsufficientAuthenticationException) { private boolean isErrorDispatch(HttpServletRequest request) {
result = Response.error(ResponseCode.AUTH_REQUIRED, MessageUtils.getMessage(ResponseCode.AUTH_REQUIRED.getMessageKey())); String errorPath = "/error";
} else { String requestUri = request.getRequestURI();
result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.UNAUTHORIZED.getMessageKey())); return requestUri != null && requestUri.equals(errorPath);
}
log.error("Custom authentication entry point error", authException);
response.getWriter().write(objectMapper.writeValueAsString(result));
} }
} }

View File

@ -32,4 +32,9 @@ entity.not.found.name.id=找不到ID为{1}的{0}
dependency.injection.service.not.found=找不到实体 {0} 对应的服务 (尝试过的bean名称: {1}) dependency.injection.service.not.found=找不到实体 {0} 对应的服务 (尝试过的bean名称: {1})
dependency.injection.repository.not.found=找不到实体 {0} 对应的Repository: {1} dependency.injection.repository.not.found=找不到实体 {0} 对应的Repository: {1}
dependency.injection.converter.not.found=找不到实体 {0} 对应的Converter: {1} dependency.injection.converter.not.found=找不到实体 {0} 对应的Converter: {1}
dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: {1} dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: {1}
# JWT相关
jwt.token.expired=登录已过期,请重新登录
jwt.token.invalid=无效的登录凭证
jwt.token.missing=未提供登录凭证

View File

@ -32,4 +32,9 @@ entity.not.found.name.id={0} with id {1} not found
dependency.injection.service.not.found=No service found for entity {0} (tried bean name: {1}) dependency.injection.service.not.found=No service found for entity {0} (tried bean name: {1})
dependency.injection.repository.not.found=No repository found for entity {0}: {1} dependency.injection.repository.not.found=No repository found for entity {0}: {1}
dependency.injection.converter.not.found=No converter found for entity {0}: {1} dependency.injection.converter.not.found=No converter found for entity {0}: {1}
dependency.injection.entitypath.failed=Failed to initialize EntityPath for entity {0}: {1} dependency.injection.entitypath.failed=Failed to initialize EntityPath for entity {0}: {1}
# JWT
jwt.token.expired=Login expired, please login again
jwt.token.invalid=Invalid token
jwt.token.missing=No token provided