news 2026/10/21 6:42:45

Spring Security认证授权全解析:从过滤器链到JWT实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Security认证授权全解析:从过滤器链到JWT实战

做Java后端开发的朋友,对Spring Security应该都不陌生。它几乎是Spring Boot生态里做认证与授权的默认选择,社区讨论多、出镜率高,但凡涉及登录、权限控制的项目,很难绕开它。不过我在面试候选人、也带过不少新人的过程中发现一个普遍现象:很多人能在项目里跑通配置,但真要问一句“请求进来之后,认证和授权到底是怎么一步步发生的”,能讲清楚的人少得可怜。大多数情况是照着网上的Demo粘贴一份,能跑通就行,至于为什么这么配、换个场景会不会出问题、报错时从哪里排查,心里其实没有底。

这篇文章,我打算把Spring Security的认证与授权机制彻底拆开来讲。先理清框架整体在解决什么问题,然后深入认证流程的每一步,再拆开授权拦截的逻辑,最后带大家落地一个“表单登录 + JWT + URL授权 + 方法级安全”的完整例子。内容定位偏向“原理 + 实战”双修,适合有一定Spring基础、但想真正搞懂Spring Security,希望从“会用”进阶到“懂用”的开发者阅读。

1. 框架定位与整体认知:先搞清楚Spring Security在解决什么问题

1.1 认证与授权的基本概念拆解

在聊框架之前,先把两个最基础、也最容易被混淆的词理清楚:认证(Authentication)和授权(Authorization)。

认证负责回答“你是谁”。它验证用户提交的身份凭证,比如用户名密码、Token、证书,确认这个请求确实来自某个合法用户。授权负责回答“你能做什么”。它基于已经确认的身份,判断这个用户有没有权限访问某个资源、执行某个操作。

我用一个生活化的类比来解释:公司大楼入口的刷卡机是认证,它确认你是本公司员工;但进到楼里之后,你刷的是“普通员工卡”,只能进办公区,而机房、财务室需要“管理卡”才能打开,这就是授权。认证是前提,授权是手段,两者缺一不可,顺序也不能颠倒。在Web系统里,最常见的错误做法是把认证和授权混在业务代码里写,比如在每个Controller手写一段“从Session里取用户,再判断角色”的逻辑。这样做的后果是,权限判断散落在各处,一旦规则变化就要全局搜索修改,安全性也很难保证。Spring Security的核心价值恰恰在于:用一套统一的过滤器链机制,把认证和授权从业务代码里抽离出来,放到请求进入应用的第一层处理。

1.2 为什么Spring Boot时代还是绕不开Spring Security

每次聊到Spring Security,总会有人问:“我只用Spring Boot做接口,自己拦截器写一写不行吗?为什么要引入一个这么重的框架?”这个问题背后其实是对框架价值的误解。

自己写Filter做登录校验,确实能应付最简单的场景,但一旦需求复杂起来,就会遇到一堆重复造的轮子:密码哈希怎么存?会话怎么管理?CSRF要不要防?接口权限怎么按角色区分?不同来源的用户(数据库、外部平台)怎么统一登录?这些问题的解决方案,Spring Security在十几年里已经被反复验证过,轮子已经做得很成熟了。另一个现实因素是,Spring Boot的Starter机制把Spring Security集成成本降得很低,只要引入依赖,默认就能获得一套基础安全防护,对项目来说,性价比非常高。

还有一点容易被忽略:Spring Security与Spring生态的耦合度极深。它在Spring AOP、注解体系、事件机制的基础上构建了完整的授权模型,尤其是方法级安全,可以直接通过注解控制Service层的方法访问权限,单靠Servlet Filter很难做到这种精细程度。所以与其重复发明,不如把这层能力纳进框架统一管理。

1.3 整体架构:过滤器链与安全上下文

Spring Security运行时最核心的骨架,是一条由多个过滤器组成的责任链,也就是常说的SecurityFilterChain。

