news 2026/10/15 3:11:58

Spring Security AccessDeniedException排查:从异常到权限配置的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Security AccessDeniedException排查:从异常到权限配置的完整指南

1. 先搞明白:AccessDeniedException到底是谁在什么时候抛出来的

1.1 那个“不允许访问”的提示,到底是从哪来的

看到org.springframework.security.access.AccessDeniedException这个异常,第一反应应该是:你的项目已经成功进入了 Spring Security 的授权链路,而不是被网络层、网关或者前端路由给拦下来的。这其实是好事,至少说明问题有明确的方向可以查,怕的是那种“啥日志都没有,页面就白屏了”的情况。

先说说“不允许访问”这几个字从哪来。做过前后端分离项目的人应该都见过,Spring Security 默认的英文提示是Access Denied,也就是访问被拒绝。如果你在页面上看到的是中文“不允许访问”,那大概率是项目自己做了两件事中的一件:要么是全局异常处理器(比如@RestControllerAdvice)把异常 catch 住之后,对外输出了一段统一的中文提示;要么是前端在拿到 HTTP 403 状态码后,统一弹了一个“不允许访问”的 toast。所以我建议你拿到这个问题后,第一件事先去确认“不允许访问”是后端返回的 JSON,还是前端自己渲染的文案。这个细节直接决定你该去查接口还是查前端路由,能省不少时间。

我个人的习惯是,出现这类异常先翻后端日志,看有没有完整的异常栈。如果日志里有一行org.springframework.security.access.AccessDeniedException: Access is denied,那么后端一定抛了异常,接下来只需要顺着这条线往下排查。如果后端日志干净得很,那就要去前端找原因了,比如路由守卫里写了角色判断、菜单权限没配置、按钮权限控制导致某个跳转被拦截等等。这两种情况的排查路径完全不同,却经常被混为一谈。

1.2 从请求进来到异常抛出,中间发生了什么

要理解AccessDeniedException,可以先把它放回 Spring Security 的完整流程里看。一个 HTTP 请求打进来,会先经过一堆过滤器(Filter),这些过滤器在 Spring Security 里被组织成一条过滤器链。其中我们最关心的三个角色是:认证过滤器(负责解析 Token、Session、Cookie,把当前用户身份放到 SecurityContext 里)、授权过滤器(负责判断当前用户是否允许访问这个 URL)和ExceptionTranslationFilter(负责捕获授权过程中抛出的异常,再做转发处理)。

授权过滤器的判断逻辑,本质上就是拿“当前用户的权限集合”和“这个资源要求的权限”做比对。比如PreAuthorizationFilter会调用AuthorizationManager,里面有一个核心的check()方法,返回一个AuthorizationDecision,也就是“通过”还是“拒绝”。如果决策结果是拒绝,授权过滤器就会直接抛出AccessDeniedException。这个过程就像拿着工牌去刷门禁,系统先确认你是不是公司员工(认证),再确认你有没有进这间办公室的权限(授权)。两关都过了才放行,第二关不过就会报访问被拒绝。

这里有个容易被忽略的点:ExceptionTranslationFilter在捕获到AccessDeniedException之后,会先看一眼当前用户是不是已经登录了。如果确实没有登录,或者是一个匿名用户,它就会调用AuthenticationEntryPoint,一般是返回 401 状态码,或者重定向到登录页。只有当用户已经登录,但权限不够时,才会真正走到AccessDeniedHandler,返回 403 状态码或者一个自定义的拒绝页面。所以很多场景下你看到的 403“不允许访问”,其实就是这个过滤器链里的最后一棒,问题往往不在这个异常本身,而在上一层的用户权限列表或者资源配置。

1.3 版本差异:为什么报错包名可能是org.springframework.security.access但新项目用的是另一个包

关于这个异常,有个特别容易让项目升级时踩坑的点:不同版本的 Spring Security,AccessDeniedException的包路径是不一样的。

