← 返回题目列表

Spring Security 的认证失败(401)和授权失败(403)怎么自定义处理?AuthenticationEntryPoint 和 AccessDeniedHandler 是什么?

高频 中等 第 11 / 23 题 更新于 2026/08/03
异常处理AuthenticationEntryPointAccessDeniedHandler401

简化版

**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)」。