当请求进入应用,会先经过Servlet容器的过滤器链,其中有一个特殊的过滤器叫FilterChainProxy,Spring Security的入口。FilterChainProxy内部持有多个SecurityFilterChain配置,根据请求URL匹配到对应的那一条,然后按顺序执行链上的过滤器。

一条典型的过滤链大致承担以下职责,按执行顺序排列:

  • 安全上下文过滤器:从会话或Header中恢复已认证信息,设置到SecurityContextHolder中。
  • 登录相关过滤器:比如处理用户名密码登录的UsernamePasswordAuthenticationFilter,拦截登录请求并执行认证。
  • 异常处理过滤器:捕获认证/授权阶段的异常,转换为HTTP响应码或跳转逻辑。
  • 授权过滤器:在链路末尾判断“当前身份能不能访问这个资源”,不能的话抛出AccessDeniedException。
  • 日志、请求缓存、CSRF、CORS等辅助过滤器:提供附加能力。

这些过滤器顺序不是随便排的。认证必须先于授权;安全上下文必须先恢复,后面的过滤器和业务代码才能读到当前用户信息。

与过滤器链紧密相关的是SecurityContext。简单理解,它就是当前线程内的一个“用户信息容器”,里面放的是经过认证后得到的Authentication对象。SecurityContextHolder负责持有这个上下文,默认使用ThreadLocal实现,意味着一次请求的整个处理链路都能从同一个地方读到当前用户。

为什么不建议在每个Controller里做鉴权?因为安全边界必须收敛在入口处统一处理。如果每个接口各自判断权限,一旦漏掉某个接口,就相当于给攻击者留了一扇没锁的门。所以Spring Security在过滤链末端做统一拦截,用配置的方式声明规则,而不是让业务代码各自为政。

2. 认证机制的核心细节:从登录表单到SecurityContext

2.1 认证流程五步拆解

认证是整个框架最核心的环节。以最常见的用户名密码登录为例,完整认证流程可以拆成五步。

第一步,请求到达UsernamePasswordAuthenticationFilter。这个过滤器会拦截配置好的登录URL,比如/login,从POST请求的参数中取出用户名和密码。

第二步,过滤器把用户名密码封装成一个UsernamePasswordAuthenticationToken对象。注意,此刻这个Token是“未认证”状态,它只是携带凭证,还没有被验证过。

第三步,过滤器把Token交给AuthenticationManager处理。AuthenticationManager是认证的顶层入口,负责调度整个认证过程。

第四步,AuthenticationManager默认的实现是ProviderManager,它内部维护了一个AuthenticationProvider列表。ProviderManager逐个询问Provider:“这个Token你能处理吗?”对于用户名密码这种认证方式,负责处理的是DaoAuthenticationProvider。

第五步,DaoAuthenticationProvider调用UserDetailsService,按用户名加载用户信息,得到UserDetails对象。然后拿UserDetails中的密码和用户提交的密码做比对,用PasswordEncoder校验。校验通过后,Provider会创建一个“已认证”状态的Authentication对象,里面包含了完整的用户身份和权限信息,最终由过滤器把这个对象放进SecurityContextHolder。

经过这五步,一次请求就完成了从匿名到认证的转变。之后同一个线程内的代码,都可以通过SecurityContextHolder.getContext().getAuthentication()拿到当前用户。

我在实际阅读源码时有个体会:理解这一步的关键,不在于记住每个类名,而在于理解Token状态的变化——从“未认证”到“已认证”,本质是凭证被验证的过程。后面自定义认证方式时,核心就是模拟这个过程。

2.2 核心组件职责与默认实现

把这五个步骤中的关键组件列成一张对照表,方便理解各自职责。