Spring Security 5.x 时代,异常类在org.springframework.security.access.AccessDeniedException下面,这也是很多老项目里 import 语句的写法。从 Spring Security 6.0 开始,这个异常类被重构到了org.springframework.security.authorization.AccessDeniedException,同时新的包路径下还有一个父类AuthorizationDeniedException。如果你把一个 Spring Security 5 的项目升级到 6,最常见的第一道坎就是编译报错:找不到org.springframework.security.access.AccessDeniedException。

我见过不少团队升级时图省事,直接全局搜索替换包名,结果把原本用于自定义异常处理、或者在某些业务代码里 catch 这个异常的地方也一起改了,后续排查问题时发现日志里抛的还是旧包的类,而代码 catch 的是新包的类,导致异常没有被正确处理,最终又漏回页面上变成“不允许访问”。我的建议是升级之前先整理一下项目中所有直接使用AccessDeniedException的代码位置,分清哪些是 Spring Security 内部自动抛出的,哪些是你自己 catch 后做业务处理的,再决定怎么迁移。如果你现在还报的是org.springframework.security.access这个包名,基本可以断定项目用的还是 Spring Security 5.x 的环境,排查时就不要按 6.x 的源码去看,避免越绕越远。

2. 新手最容易踩的三个配置坑:匹配顺序、角色前缀、注解开关

2.1 过滤器链的匹配顺序,比你想的更“先到先得”

在 Spring Security 的 Java Config 配置里,最常见的写法是HttpSecurity上挂一串规则,比如:

http.authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() );

