From 527b7563c0f29caa358c3b2509a6ce6224cb5bc7 Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 29 Nov 2024 18:39:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=99=BB=E5=BD=95=E7=9A=84?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=EF=BC=8C=E4=BF=AE=E5=A4=8D=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E8=B0=83=E7=94=A8error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/framework/enums/ResponseCode.java | 7 +- .../exception/GlobalExceptionHandler.java | 16 ++++ .../exception/JwtAuthenticationException.java | 29 +++++++ .../filter/JwtAuthenticationFilter.java | 80 +++++++++++++------ .../CustomAuthenticationEntryPoint.java | 37 +++++---- .../src/main/resources/messages.properties | 7 +- .../src/main/resources/messages_en.properties | 7 +- 7 files changed, 142 insertions(+), 41 deletions(-) create mode 100644 backend/src/main/java/com/qqchen/deploy/backend/framework/exception/JwtAuthenticationException.java diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java index bb1f1f25..7a8bc5eb 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/enums/ResponseCode.java @@ -32,7 +32,12 @@ public enum ResponseCode { DEPENDENCY_INJECTION_SERVICE_NOT_FOUND(1100, "dependency.injection.service.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_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; diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java index 4683a418..64943cdd 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/GlobalExceptionHandler.java @@ -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.enums.ResponseCode; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; 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.RestControllerAdvice; +import java.security.SignatureException; + @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { @@ -37,4 +41,16 @@ public class GlobalExceptionHandler { log.error("Unexpected error occurred", e); 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); + } } \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/JwtAuthenticationException.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/JwtAuthenticationException.java new file mode 100644 index 00000000..6dbacd56 --- /dev/null +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/exception/JwtAuthenticationException.java @@ -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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java index d479943d..690ee162 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/filter/JwtAuthenticationFilter.java @@ -1,19 +1,32 @@ 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.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 io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.security.SignatureException; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; @@ -43,37 +56,58 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { try { -// String tenantId = request.getHeader("X-Tenant-ID"); -// if (tenantId != null && !tenantId.isEmpty()) { -// TenantContext.setTenantId(tenantId); -// } - String authHeader = request.getHeader("Authorization"); - if (authHeader == null || !authHeader.startsWith("Bearer ")) { - chain.doFilter(request, response); - return; - } + String jwt = getJwtFromRequest(request); + if (StringUtils.hasText(jwt)) { + String username = jwtTokenUtil.getUsernameFromToken(jwt); - String token = authHeader.substring(7); - String username = jwtTokenUtil.getUsernameFromToken(token); + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); - if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - - if (jwtTokenUtil.validateToken(token, userDetails)) { - UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( - userDetails, null, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - SecurityContextHolder.getContext().setAuthentication(authentication); + if (jwtTokenUtil.validateToken(jwt, userDetails)) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } } } - chain.doFilter(request, response); + } catch (ExpiredJwtException e) { + handleAuthenticationException(response, JwtAuthenticationException.expired()); + } catch (SignatureException | MalformedJwtException e) { + handleAuthenticationException(response, JwtAuthenticationException.invalid()); } catch (Exception e) { log.error("JWT authentication failed", e); - SecurityContextHolder.clearContext(); - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + handleAuthenticationException(response, new SystemException("JWT authentication failed", e)); } finally { TenantContext.clear(); } } -} \ No newline at end of file + + 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); + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/handler/CustomAuthenticationEntryPoint.java b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/handler/CustomAuthenticationEntryPoint.java index 9c162f85..82e48e11 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/framework/security/handler/CustomAuthenticationEntryPoint.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/framework/security/handler/CustomAuthenticationEntryPoint.java @@ -18,27 +18,34 @@ import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.charset.StandardCharsets; -@Component @Slf4j +@Component public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; + + public CustomAuthenticationEntryPoint(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } @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.setCharacterEncoding(StandardCharsets.UTF_8.name()); - Response result; - if (authException instanceof BadCredentialsException) { - result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.UNAUTHORIZED.getMessageKey())); - } else if (authException instanceof InternalAuthenticationServiceException) { - result = Response.error(ResponseCode.USER_NOT_FOUND, MessageUtils.getMessage(ResponseCode.USER_NOT_FOUND.getMessageKey())); - } else if (authException instanceof InsufficientAuthenticationException) { - result = Response.error(ResponseCode.AUTH_REQUIRED, MessageUtils.getMessage(ResponseCode.AUTH_REQUIRED.getMessageKey())); - } else { - result = Response.error(ResponseCode.UNAUTHORIZED, MessageUtils.getMessage(ResponseCode.UNAUTHORIZED.getMessageKey())); - } - log.error("Custom authentication entry point error", authException); - response.getWriter().write(objectMapper.writeValueAsString(result)); + + Response errorResponse = Response.error(ResponseCode.UNAUTHORIZED); + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } + + private boolean isErrorDispatch(HttpServletRequest request) { + String errorPath = "/error"; + String requestUri = request.getRequestURI(); + return requestUri != null && requestUri.equals(errorPath); } } \ No newline at end of file diff --git a/backend/src/main/resources/messages.properties b/backend/src/main/resources/messages.properties index 259f7409..80956c58 100644 --- a/backend/src/main/resources/messages.properties +++ b/backend/src/main/resources/messages.properties @@ -32,4 +32,9 @@ entity.not.found.name.id=找不到ID为{1}的{0} dependency.injection.service.not.found=找不到实体 {0} 对应的服务 (尝试过的bean名称: {1}) dependency.injection.repository.not.found=找不到实体 {0} 对应的Repository: {1} dependency.injection.converter.not.found=找不到实体 {0} 对应的Converter: {1} -dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: {1} \ No newline at end of file +dependency.injection.entitypath.failed=初始化实体 {0} 的EntityPath失败: {1} + +# JWT相关 +jwt.token.expired=登录已过期,请重新登录 +jwt.token.invalid=无效的登录凭证 +jwt.token.missing=未提供登录凭证 \ No newline at end of file diff --git a/backend/src/main/resources/messages_en.properties b/backend/src/main/resources/messages_en.properties index 73a1599b..9a89855a 100644 --- a/backend/src/main/resources/messages_en.properties +++ b/backend/src/main/resources/messages_en.properties @@ -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.repository.not.found=No repository 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} \ No newline at end of file +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 \ No newline at end of file