组件接口/抽象类默认实现核心职责
AuthenticationManagerAuthenticationManagerProviderManager认证调度入口,协调各Provider
AuthenticationProviderAuthenticationProviderDaoAuthenticationProvider执行具体认证逻辑
UserDetailsServiceUserDetailsServiceInMemoryUserDetailsManager / JdbcDaoImpl按标识加载用户信息
UserDetailsUserDetails自定义实现 / User类描述用户身份与授权信息
PasswordEncoderPasswordEncoderBCryptPasswordEncoder负责密码加密与校验
SecurityContextHolder--持有当前线程的安全上下文

这里有一个值得细品的设计:AuthenticationManager为什么只是“调度”,而不是“亲自做校验”?因为一个系统里认证方式可能不止一种:用户名密码、短信验证码、外部平台授权,它们校验逻辑完全不同。如果让AuthenticationManager直接实现所有逻辑,这个类会膨胀到无法维护。所以框架把“认证方式”抽象成AuthenticationProvider,每个Provider只负责一种凭证类型,AuthenticationManager负责按顺序和类型分发。

ProviderManager内部还有一个容易被忽略的细节:如果所有Provider都无法处理当前认证类型,它会将异常向上抛出;如果有Provider处理了,但认证失败,会抛出AuthenticationException。所以配置多个Provider时,顺序会影响认证行为,尤其要谨慎处理“匿名支持”这种兜底Provider。

2.3 密码存储与校验:为什么推荐BCrypt

在Spring Security里,密码不能明文存储,这是底线。登录校验也是围绕PasswordEncoder进行的,它决定了“用户提交的密码”和“数据库里的密码”如何比对。

早期版本中,常用的是StandardPasswordEncoder和Md5PasswordEncoder,它们都有已知缺陷:MD5哈希速度太快,攻击者可以用暴力破解或彩虹表快速还原明文;简单的加盐方式也容易被绕过。所以从5.0开始,框架推荐并默认使用BCryptPasswordEncoder。

BCrypt是一种基于Blowfish密码算法的自适应哈希函数,有几个重要特性:

  • 自动加盐:每次哈希生成的盐是随机的,因此同一密码两次加密结果不同。
  • 可调节代价:通过strength参数控制计算轮数,强度越高,破解成本越高。
  • 抗GPU加速:BCrypt需要较多内存和计算步骤,比MD5/SHA-256更难被硬件并行破解。

在实际配置中,更推荐使用DelegatingPasswordEncoder,它可以根据密码字符串前缀自动选择编码器。例如,存储格式为{bcrypt}$2a$10$...,框架看到{bcrypt}前缀就知道用BCrypt校验。这种设计的优势在于,将来需要升级密码算法时,旧密码仍可以按旧算法校验,用户下次登录时再平滑迁移到新算法。

我在项目中看到过不少把密码以明文入库、或者用普通MD5哈希就上线的案例,这类系统遇到拖库基本等于裸奔。无论项目大小,密码存储都应该在第一步就按最高标准配置。

2.4 SecurityContext与会话持久化

认证完成后,Authentication对象放进SecurityContextHolder只是当前线程可见。对于有状态会话(如传统Servlet Session),还希望下一个请求进来时,用户身份能自动恢复。这个工作由SecurityContextRepository完成。

框架提供了几种持久化策略:

  • HttpSessionSecurityContextRepository:把SecurityContext保存在Session中,默认在有状态会话下使用。
  • RequestAttributeSecurityContextRepository:把SecurityContext保存在Request属性中,适用于无状态API场景。
  • NullSecurityContextRepository:不持久化,搭配无状态JWT时常用。

对于Spring Boot 3 + Spring Security 6的项目,默认的配置会启用基于会话的持久化。但如果我们做的是前后端分离、无状态JWT场景,通常要显式把session策略设为STATELESS,并自定义过滤器在每次请求中从Token重建SecurityContext。

