{"record":{"id":"e64a2c594ee046df","repo":"xkcoding/spring-boot-demo","slug":"401","errorCode":"401","errorMessage":"请先登录！","messagePattern":"请先登录！","errorType":"exception","errorClass":"SecurityException","httpStatus":null,"severity":"warning","filePath":"demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java","lineNumber":61,"sourceCode":"     * 登录\n     */\n    @PostMapping(\"/login\")\n    public ApiResponse login(@Valid @RequestBody LoginRequest loginRequest) {\n        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsernameOrEmailOrPhone(), loginRequest.getPassword()));\n\n        SecurityContextHolder.getContext().setAuthentication(authentication);\n\n        String jwt = jwtUtil.createJWT(authentication, loginRequest.getRememberMe());\n        return ApiResponse.ofSuccess(new JwtResponse(jwt));\n    }\n\n    @PostMapping(\"/logout\")\n    public ApiResponse logout(HttpServletRequest request) {\n        try {\n            // 设置JWT过期\n            jwtUtil.invalidateJWT(request);\n        } catch (SecurityException e) {\n            throw new SecurityException(Status.UNAUTHORIZED);\n        }\n        return ApiResponse.ofStatus(Status.LOGOUT);\n    }\n}\n","sourceCodeStart":43,"sourceCodeEnd":66,"githubUrl":"https://github.com/xkcoding/spring-boot-demo/blob/87a142f9604c1a5365b4d24d22c2c11c26a9d5ab/demo-rbac-security/src/main/java/com/xkcoding/rbac/security/controller/AuthController.java#L43-L66","documentation":"Thrown by the logout endpoint when jwtUtil.invalidateJWT(request) raises a SecurityException internally. invalidateJWT calls parseJWT, which throws SecurityException for an expired, malformed, or signature-invalid token. The outer catch re-wraps it as Status.UNAUTHORIZED (401, 'please login first'), collapsing all token errors into a single 'not logged in' message. This is a deliberate obfuscation to avoid leaking token-validation details.","triggerScenarios":"POST /api/auth/logout with an expired, invalid, or missing JWT in the Authorization header. parseJWT fails (TOKEN_EXPIRED, TOKEN_OUT_OF_CTRL, or TOKEN_PARSE_ERROR), the inner SecurityException propagates to the catch, and the endpoint re-throws UNAUTHORIZED.","commonSituations":"Session already expired before logout is clicked; token was invalidated from another device (TOKEN_OUT_OF_CTRL); client sends a malformed or tampered token; token signature mismatch due to jwtConfig.key change.","solutions":["Handle the 401 response on the client by redirecting to the login page — the logout is moot if the token is already invalid.","If the token is already expired, the user is effectively logged out server-side (Redis key expired), so a client-side token clear suffices.","Consider making logout idempotent: catch SecurityException and return Status.LOGOUT success regardless, since the goal (invalidating the session) is already achieved.","Verify jwtConfig.key has not changed between token issuance and logout."],"exampleFix":"// before — re-throws as UNAUTHORIZED on any token error\n} catch (SecurityException e) {\n    throw new SecurityException(Status.UNAUTHORIZED);\n}\n\n// after — logout is idempotent; succeed even if token is already invalid\n} catch (SecurityException e) {\n    log.warn(\"Logout called with invalid token, treating as already logged out\");\n}\nreturn ApiResponse.ofStatus(Status.LOGOUT);","handlingStrategy":"try-catch","validationCode":"// Client-side: check token expiry before calling logout\n// Decode the JWT exp claim and compare to current time\nlong exp = decodeJwtExp(jwt); // client-side JWT decode\nif (exp < System.currentTimeMillis() / 1000) {\n    // Token already expired — just clear client-side state, no need to call logout\n    clearTokenAndRedirectToLogin();\n    return;\n}","typeGuard":null,"tryCatchPattern":"// Client-side: handle the 401 from logout\ntry {\n    apiClient.logout();\n} catch (HttpClientErrorException e) {\n    if (e.getStatusCode() == HttpStatus.UNAUTHORIZED) {\n        // Token is invalid/expired — user is effectively logged out already\n        clearTokenAndRedirectToLogin();\n    }\n}","preventionTips":["Make logout idempotent: treat 401 from logout as success (user is already logged out).","On the client, clear local token state regardless of the logout API response.","Do not change jwtConfig.key after deployment — existing tokens will all fail validation."],"tags":["spring-security","jwt","logout","authentication","http-401","token"],"backgroundTag":null,"analyzedSha":"87a142f9604c1a5365b4d24d22c2c11c26a9d5ab","analyzedAt":"2026-08-14T01:16:58.217Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}