这里有个初学者容易忽略的规则:过滤器链是从上往下匹配的,先匹配到哪条规则就用哪条,后面的规则不会再看。也就是说,如果你把anyRequest().authenticated()写在了最上面,那么下面所有.permitAll()和.hasRole()全都白写,因为每个请求到第一行就已经被anyRequest()接住了。这个点看着简单,但实际项目里经常有人把permitAll写在最后,导致本应放行的静态资源(比如/assets/**、/favicon.ico)全部被拦,页面上就会出现一堆访问被拒绝的报错。

我自己一般会强制自己遵守一个顺序:先放行完全公开的资源,再放行需要特定角色的接口,最后才是兜底的anyRequest()。这就像写路由时要求“具体规则放前面,兜底规则放最后”一样,逻辑清晰不说,也方便后面的人维护。如果哪天你发现某个接口明明配置了permitAll却还是 403,先去检查是不是匹配顺序的问题,八成能在前几行规则里找到原因。

2.2 角色名的前缀坑:hasRole和hasAuthority是两回事

第二个高发问题出在角色名的前缀上。Spring Security 里有两个特别像的方法:hasRole("ADMIN")和hasAuthority("ROLE_ADMIN")。实际上,hasRole("ADMIN")内部会帮你自动拼上ROLE_前缀,也就是它等价于hasAuthority("ROLE_ADMIN")。

问题就来了:如果你的用户权限列表里,存放的角色名是ADMIN,而不是ROLE_ADMIN,那么你用hasRole("ADMIN")是能通过的,因为框架会拼出ROLE_ADMIN去比对。但如果你的数据库里存的是admin(小写),或者存的是ROLE_ADMIN,而配置里用了hasAuthority("ADMIN"),就永远比对不上,因为hasAuthority是原封不动地拿字符串去比。最终表现就是:用户明明有管理员角色,访问该角色专属接口的时候照样抛AccessDeniedException。

这种问题非常迷惑,因为看日志、看数据库都觉得对,就差一个字符的差。排查的时候最快的办法是在代码里打印一下当前用户的Authentication.getAuthorities(),看一眼里面的值到底长什么样,再比对配置里的写法。我做过一个统计,在各种“不允许访问”的案例里,因为角色前缀或大小写不一致引起的问题,占了差不多三成。这不是小事。

2.3 方法级安全注解没开启:@PreAuthorize直接当摆设

还有一类场景和 URL 级授权无关,而是发生在 Service 层或者 Controller 层的@PreAuthorize注解上。这个注解本身不会自动生效,你必须在配置类里显式开启方法级安全。Spring Security 5.x 用的是@EnableGlobalMethodSecurity(prePostEnabled = true),Spring Security 6.x 则换成了@EnableMethodSecurity。

如果你的项目没加这个注解,那么你的 Service 方法上写的@PreAuthorize("hasRole('ADMIN')")就完全没有作用。注意,这里出问题的方式有两种:一种是“该拦的没拦”,比如普通用户也能访问管理员接口;另一种则是“不该拦的拦了”,比如说你开了方法级安全,但 SecurityContext 里拿不到正确的权限信息,所有带有@PreAuthorize的方法全部报访问被拒绝。后面这种情况在微服务环境下尤其常见,因为有些服务做了登录状态的互相信任,但并没有把用户角色信息完整传递过去。

建议所有用方法级安全注解的团队,在项目启动的时候先自查一下有没有加@EnableMethodSecurity,并且在测试环境写一个最简单的@PreAuthorize("hasRole('TEMP')")接口,用不同角色的账号各调一次,确认注解真的生效了再继续搞复杂逻辑。这一步能帮你把“配置问题”和“业务问题”快速分开。

2.4 未登录用户也可能报AccessDeniedException,别被表面现象骗了

还有一个让人头疼的情况:明明没有登录,按理说应该先跳登录页,结果控制台报的也是AccessDeniedException。这是因为 Spring Security 默认的授权过滤器在判断权限不通过时,会先看当前用户有没有完整认证。如果项目里用了匿名过滤器,那么每个未登录用户在安全框架里也会有一个“匿名用户”的身份,它的权限列表里通常只有一个ROLE_ANONYMOUS。

当这个匿名用户请求一个需要ROLE_USER的接口时,授权过滤器推断出权限不足,照样抛出访问被拒绝的异常。这个时候ExceptionTranslationFilter会做一个判断:如果当前用户是匿名用户,就转入AuthenticationEntryPoint,让你去登录;但如果你的代码在某个try-catch里自己吞掉了这个异常,或者拦截器配置有问题,就可能把 401 和 403 混在一起,最后展示成“不允许访问”。

所以,看到AccessDeniedException一定要先分清当前用户是“已登录无权限”还是“未登录”。最简单的分辨方法是看SecurityContextHolder.getContext().getAuthentication()是不是AnonymousAuthenticationToken。如果是,说明你面对的是未登录访问受保护资源的问题,重点应该去查登录拦截和路由配置,而不是去查角色权限。

3. 一个真实案例:从“不允许访问”到放行的完整排查

3.1 场景还原:一个订单后台管理系统的 403 问题

去年下半年,我一个朋友负责的订单后台管理系统出了这么一个问题:前端页面访问/api/orders/list接口时,页面直接弹“不允许访问”,但系统里同一个管理员账号在另一台机器上却是正常的。开发环境、测试环境都试了,有时好有时坏,非常有迷惑性。我把当时的排查思路完整记录了下来,可以作为这类问题的一个参考范例。

项目基本情况是:Spring Boot 2.7 + Spring Security 5.8,使用 JWT Token 做认证,用户服务会把用户的角色列表放在 Redis 里,订单系统从 Redis 拉取权限后,在本地生成UsernamePasswordAuthenticationToken并放进SecurityContext。接口层没有写@PreAuthorize,只是在SecurityConfig里配置了requestMatchers和anyRequest().authenticated()。也就是说,理论上只要用户登录了就能访问,并没有细到角色级别。照理说管理员登录后应该有权限,为什么还是出现“不允许访问”?

3.2 第一步:先看异常栈,而不是急着改代码

我做的第一个动作是让他把后端控制的完整异常栈发过来。日志里确实是org.springframework.security.access.AccessDeniedException: Access is denied,紧接着是ExceptionTranslationFilter的调用链。这里面有一个关键信息:异常是在过滤器层面抛出来的,不是业务代码抛出来的。这意味着我们需要查的是 Spring Security 过滤器链里的环节,而不是某个 Service 或 Mapper。

由于是“有时好有时坏”,我第一时间排除了静态配置写错的可能性,因为如果是配置写错了,那应该是所有请求、所有用户都 403,不会出现随机性。接下来重点怀疑两个地方:一是用户信息和权限的加载过程不稳定,二是多台机器之间 Redis 数据不一致,或者 Token 解析出来的权限列表有差异。

3.3 第二步:打印当前用户的Authentication对象

定位异常类型之后,我们加了一段临时调试代码,在ExceptionTranslationFilter之前的位置记录当前请求对应的用户信息:

Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null) { logger.info("当前用户: {}, 权限列表: {}", authentication.getName(), authentication.getAuthorities()); } else { logger.info("当前用户: null"); }

加了这个日志后,很快就发现了问题:报 403 的请求里,SecurityContext中保存的用户信息是空的,authentication直接就是null。而正常的请求里,用户信息和权限列表都在。这就说明问题不在授权配置,而在认证阶段——某些请求进来时,Token 没有被成功解析,导致后面的授权过滤器“不认识”你,把你当成匿名用户处理了。

顺着这个线索再查,发现这段系统里用了一个比较老的 JWT 解析工具,它对 Token 的过期时间处理有一个隐藏 bug:当请求进来的时刻恰好在 Token 过期时间前后时,解析会随机失败。最终修复方式是换成统一的 JWT 解析组件,加了过期时间的容错逻辑,并且在后端增加了一个“Token 解析失败则返回 401 而不是 403”的兜底处理。处理完之后,这个“有时好有时坏”的 403 就再也没复现过。

3.4 第三步:回到配置,检查拦截规则是否把所有接口都兜住了

在这个案例里还有一个衍生问题:为什么 Token 解析失败时抛出的是AccessDeniedException,而不是认证异常?因为ExceptionTranslationFilter在处理时发现SecurityContext里没有认证信息,走的是匿名用户分支,而授权过滤器对匿名的requestMatchers配置是anyRequest().authenticated(),于是直接抛访问拒绝。最后我们调整了认证过滤器,让它一旦发现 Token 解析失败,就立刻抛出一个认证异常,这样 401 和 403 才能正确区分,前端也能根据状态码做对应的跳转提示,而不是永远收到一个模糊的“不允许访问”。

3.5 修复后的配置代码

为了把这类问题尽早暴露,我在项目里加了这样一段配置:

http.exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"message\":\"登录已失效,请重新登录\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(403); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"没有权限访问该资源\"}"); }) );

这两段代码表面上是给前端返回更友好的提示,实际上更重要的是让我们能区分异常来源:如果页面提示“登录已失效”,说明是认证阶段出了问题;如果提示“没有权限访问该资源”,说明是授权阶段出了问题。排查范围一下子缩小了一半。这个思考路径,比单纯盯着异常本身转圈要高效得多。

4. 不要把异常直接抛给用户:自定义 AccessDeniedHandler 的细节

4.1@RestControllerAdvice为什么抓不到它

很多团队在前后端分离项目里会写一个全局异常处理器,捕获各种业务异常,然后统一返回 JSON。但第一次接触 Spring Security 的人通常会踩一个坑:明明全局异常处理器里写了@ExceptionHandler(AccessDeniedException.class),却始终不生效,页面还是照样显示“不允许访问”。原因在于过滤器中抛出的异常发生在请求进入DispatcherServlet之前,@RestControllerAdvice只处理进入了 Spring MVC 分发流程后抛出的异常,它根本看不到过滤器里发生了什么。你写一万个@ExceptionHandler都拦截不到。

正确的做法是在HttpSecurity的exceptionHandling里指定AccessDeniedHandler和AuthenticationEntryPoint。这两个 Web Security 层面的处理器才是真正“接住”异常的地方。很多教程会提到自定义AccessDeniedHandler,但很少有人把“为什么不用@RestControllerAdvice”讲清楚,实际上这才是很多人的困惑源头。

4.2 自定义AccessDeniedHandler返回统一 JSON 的正确姿势

自定义处理器的核心接口就一个方法:

public class JsonAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":403,\"message\":\"您没有权限进行此操作\"}"); } }

然后在配置里挂上去:

http.exceptionHandling(ex -> ex.accessDeniedHandler(new JsonAccessDeniedHandler()));

这段代码本身并不复杂,但有三个细节值得注意。第一,response.setStatus一定要设置,否则前端拿到的是 HTTP 200 但 body 里告诉你没权限,这会让前端同事非常困惑;第二,响应 JSON 的字段最好和你的全局异常格式保持一致,比如都叫code和message,避免前端要写两套解析逻辑;第三,response.getWriter().write()之后不用调用flush和close,交给容器处理就行,但如果你用了一些包装 response 的过滤器,也有可能遇到流被关闭的问题,到时候再手动flush也不迟。

4.3 自定义处理器不生效的几种排查方向

写好了AccessDeniedHandler,重启项目后发现还是不生效,这种情况我也遇到过好几次。最常见的原因有三个。

第一个原因是配置被覆盖。如果你的项目里加了多个SecurityFilterChain的@Bean,一些老代码里还可能存在一个“默认配置”的WebSecurityConfigurerAdapter(Spring Security 5 时代遗留),两个配置起了冲突,最终生效的是没有挂自定义 handler 的那条链路。排查方法是在项目里搜一下SecurityFilterChain和WebSecurityConfigurerAdapter的类,看有没有重复定义。

第二个原因是异常根本没有被ExceptionTranslationFilter捕获。比如某些过滤器在你自定义的授权逻辑中直接 try-catch 了异常,然后自己处理掉了,那么你配置的accessDeniedHandler自然不会被调用。还有种情况是使用了早于 Spring Security 5.6 的版本,部分行为的细节和后来版本不一致。

第三个原因可能出在拦截器顺序上。如果你的AccessDeniedHandler内部返回了自定义 JSON,但某个后续过滤器在输出时又添加了页头或者改写了响应体,也可能导致最终的响应内容不是预期内容。遇到这种情况最简单的验证办法是在浏览器开发者工具里看“Network”面板中该请求的状态码,如果状态码是 403,说明 handler 生效了,失败点在后面对响应体的处理;如果状态码是 200,说明 handler 压根没有被调用,应该回到配置去查。

5. 工作中遇到的 AccessDeniedException 变体问题

5.1@PreAuthorize在类内部方法调用时不生效

这是一个特别隐蔽的问题。你在同一个类里写了一个方法,上面标了@PreAuthorize("hasRole('ADMIN')"),然后在同一个类的另一个方法里直接调用它,比如this.someMethod(),结果发现权限校验完全没生效。原因在于 Spring Security 的方法级安全是基于 AOP 代理实现的,只有通过代理对象调用方法时,切面逻辑才会执行。如果你在类内部用this直接调用,实际调用的是原始对象的方法,注解自然被绕过了。

解决办法是注入自己或者通过ApplicationContext.getBean()获取代理对象后再调用。我在实际项目中见过不少因为这种问题导致的“看似配置了权限,实际上任何人都能访问管理员方法”的情况,比 403 更危险,因为表面一切正常,实际上权限控制完全失灵。建议团队在写权限敏感代码时,把这类方法拆到独立的 Service 类中,避免同类内调用。

5.2 异步线程里的SecurityContext丢失

当你的业务代码里使用@Async、CompletableFuture或者手动创建线程去处理任务时,新线程里通常会找不到原来的用户信息。因为SecurityContext默认绑定在创建它的线程上,线程池中的新线程并不会自动继承这份上下文。如果异步任务需要读取当前用户信息或者再次做权限校验,就会出现AccessDeniedException,因为那个线程里的authentication对象是空的。

解决办法有两种,一种是在提交任务时手动把SecurityContext传进去:

SecurityContext context = SecurityContextHolder.getContext(); CompletableFuture.runAsync(() -> { SecurityContextHolder.setContext(context); // 你的业务逻辑 });

另一种是使用 Spring Security 提供的DelegatingSecurityContextCallable或DelegatingSecurityContextRunnable包装任务。这两种方式都能把 SecurityContext 传递到子线程。这个坑在微服务拆分的异步链路里很常见,排查的时候往往要找很久才能发现是线程切换的问题。

5.3 JWT 里的权限 claim 没被解析出来

如果你的项目是用 JWT 作为认证凭证,服务器端从 Token 中解析用户权限时,经常会出现“权限列表为空”的问题。常见原因是 JWT 的荷载(Payload)里,权限字段的 key 和解析代码里使用的 key 不一致。比如你在发行 Token 时写入的是roles,而后端解析代码里读的是authorities,那最终生成的权限列表肯定是空的。此时访问任何受保护接口,都会因为用户没有任何权限而触发AccessDeniedException。

排查方法非常简单:把 Token 在网站或者本地的调试工具里解出来,看一下payload里的字段名,然后去对比后端解析的代码。另外还有一个容易忽视的点,就是 JWT 里存的角色名到底带不带ROLE_前缀。如果你在生成 Token 时直接把角色名单交给了setClaims,而后端用hasRole("ADMIN")校验,注意hasRole自己会加前缀,所以 Token 里存的就必须是ADMIN而不能是ROLE_ADMIN,否则会变成边对边错。这个细节稍不留意就会浪费半天时间。

5.4 从 Spring Security 5 升级到 6 之后出现的全新 403

Spring Security 6 的默认行为比 5 更严格,比如默认开启了 CSRF 防护,默认要求所有请求都要走认证链,不少从 5 升上来的项目会突然出现大量“不允许访问”。原因往往不是业务逻辑变了,而是框架默认行为变了。典型的例子是:升级到 6 之后,老项目里WebSecurityConfigurerAdapter的写法被废弃,配置需要迁移到SecurityFilterChain模式;同时securityMatcher的匹配逻辑、authorizeRequests之类的旧方法名都被替换成了新写法。

遇到这种情况,我的建议是不要急着改业务配置,先翻一下官方从 5 到 6 的迁移文档,重点看三点:一是HttpSecurity配置的写法变化,二是AccessDeniedException包名变化,三是默认开启的 CSRF 和 CORS 策略对现有请求的影响。很多时候升级完 403,其实是新版本默认把某些接口当成了受保护接口,而老配置还没有迁移过来。理解了这些差异再排查,远比一条一条试错要快。

6. 一张速查表,快速定位 AccessDeniedException 的常见原因

日常工作里,我排查AccessDeniedException基本已经形成了一套肌肉记忆。下面这张表,是我结合自己的实际经历和团队其他同学的反馈整理出来的高频原因和对应的解决方向,遇到问题可以直接拿去对照:

现象特征常见原因优先排查方向
已登录用户访问某些接口 403,日志有AccessDeniedException用户权限列表中缺少资源要求的角色打印authentication.getAuthorities(),对比配置里的hasRole/hasAuthority
未登录用户访问受保护接口,却显示“不允许访问”匿名用户被授权过滤器拒绝,401/403 未区分检查SecurityContext是否是AnonymousAuthenticationToken,并在exceptionHandling里配置AuthenticationEntryPoint
配置了@PreAuthorize但完全不生效方法级安全注解未开启检查是否加了@EnableMethodSecurity或@EnableGlobalMethodSecurity(prePostEnabled = true)
同一个请求时好时坏,偶尔 403Token 解析异常或权限数据加载不稳定检查 JWT 解析逻辑、Redis/Session 中角色数据是否完整
普通角色无法访问管理员接口,但回显并不是 404hasRole与hasAuthority的前缀/大小写不匹配打印授权列表,确认是否为ROLE_ADMINvsADMIN
自定义AccessDeniedHandler没生效SecurityFilterChain被重复配置或异常被提前捕获搜项目里的所有SecurityFilterChainBean,确认是否有多套配置
异步线程里抛出的权限异常SecurityContext未传递到子线程使用DelegatingSecurityContextRunnable或手动设置SecurityContext
从 Servlet 3.1 迁移或者从老版本升级后出现 403框架版本行为变化,旧配置不兼容查阅对应版本的迁移指南,重点看默认 CSRF、匹配规则和包名迁移

这张表不是万能的,但它能帮你把问题快速归到“配置问题”“数据问题”“环境问题”三个大类里。在团队内部排查时,我一般还会让大家把这张表和“最后一步操作”关联起来,谁改了什么配置、谁换了什么依赖、谁调整了哪个接口的权限要求,往往一对比就能立刻看到问题本质。

回到文章开头说的那个现象:看到org.springframework.security.access.AccessDeniedException,先别急着改代码。这个异常是 Spring Security 用“主动拒绝”的方式告诉你,某个人、某个 token、某个请求在授权链路上的身份或者权限信息和目标资源对不上。至于是人没登录、角色没配、前缀写错、注解没开、上下文丢失还是版本迁移遗留的问题,只需要按上面的几个方向逐一排查。

我自己的排查习惯,永远是先打印Authentication,看当前用户到底是“谁”,再对比配置里期待的是“谁”,然后才开始改代码。这听起来像一个笨办法,却能在十有八九的场景里一击命中。最后再分享一个小习惯:在开发环境里,我会把这些异常日志的级别调到 DEBUG,把拦截器和授权链路上每一步决策结果都打出来,这样即使前端什么都看不出来,后端也能清清楚楚地看到是哪个环节把请求拒掉了。排查这类问题,最忌讳的就是一边看代码一边猜,与其猜,不如花两分钟把现场信息打全,方向对了,解决起来就快多了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 6:49:00

YOLOv8+LPRNet车牌识别:训练、推理与部署实战

简介:基于YOLOv8与LPRNet的车牌识别系统,是一套面向计算机相关专业毕业设计、课程实践和小型智能监控项目的完整工程。项目采用YOLOv8定位车牌区域,LPRNet识别车牌字符,并整合Flask后端与Vue前端,实现了从图像/视频输入…

作者头像 李华
网站建设 2026/10/14 16:23:37

EEMD-LSTM非平稳时间序列预测:Python实现与避坑指南

简介:这是一套基于Python实现的EEMD-LSTM时间序列预测完整源码与配套数据,面向计算机、电子信息工程、数学等专业学生,可满足课程设计、期末大作业及毕业设计需求。项目在Anaconda、PyCharm环境下以Python与TensorFlow构建,采用集…

作者头像 李华
网站建设 2026/10/15 5:09:00

2026企业级AI编程工具深度测评:TaoToken统一Key接入与选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/15 1:06:04

LeetCode 70 爬楼梯全解:从递归到滚动数组的机考实战指南

机考刷题到第 70 题爬楼梯,很多人会觉得这题太简单了,一个 easy,递归几行就写完。但真到了机考或者面试手写代码的环节,这道题往往能筛掉一批人。LeetCode 70 爬楼梯是 LeetCode 热门 100 题里的钉子户,也是动态规划入…

作者头像 李华
网站建设 2026/10/14 17:53:46

红黑树与哈希表深度对比:从C++ map/unordered_map底层原理到工程实践

1. 容器选型背后的设计逻辑:先从使用感受说起很多人在初学阶段就背过一个结论:map底层是红黑树,unordered_map底层是哈希表。但真正被问到“为什么set和map可以共用同一套红黑树实现”“为什么unordered_map的元素不是有序的”这类问题时&…

作者头像 李华