这里有个并发场景的经典坑:子线程里通过SecurityContextHolder.getContext()拿不到用户信息。因为默认策略是MODE_THREADLOCAL,子线程不会继承父线程的上下文。解决思路有两种:一是把需要的用户信息作为参数传递,而不是依赖上下文;二是把SecurityContextHolder配置为MODE_INHERITABLETHREADLOCAL,让子线程继承,但这种方式在复杂线程池场景下可能带来上下文污染,要谨慎使用。

3. 授权机制的核心逻辑:过滤、匹配、表达式

3.1 两层授权设计:URL级与方法级

认证解决“你是谁”,授权解决“你能做什么”。Spring Security把授权分成两个层次。

第一层是URL级授权,也是最常见的配置方式:在SecurityFilterChain里声明什么样的URL需要什么角色或权限。比如/admin/**需要ROLE_ADMIN,/user/**登录后可用。这一层适合做粗粒度的路由保护。

第二层是方法级授权,通过注解作用在Service或Controller的方法上,由Spring AOP在方法调用前拦截判断。比如@PreAuthorize("hasAuthority('order:update')"),只有拥有指定权限的用户才能调用这个方法。这一层可以做非常细粒度的控制,甚至能引用方法参数、返回值、当前用户信息做动态判断。

为什么需要两层?因为前端路由不是安全边界。前端页面可以隐藏按钮、跳转路由,但接口是裸奔的,直接调用后端接口就能绕过前端限制。URL级授权把住了接口的第一道关,方法级授权则在代码层面做兜底,尤其适用于那种“接口入口允许登录用户访问,但具体业务数据要按归属权判断”的场景。

两层授权不是互斥关系,而是互补关系。我的习惯是:URL层做粗粒度路由控制,方法层做业务级精细控制。

3.2 URL级授权:RequestMatcher与规则顺序

Spring Security 6中,URL级授权在SecurityFilterChain里通过authorizeHttpRequests配置。一个典型的配置如下:

http.authorizeHttpRequests(auth -> auth .requestMatchers("/login", "/register", "/public/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") .requestMatchers("/order/**").hasAuthority("order:view") .anyRequest().authenticated() );

这里有几个容易踩坑的细节。

第一个是匹配器版本的差异。早期版本区分antMatchers和mvcMatchers,Spring Security 6中统一为requestMatchers,它内部根据路径处理规则做匹配。写的时候要清楚它的匹配语义,比如/admin/**能匹配/admin/user/list,但不会匹配/admin本身,需要精确匹配时单独配置。

第二个是anyRequest的位置。anyRequest是兜底规则,必须放在最后面。如果把具体规则写在anyRequest后面,后面的规则永远不会生效,因为anyRequest匹配了所有请求后会直接决定放行或拦截。

第三个是规则的粒度控制。hasRole和hasAuthority虽然都用于权限判断,但语义不同:hasRole内部会自动拼接ROLE_前缀,比如hasRole("ADMIN")实际检查的是ROLE_ADMIN;hasAuthority则直接检查权限集合中的权限标识,不自动加前缀。混用前一定要确认自己业务里存的是角色还是权限字符串。

3.3 方法级授权:注解体系与SpEL表达式

启用方法级安全需要在配置类上加@EnableMethodSecurity,然后就可以在方法上使用授权注解了。

常用的注解有四个:

  • @PreAuthorize:方法执行前校验,最常用,比如判断当前用户是否有某个权限。
  • @PostAuthorize:方法执行后校验,适合返回数据需要做细粒度校验的场景,比如只能返回用户自己的订单。
  • @PreFilter / @PostFilter:分别对方法入参集合和返回集合做过滤,去掉无权访问的元素。

另外还有一组功能相似的注解:@Secured和@RolesAllowed,后者是JSR-250标准。两者都只支持角色判断,不像@PreAuthorize那样支持复杂的SpEL表达式,所以现在的新项目我更推荐主用@PreAuthorize。

方法级授权表达式最核心的变量是authentication,它代表当前认证信息;更直观地,可以用principal拿到当前用户对象。常见的表达式写法如下:

表达式作用
hasRole('ADMIN')当前用户是否拥有ROLE_ADMIN角色
hasAuthority('order:delete')当前用户是否拥有指定权限标识
hasAnyRole('ADMIN', 'MANAGER')是否具备任一角色
isAuthenticated()是否已认证
#param.ownerId == authentication.principal.userId参数中的ownerId是否等于当前用户ID
@permissionService.checkOrder(#id)调用自定义Bean完成复杂校验

最后一种写法特别有用。当业务授权逻辑很复杂、不方便用一个表达式搞定,或者需要复用同一套校验逻辑时,可以直接在表达式里调用一个Spring Bean的方法。这样既保持了表达式的简洁,又能把复杂逻辑放到Service里做单元测试。

3.4 自定义权限评估与基于业务数据的授权

方法级表达式内置的能力在某些场景不够用。比如想实现“用户只能操作自己的数据”,表达式会写成#order.userId == authentication.principal.id,这样写能做,但若多处重复这种逻辑,表达式会很长,维护成本高。

更好的方案是扩展PermissionEvaluator。这个接口实现hasPermission方法,配合@PreAuthorize里的hasPermission(...)表达式使用。

@Component public class OrderPermissionEvaluator implements PermissionEvaluator { @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { if (targetDomainObject instanceof Order order) { User principal = (User) authentication.getPrincipal(); return order.getUserId().equals(principal.getId()); } return false; } // 省略另一重载 }

然后在配置里注册这个Evaluator,方法上就可以写:

@PreAuthorize("hasPermission(#order, 'update')") public void updateOrder(Order order) { ... }

这种做法的好处在于,授权规则从业务方法中解耦出来,集中在一处维护。如果规则调整,只需要修改Evaluator,不必翻遍所有业务方法。

业务数据授权是后端系统里最容易出错的一类需求。比如电商订单、社交关系的资源归属,做不好就存在越权漏洞。业内通常叫“水平越权”问题——正常登录的用户,通过修改请求参数访问了其他用户的资源。URL级授权无法阻挡这类攻击,必须在Service层用方法级授权做数据归属校验。

4. 实操:Spring Boot集成、JWT认证与双层授权

4.1 环境准备与版本选型

理论说了一大堆,还是得上手做一遍。我选择Spring Boot 3.x,对应的Spring Security版本是6.x,JDK用17以上。

这里特别说明为什么不用Spring Boot 2.x。Spring Security 5.x中的很多配置写法在6.x中发生了变化,比如WebSecurityConfigurerAdapter被移除,改成基于SecurityFilterChain的Bean声明式配置。如果新项目还用旧写法,光是一个“彻底搞明白配置方式”就要花不少时间。直接上6.x可以用最简洁的lambda DSL,也更符合当前主流技术栈。

Maven依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>

数据库访问用Spring Data JPA或者MyBatis都能跑,这里不纠缠具体持久层方案。为了聚焦安全逻辑,我用一个内存版的UserDetailsService演示。

4.2 核心配置:SecurityFilterChain

先写最核心的安全配置类。

@Configuration @EnableWebSecurity @EnableMethodSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login", "/api/auth/register").permitAll() .requestMatchers("/api/admin/**").hasRole("ADMIN") .requestMatchers("/api/order/**").authenticated() .anyRequest().authenticated() ) .exceptionHandling(ex -> ex .authenticationEntryPoint((request, response, authException) -> { response.setStatus(401); response.getWriter().write("{\"code\":401,\"message\":\"unauthorized\"}"); }) .accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(403); response.getWriter().write("{\"code\":403,\"message\":\"forbidden\"}"); }) ) .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } }

逐个解释这些配置的含义。

.csrf(csrf -> csrf.disable()):无状态API场景下,没有Session,CSRF攻击的立足点不成立,可以关闭。但如果有状态会话、且涉及表单提交,务必保留CSRF防护。

.sessionCreationPolicy(SessionCreationPolicy.STATELESS):告诉Spring Security不使用Session,这个设置会决定安全上下文持久化策略。

authorizeHttpRequests里的规则顺序很重要:放行规则必须写前面,具体业务规则写中间,anyRequest兜底写最后。

自定义的AuthenticationEntryPoint和AccessDeniedHandler分别处理未认证和未授权两种情况,统一返回JSON而不是302跳转,这是前后端分离项目的基本要求。

4.3 用户体系与认证逻辑

定义一个简单的用户内存存储:

@Repository public class LocalUserDetailsService implements UserDetailsService { private final Map<String, UserDetails> users = new ConcurrentHashMap<>(); public LocalUserDetailsService(PasswordEncoder passwordEncoder) { users.put("admin", User.builder() .username("admin") .password(passwordEncoder.encode("admin123")) .roles("ADMIN", "USER") .build()); users.put("user", User.builder() .username("user") .password(passwordEncoder.encode("user123")) .roles("USER") .build()); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = users.get(username); if (user == null) { throw new UsernameNotFoundException("user not found"); } return user; } }

PasswordEncoder因为要跨类使用,单独用Bean声明:

@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

然后是最关键的JWT认证过滤器。它的职责是:从请求头解析Token,验证通过后,把用户身份重建为Authentication对象,放进SecurityContextHolder,让后续过滤器感知到“这个请求已认证”。

@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String resolveToken = jwtTokenProvider.resolveToken(request); if (resolveToken != null && SecurityContextHolder.getContext().getAuthentication() == null) { if (jwtTokenProvider.validateToken(resolveToken)) { String username = jwtTokenProvider.getUsername(resolveToken); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }

这段代码的逻辑不复杂,但有几个点值得强调。

过滤器的执行阶段必须在UsernamePasswordAuthenticationFilter之前,这样才能在认证流程启动前恢复用户身份。所以配置里用addFilterBefore把这过滤器插入链路靠前的位置。

Token验证时,我没有反复查数据库来核对用户是否存在,而是先验证签名合法,再去UserDetailsService加载用户信息。这样做既确保Token可信,又能及时发现“用户已被删除”的情况。

SecurityContextHolder里已经存在Authentication时不重复设置,防止一个请求里多次解析Token造成性能浪费。

JwtTokenProvider负责生成和解析Token,我用JJWT库完成:

@Component public class JwtTokenProvider { private final SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); public String generateToken(String username, List<String> roles) { Instant now = Instant.now(); return Jwts.builder() .setSubject(username) .claim("roles", roles) .setIssuedAt(Date.from(now)) .setExpiration(Date.from(now.plus(24, ChronoUnit.HOURS))) .signWith(key) .compact(); } public String resolveToken(HttpServletRequest request) { String bearer = request.getHeader("Authorization"); if (bearer != null && bearer.startsWith("Bearer ")) { return bearer.substring(7); } return null; } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } public String getUsername(String token) { return Jwts.parserBuilder().setSigningKey(key).build() .parseClaimsJws(token).getBody().getSubject(); } }

生成Token时把角色信息写进claims,后续过滤器加载UserDetails时可以据此构建权限集合,可以省掉一次数据库交互。

4.4 双层授权验证:URL规则与方法注解

配置完成后,再来看授权规则的实际效果。

URL级授权已经在SecurityFilterChain中声明:/api/admin/**只有ROLE_ADMIN可以访问,/api/order/**登录后即可访问,/api/auth/login和/api/auth/register允许匿名。

方法级授权加上之后,同一个URL入口下不同方法还能有更细致的区分。比如订单接口:

@RestController @RequestMapping("/api/order") public class OrderController { @GetMapping("/list") @PreAuthorize("hasAuthority('order:view')") public List<Order> list() { ... } @PostMapping("/create") @PreAuthorize("hasAuthority('order:create')") public Order create() { ... } @DeleteMapping("/{id}") @PreAuthorize("@orderPermissionEvaluator.hasPermission(#id, 'ORDER', 'delete')") public void delete(@PathVariable Long id) { ... } }

这里用了两种写法:前两个是简单的权限标识判断,第三个调用了自定义校验逻辑,在删除前确认当前用户是否是订单所属人。实际项目里,删除、修改这类操作建议都做数据归属校验,防止水平越权。

4.5 登录与认证联调

登录接口本身比较标准:

@RestController @RequestMapping("/api/auth") public class AuthController { @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword() ) ); UserPrincipal principal = (UserPrincipal) authentication.getPrincipal(); String token = jwtTokenProvider.generateToken(principal.getUsername(), principal.getRoles()); return ResponseEntity.ok(new LoginResponse(token)); } }

这里直接注入AuthenticationManager,它由Spring Security通过AuthenticationConfiguration自动暴露。核心逻辑就是执行一次完整的认证流程——这正好呼应了前面第2节拆解的认证过程。

启动项目后,用POST请求访问/api/auth/login,带上用户密码,返回Token。拿Token放进Authorization: Bearer xxx请求头,再访问受保护接口。用admin账号访问/api/admin/**能成功,用user账号访问同样的接口会被AccessDeniedHandler拦下,返回403。通过这个流程,认证和授权的完整链路就串起来了。

5. 常见问题与排查技巧实录

5.1 401和403:两个容易混乱的状态码

实际联调中,401和403的区分是最常见的困惑点。

401 Unauthorized表示未认证,也就是请求缺少有效凭证。比如没带Token、Token过期、Token格式不对。此时请求根本还没进入授权判断环节。403 Forbidden表示已认证但无权限,也就是身份合法但等级不够。比如普通用户去访问管理员接口。

如果一个请求返回401,优先检查SecurityContextHolder里是否成功设置了Authentication,反向排查JwtAuthenticationFilter是否生效、Token校验是否通过。如果一个请求返回403,优先检查授权表达式、角色前缀、规则匹配顺序。

有一种很容易误导人的场景:接口返回403而不是401,但用户明明没有登录。这种多数是因为anonymous()过滤器先设置了一个匿名的Authentication对象,导致授权阶段把“匿名”当作“已认证但无权限”,从而走403分支。定位时用日志看一下当前Authentication的authorities就清楚了。

5.2 常见报错与解决方案速查

报错信息问题原因解决方向
There is no PasswordEncoder mapped for the id "null"存储的密码没有{id}前缀,且配置中没有明确PasswordEncoder统一用PasswordEncoder加密,或配置DelegatingPasswordEncoder
Failed to configure a DataSource引入Spring Security后默认走JDBC用户存储,却未配置数据源显式声明UserDetailsService或排除默认配置
401 Unauthorized,但登录凭证没问题过滤器配置顺序错误,自定义JWT过滤器未生效检查addFilterBefore的位置与类上的@Component注解
403 Forbidden,所有接口都访问不了授权规则顺序不当,anyRequest写得太靠前调整requestMatchers顺序,兜底放最后
Circular view path / login未配置permitAll,导致登录页也被拦截,形成死循环放行登录相关路径
方法级注解不生效缺少@EnableMethodSecurity在配置类上加注解并启用

这里特别提一点:很多人在项目启动后遇到“No PasswordEncoder”的报错,往往是因为数据库里存的密码没有带{bcrypt}前缀。解决办法不是去掉PasswordEncoder,而是统一迁移密码数据格式。

5.3 安全实践补充与调试技巧

几个实战中体会比较深的点:

CSRF防护在有状态会话下必须保留。只是前后端分离、无状态API场景才可以关闭,自己要对安全边界有清晰判断。

CORS配置常被忽视。允许跨域时不要把allowedOrigins("*")和allowCredentials(true)同时使用,浏览器规范层面会拒绝该配置。更稳妥的做法是维护显式的域名白名单。

排查安全问题建议开启调试日志,在application.yml里配置:

logging: level: org.springframework.security: DEBUG

开启后能看到过滤器链的执行过程、请求匹配到了哪些规则、授权表达式求值结果,排查效率会大幅提升。生产环境记得关闭。

Session固定攻击防护默认是开启的,如果你确实需要自定义登录流程,要注意不要覆盖掉防护逻辑。简单说就是认证成功后要变更会话标识,防止攻击者在登录前“种”一个已知会话ID。

最后说一个我个人的体会:Spring Security的学习曲线,其实大部分时间不是在记类名和API,而是在建立“请求穿过过滤器链”的动态图像。只要头脑里有这条链,认证怎么走、授权在哪拦、报错怎么定位,全都顺理成章。配置虽然看起来多,但它本质上是在声明边界,而不是在写业务逻辑,想清楚这一层,写起来心情会轻松不少。

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

knowledge-work-plugins:知识工作流的操作系统级重构

1. “knowledge-work-plugins”不是插件集合&#xff0c;而是一套知识工作者的数字工作流操作系统你第一次看到“knowledge-work-plugins”这个词&#xff0c;大概率会下意识把它拆成“knowledge work”“plugins”&#xff0c;然后脑补出一堆浏览器插件、VS Code扩展或者Notio…

作者头像 李华
网站建设 2026/10/21 6:42:09

Git reset与revert深度解析:原理、区别与团队协作最佳实践

1. 版本回退的两种思路&#xff1a;先搞懂它们各自在解决什么问题刚接触 Git 那会儿&#xff0c;我最怕的就是看到提交历史里出现一堆乱七八糟的记录&#xff0c;想退回去又怕把代码搞丢。后来用多了才发现&#xff0c;git reset和git revert这两个命令虽然都跟“撤销”有关&am…

作者头像 李华
网站建设 2026/10/21 6:41:36

横向联邦聚合实战:从数学直觉到工程落地的完整拆解

1. 横向联邦聚合到底在解决什么问题第一次接触横向联邦聚合这个概念&#xff0c;很多人会把它和分布式训练混为一谈。我刚开始也这么以为&#xff0c;直到在一个跨机构数据协作的模拟项目里踩了坑&#xff0c;才发现两者的核心矛盾完全不同。分布式训练假设所有节点数据可以自由…

作者头像 李华
网站建设 2026/10/21 6:41:32

AI论文写作工具怎么选?七类场景实用适配指南

每年一到毕业季&#xff0c;后台就会涌进来一堆问AI写论文工具的消息。问得最多的不是“哪个工具最强”&#xff0c;而是“我这种情况到底该用哪个”。我接触过的毕业论文写作者&#xff0c;从专科到博士都有&#xff0c;学科从文学历史到计算机化工跨度极大&#xff0c;需求差…

作者头像 李华
网站建设 2026/10/21 6:41:32

agent-skills:可插拔智能体技能引擎的设计与生产实践

1. 项目概述&#xff1a;这不是一个“技能库”&#xff0c;而是一套可插拔的智能体行为引擎“agent-skills”这个名称乍看像某个开源项目的子模块&#xff0c;或是某篇论文里被轻描淡写带过的术语&#xff0c;但实际在当前智能体&#xff08;Agent&#xff09;开发实践中&#…

作者头像 李华
网站建设 2026/10/21 6:41:32

51单片机PID直流电机调速实战:从编码器测速到参数整定

1. 先说这个项目到底在解决什么问题做51单片机直流电机调速的人&#xff0c;多半都经历过这种尴尬&#xff1a;旋钮拧到某个位置&#xff0c;电机空载跑得挺欢&#xff0c;手一捏阻力上来&#xff0c;转速立刻掉一截&#xff1b;供电电压从12V跌到9V&#xff0c;转速也跟着缩水…

作者头像 李华