Spring Security 的认证失败(401)和授权失败(403)怎么自定义处理?AuthenticationEntryPoint 和 AccessDeniedHandler 是什么?
简化版
**Spring Security 里,「认证失败」和「授权失败」是两类不同的异常,由两个不同的组件处理——前后端分离项目常需要自定义它们,返回 JSON 而不是默认的跳转登录页/错误页。**两类失败:① 认证失败(401 Unauthorized)——「你是谁都没搞清楚」(没登录、Token 无效/过期),由 AuthenticationEntryPoint 处理,默认行为是跳转到登录页(表单登录)或返回 401;② 授权失败(403 Forbidden)——「知道你是谁,但你没权限做这件事」(已登录但角色不够),由 AccessDeniedHandler 处理,默认返回 403 错误页。为什么要自定义:前后端分离项目里,后端应该返回 JSON({"code":401,"msg":"未登录"})而不是跳转 HTML 页面——前端根据 JSON 处理(跳登录页、提示无权限)。怎么区分 401 和 403:401 是「未认证/认证失败」(先登录)、403 是「已认证但无权限」(登录了也没用,权限不够)。核心记忆:AuthenticationEntryPoint 管认证失败(401,你没登录)、AccessDeniedHandler 管授权失败(403,你没权限)。
详细版
两类异常处理组件:
| 组件 | 处理什么 | HTTP 状态 | 含义 |
|---|---|---|---|
| AuthenticationEntryPoint | 认证失败/未认证 | 401 Unauthorized | 你是谁没搞清(没登录/Token 无效) |
| AccessDeniedHandler | 授权失败 | 403 Forbidden | 知道你是谁,但你没权限 |
// 配置自定义异常处理(返回 JSON)
http.exceptionHandling(ex -> ex
// ① 认证失败(401):未登录/Token 无效
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}");
})
// ② 授权失败(403):已登录但无权限
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无权限访问\"}");
})
);
401 认证失败:没登录 / Token 过期无效 → 先去登录
403 授权失败:登录了,但角色不够(如普通用户访问 /admin)→ 权限不足
⚠️ 搞清「401 vs 403」的区别是这题的核心:401 是「认证」问题(你没登录/登录失效),403 是「授权」问题(你登录了但权限不够)。这个区分很重要,因为前端的处理逻辑完全不同:收到 401,前端应该跳转到登录页(让用户重新登录);收到 403,前端应该提示「无权限」(用户已登录,跳登录页也没用——不是登录的问题,是权限的问题)。如果混用(比如权限不足也返回 401),前端会错误地把用户踢去登录页,用户重新登录后还是没权限,陷入死循环。所以认证失败用
AuthenticationEntryPoint(401)、授权失败用AccessDeniedHandler(403),各司其职。另外前后端分离项目一定要自定义这两个组件返回 JSON——默认行为(跳转登录页 HTML、返回错误页)对前端不友好,前端拿到的是一堆 HTML 而不是能解析的 JSON。
完整版教学
一、认证 vs 授权:两类失败
先分清「认证失败」和「授权失败」——这是这题的根本:
认证(Authentication,你是谁):
验证身份——你登录了吗、Token 有效吗
认证失败:没登录、Token 无效/过期、凭证错误
→ "我不知道你是谁" → 401 Unauthorized
授权(Authorization,你能不能做):
验证权限——你有权限访问这个资源/执行这个操作吗
授权失败:已登录,但角色/权限不够
→ "我知道你是谁,但你不能做这件事" → 403 Forbidden
两类失败的本质区别:
认证失败(401):身份问题——你还没证明"你是谁"
解决:去登录(证明身份)
授权失败(403):权限问题——你证明了身份,但权限不够
解决:无解(除非提升权限)——登录也没用
例子:
没登录访问 /admin → 401(认证失败,先登录)
普通用户登录后访问 /admin → 403(授权失败,你登录了但不是 admin)
所以:
401 = 认证问题(AuthenticationEntryPoint 处理)
403 = 授权问题(AccessDeniedHandler 处理)
先分清两类失败:认证(你是谁)失败 = 401(没登录/Token 无效/凭证错误,「不知道你是谁」,解决是去登录)、授权(你能不能做)失败 = 403(已登录但权限不够,「知道你是谁但不能做」,解决无解除非提权、登录也没用)。例子:没登录访问 /admin=401、普通用户登录后访问 /admin=403。所以 401 是认证问题(AuthenticationEntryPoint)、403 是授权问题(AccessDeniedHandler)。理解「认证失败 401(身份问题没登录、去登录)vs 授权失败 403(权限问题已登录但不够、登录也没用);401 认证问题 403 授权问题」,就抓住了这题的根本区分。
二、AuthenticationEntryPoint:认证失败
AuthenticationEntryPoint 处理认证失败(401):
AuthenticationEntryPoint(认证入口点):
接口方法:commence(request, response, authException)
触发时机:用户"未认证"却访问需要认证的资源时
- 没登录就访问受保护资源
- Token 无效/过期
- 认证失败
默认行为(不同登录方式不同):
表单登录:重定向到登录页
HTTP Basic:返回 401 + WWW-Authenticate 头
→ 都是"引导用户去认证"
自定义(前后端分离返回 JSON):
http.exceptionHandling(ex -> ex.authenticationEntryPoint(
(request, response, authException) -> {
response.setStatus(401);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
}
));
→ 前端收到 401 JSON,跳转登录页
为什么叫 EntryPoint(入口点):
它是"引导用户进入认证流程"的入口
未认证访问 → 触发 EntryPoint → 引导去认证(跳登录页/返回 401)
所以 AuthenticationEntryPoint = 认证失败/未认证时的处理器(401)
AuthenticationEntryPoint 处理认证失败(401)——commence 方法,触发时机是「未认证却访问需认证的资源」(没登录/Token 无效/认证失败)。默认行为:表单登录重定向登录页、HTTP Basic 返回 401。自定义(前后端分离返回 JSON):authenticationEntryPoint 设 401 + JSON。叫 EntryPoint 是因为它是「引导用户进入认证流程的入口」。理解「AuthenticationEntryPoint 处理认证失败 401(commence 方法)、触发时机未认证访问受保护资源、默认跳登录页/返回 401、自定义返回 JSON、是引导进入认证的入口」,就掌握了 AuthenticationEntryPoint。
三、AccessDeniedHandler:授权失败
AccessDeniedHandler 处理授权失败(403):
AccessDeniedHandler(拒绝访问处理器):
接口方法:handle(request, response, accessDeniedException)
触发时机:用户"已认证"但"权限不够"访问资源时
- 已登录,但角色/权限不满足(如普通用户访问 /admin)
- @PreAuthorize 权限校验不通过
默认行为:
返回 403 Forbidden 错误页
自定义(前后端分离返回 JSON):
http.exceptionHandling(ex -> ex.accessDeniedHandler(
(request, response, accessDeniedException) -> {
response.setStatus(403);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":403,\"msg\":\"无权限\"}");
}
));
→ 前端收到 403 JSON,提示"无权限"(不跳登录页)
关键:403 是"已登录但权限不够"
→ 前端提示"无权限",别跳登录页(登录也没用)
所以 AccessDeniedHandler = 授权失败时的处理器(403)
AccessDeniedHandler 处理授权失败(403)——handle 方法,触发时机是「已认证但权限不够」(已登录但角色不满足、@PreAuthorize 校验不通过)。默认返回 403 错误页。自定义(前后端分离返回 JSON):accessDeniedHandler 设 403 + JSON。关键:403 是已登录但权限不够,前端提示「无权限」别跳登录页(登录也没用)。理解「AccessDeniedHandler 处理授权失败 403(handle 方法)、触发时机已认证但权限不够(@PreAuthorize 不通过)、默认返回 403 错误页、自定义返回 JSON、前端提示无权限别跳登录页」,就掌握了 AccessDeniedHandler。
四、为什么区分很重要
深入理解「为什么必须区分 401 和 403」:
前端处理逻辑完全不同:
收到 401(认证失败):
→ 用户没登录/登录失效
→ 前端跳转登录页,让用户登录
收到 403(授权失败):
→ 用户登录了,但权限不够
→ 前端提示"无权限访问",不跳登录页
如果混用(都返回 401):
权限不足也返回 401 → 前端把用户踢去登录页
→ 用户重新登录(登录成功了)→ 还是访问那个没权限的资源
→ 还是权限不足 → 又被踢去登录页
→ 死循环!(用户困惑:"我明明登录了啊")
如果混用(都返回 403):
未登录也返回 403 → 前端提示"无权限"
→ 但用户其实是"没登录",应该去登录,不是"无权限"
→ 误导用户
所以必须准确区分:
401 = 认证问题 = 去登录
403 = 授权问题 = 权限不足(登录也没用)
→ 前端才能正确引导用户
HTTP 语义(标准):
401 Unauthorized:其实是"未认证"(命名有点误导,应该叫 Unauthenticated)
403 Forbidden:已认证但被禁止(无权限)
为什么必须区分:前端处理逻辑完全不同——收到 401(认证失败)跳登录页、收到 403(授权失败)提示无权限不跳登录页。如果混用:都返回 401 则权限不足也踢去登录页、用户重新登录还是没权限、死循环;都返回 403 则未登录也提示无权限、误导用户(其实该去登录)。所以必须准确区分「401=认证问题=去登录、403=授权问题=权限不足登录也没用」。HTTP 语义:401 其实是「未认证」(命名误导)、403 是「已认证但被禁止」。理解「必须区分因前端处理不同:401 跳登录页、403 提示无权限;混用都 401 会死循环(权限不足踢去登录重登还没权限)、都 403 会误导;401 认证问题去登录、403 授权问题登录也没用」,就理解了区分的重要性。
五、前后端分离的场景
前后端分离项目为什么一定要自定义:
前后端分离的问题(默认行为不友好):
默认 AuthenticationEntryPoint:跳转登录页(返回登录页 HTML)
默认 AccessDeniedHandler:返回 403 错误页 HTML
→ 前端(Vue/React)调 API,期望的是 JSON
→ 却收到一堆 HTML(登录页/错误页)→ 前端没法解析、处理
自定义返回 JSON:
认证失败 → {"code":401,"msg":"未登录"} → 前端跳登录页
授权失败 → {"code":403,"msg":"无权限"} → 前端提示无权限
→ 前端拿到能解析的 JSON,正确处理
统一响应格式:
和项目的统一响应结构一致(如 {code, msg, data})
→ 前端统一处理错误码
配置(Spring Security 6):
http.exceptionHandling(ex -> ex
.authenticationEntryPoint(myAuthEntryPoint) // 401 JSON
.accessDeniedHandler(myAccessDeniedHandler) // 403 JSON
);
注意跨域(CORS):
前后端分离常跨域,异常响应也要带 CORS 头
否则前端拿不到响应(被 CORS 拦)
所以前后端分离一定要自定义这两个组件返回 JSON
前后端分离一定要自定义,因为默认行为不友好:默认 AuthenticationEntryPoint 跳登录页 HTML、AccessDeniedHandler 返回 403 HTML,而前端调 API 期望 JSON、收到 HTML 没法解析。自定义返回 JSON:认证失败返回 {"code":401} 前端跳登录页、授权失败返回 {"code":403} 前端提示无权限,且统一响应格式与项目一致。注意跨域(CORS)——异常响应也要带 CORS 头否则前端拿不到。理解「前后端分离默认行为不友好(返回 HTML 前端要 JSON)、必须自定义返回 JSON(401/403 各自)、统一响应格式、注意 CORS 头」,就理解了前后端分离的场景。
六、实践与相关组件
总结异常处理的实践和相关组件:
实践建议:
① 前后端分离必须自定义 AuthenticationEntryPoint + AccessDeniedHandler
→ 返回 JSON(401 未登录、403 无权限)
② 准确区分 401 和 403(认证 vs 授权)
③ 响应格式和项目统一({code, msg})
④ 注意 CORS(异常响应带跨域头)
⑤ 记录安全日志(认证/授权失败可能是攻击)
相关的异常处理层次:
① Spring Security 过滤器链里的异常:
由 ExceptionTranslationFilter 捕获,分派给
AuthenticationEntryPoint(认证异常)/ AccessDeniedHandler(授权异常)
② 业务代码里的异常:
由 @ControllerAdvice / @ExceptionHandler 全局异常处理
→ 两个层次:安全过滤器层(EntryPoint/DeniedHandler)
和业务层(ControllerAdvice),别混淆
注意:@PreAuthorize 抛的 AccessDeniedException
如果在 Controller 方法上,可能被 ControllerAdvice 捕获(要协调)
其他相关组件:
AuthenticationFailureHandler:登录时认证失败的处理(登录接口专用)
AuthenticationSuccessHandler:登录成功的处理
LogoutSuccessHandler:登出成功的处理
→ 这些是"登录/登出流程"的处理器,和 EntryPoint/DeniedHandler 不同
核心总结:
AuthenticationEntryPoint:认证失败/未认证(401)
AccessDeniedHandler:授权失败(403)
前后端分离都自定义返回 JSON,准确区分 401/403
异常处理实践:① 前后端分离必须自定义两个组件返回 JSON、② 准确区分 401/403、③ 响应格式统一、④ 注意 CORS、⑤ 记录安全日志。异常处理层次:安全过滤器层(ExceptionTranslationFilter 捕获分派给 EntryPoint/DeniedHandler)和业务层(@ControllerAdvice),别混淆(@PreAuthorize 抛的 AccessDeniedException 在 Controller 方法上可能被 ControllerAdvice 捕获,要协调)。相关组件:AuthenticationFailureHandler(登录失败)、AuthenticationSuccessHandler(登录成功)、LogoutSuccessHandler(登出成功)是登录/登出流程的处理器。理解「实践:前后端分离自定义返回 JSON/区分 401 403/格式统一/CORS/日志;层次:安全过滤器层(EntryPoint/DeniedHandler)vs 业务层(ControllerAdvice);相关组件 FailureHandler/SuccessHandler(登录流程)」,就掌握了异常处理的实践和相关组件。
记忆钩子:「Spring Security 两类失败:①认证失败(401 Unauthorized,你是谁没搞清-没登录/Token 无效)→AuthenticationEntryPoint 处理(commence,默认跳登录页/返回 401,自定义返回 JSON)②授权失败(403 Forbidden,知道你是谁但没权限-已登录角色不够/@PreAuthorize 不通过)→AccessDeniedHandler 处理(handle,默认 403 错误页,自定义返回 JSON);★区分很重要:401 前端跳登录页、403 前端提示无权限别跳登录(混用都 401 会死循环、都 403 误导);前后端分离必须自定义返回 JSON(默认返回 HTML 前端要 JSON);异常处理层次:安全过滤器层 EntryPoint/DeniedHandler vs 业务层 ControllerAdvice」。
七、常见误区与追问
- 误区:认证失败和授权失败都返回 401。 应该区分——认证失败(未登录/Token 无效)返回 401(AuthenticationEntryPoint),授权失败(已登录但权限不够)返回 403(AccessDeniedHandler);如果都返回 401,权限不足的用户会被踢去登录页、重新登录后还是没权限、陷入死循环。
- 误区:403 也应该让用户去登录。 不——403 是「已登录但权限不够」,用户已经登录了,跳登录页重新登录也没用(不是登录的问题,是权限的问题);前端收到 403 应该提示「无权限访问」,不是跳登录页;跳登录页是 401(未认证)的处理。
- 误区:默认的异常处理适合前后端分离。 不适合——默认 AuthenticationEntryPoint 跳转登录页 HTML、AccessDeniedHandler 返回 403 错误页 HTML;前后端分离项目前端调 API 期望 JSON,拿到 HTML 没法解析处理;必须自定义这两个组件返回 JSON(401/403 各自的 JSON 响应)。
- 误区:AuthenticationEntryPoint 和 AccessDeniedHandler 处理一样的异常。 不同——AuthenticationEntryPoint 处理「认证异常」(AuthenticationException,未认证/认证失败,401);AccessDeniedHandler 处理「授权异常」(AccessDeniedException,已认证但权限不够,403);由 ExceptionTranslationFilter 根据异常类型分派给对应的处理器。
- 追问:401 和 403 有什么区别? 401 Unauthorized(其实是「未认证」):认证失败——用户没登录、Token 无效/过期,「我不知道你是谁」,解决办法是去登录;403 Forbidden:授权失败——用户已认证(登录了)但权限不够(如普通用户访问管理员接口),「我知道你是谁,但你没权限做这件事」,登录也没用(除非提升权限);前端处理不同:401 跳登录页、403 提示无权限。
- 追问:前后端分离项目为什么要自定义 AuthenticationEntryPoint 和 AccessDeniedHandler? 因为默认行为对前端不友好——默认认证失败会重定向到登录页(返回登录页 HTML)、授权失败返回 403 错误页 HTML;而前端(Vue/React)调 API 期望的是能解析的 JSON,收到 HTML 没法处理;所以要自定义这两个组件,认证失败返回 {“code”:401,“msg”:“未登录”}、授权失败返回 {“code”:403,“msg”:“无权限”} 的 JSON,前端才能根据状态码正确处理(跳登录页 or 提示无权限)。
- 追问:@PreAuthorize 校验不通过抛的异常谁来处理? @PreAuthorize 校验不通过抛 AccessDeniedException(授权异常);如果在 Security 过滤器链层面,由 ExceptionTranslationFilter 捕获、交给 AccessDeniedHandler 处理(返回 403);但如果 @PreAuthorize 在 Controller 方法上、异常在方法执行时抛出,可能被 @ControllerAdvice 的全局异常处理捕获;要注意协调这两个层次(安全过滤器层和业务层),避免处理逻辑冲突或重复。
八、加强记忆
Spring Security 里「认证失败」和「授权失败」是两类不同的异常,由两个组件处理:① AuthenticationEntryPoint 处理认证失败(401 Unauthorized)——「你是谁没搞清楚」(没登录、Token 无效/过期),commence 方法,默认跳登录页/返回 401;② AccessDeniedHandler 处理授权失败(403 Forbidden)——「知道你是谁,但你没权限」(已登录但角色不够、@PreAuthorize 不通过),handle 方法,默认返回 403 错误页。区分 401 和 403 很重要:401 是认证问题(去登录)、403 是授权问题(已登录但权限不够、登录也没用)——前端处理不同:收到 401 跳登录页、收到 403 提示无权限(别跳登录页);混用会出问题(都 401 则权限不足踢去登录、重登还没权限、死循环;都 403 则未登录也提示无权限、误导)。前后端分离项目一定要自定义这两个组件返回 JSON(默认返回 HTML 前端没法解析),且注意 CORS(异常响应带跨域头)。异常处理分安全过滤器层(ExceptionTranslationFilter 分派给 EntryPoint/DeniedHandler)和业务层(@ControllerAdvice)。一句话「AuthenticationEntryPoint 处理认证失败(401,你没登录,跳登录页)、AccessDeniedHandler 处理授权失败(403,已登录但没权限,提示无权限);区分很重要-401 去登录 403 权限不足登录也没用(混用都 401 会死循环);前后端分离必须自定义返回 JSON(默认返回 HTML 前端要 JSON)」。