1. 前后端分离项目中Spring Security的改造思路先说结论Spring Security这套框架天生是为“服务端渲染”的传统Web应用设计的。它默认要给你一个登录页、要管Session、要在重定向时带上跳转地址。一旦拿到前后端分离项目里这些默认行为几乎全都要推翻重来。做过前后端分离项目的同学应该都有这个感受后端接口是纯JSON交互前端用Vue或React登录状态用Token来维持压根没有“页面跳转”这回事。而Spring Security默认的表单登录、Session管理、CSRF防护、403/401跳转页全都和这个模型冲突。你如果不改直接把Spring Security加进来就会出现“接口调不通、前端拿不到友好提示、跨域被拦、Token没地方放”等一堆问题。我见过不少同学在前后端分离项目里折腾Spring Security卡住的点其实就那么几个登录成功和失败怎么返回JSON、无状态Token怎么塞进SecurityContext、放行的接口怎么配置、权限不足时怎么给前端返回规范结构体。这篇文章就围绕这套场景把我实际落地时踩过的坑和验证过的方案都写出来。先说清楚适用场景Spring Boot 2.7、Spring Security 5.8这也是目前新项目的主流选型。网上很多教程还在用WebSecurityConfigurerAdapter那个类在新版本已经废弃了照抄老代码大概率编译不过。这里全部使用基于SecurityFilterChain的组件式写法新老项目都能直接参考。整个改造的核心思路可以概括成六件事关闭表单登录不启用默认的登录页跳转逻辑关闭CSRF前后端分离用Token做认证CSRF的意义不大把认证成功的返回逻辑替换成自定义JSON响应把认证失败的返回逻辑替换成自定义JSON响应通过OncePerRequestFilter实现Token解析把登录状态塞进SecurityContext配置CORS允许前端跨域调用这六件事做完Spring Security在前端分离项目里就能跑得很顺。下面逐一拆开讲。2. 认证机制选型为什么用无状态Token方案这里先解决一个最基础的问题前后端分离项目到底用什么维持登录状态传统Web应用的做法是Session浏览器自动维护一个Session Cookie服务端根据Session ID找到对应的登录用户。这个过程对前端是完全透明的你不需要自己处理任何东西。但前后端分离之后前端和后端可能是不同域名、不同端口跨域带上Cookie本身就有一堆限制而且后端如果部署了多个实例Session共享还要引入Redis维护成本一下就上去了。无状态Token方案的做法就简单粗暴用户登录成功后后端签发一个Token返回给前端前端保存起来以后每次请求都塞在请求头里发过来。后端不需要存Token拿到Token验证签名、解析出用户身份就行。这背后其实是JWTJSON Web Token在支撑一个JWT由三部分组成Header头部、Payload载荷、Signature签名。Header和Payload都是Base64编码的可读数据Signature是用密钥对前两段做的签名只要密钥不泄露Token就无法伪造。这个方案来看一个实际过程会比较直观。用户携带用户名密码请求登录接口后端校验成功后生成一个TokenToken里可以放用户ID、用户名、过期时间这些关键信息。前端拿到后存到LocalStorage或内存中之后请求时带上Authorization: Bearer token后端过滤器解析Token把用户信息放入SecurityContext后续业务接口就能直接通过SecurityContextHolder.getContext().getAuthentication()获取当前登录用户。无状态Token方案的一个劣势也值得提醒Token签发之后在过期之前你是没办法主动把它作废的。假设用户的账号被顶下线或修改了密码已经签发的Token依然有效直到自然过期。解决这个问题的常见做法是引入Token黑名单机制或者把Token续期做成Refresh Token的格式但会增加不少复杂度。项目初期Token有效期设置得合理一点比如2小时完全够用。安全性方面有几个细节必须注意不要把敏感信息如明文密码放进TokenPayload虽然经过数字签名但整体只做了Base64编码别人是可以解码看内容的Token的过期时间不要设置太长通常2到8小时比较合适使用HTTPS传输接口数据防止Token在传输过程中被截获修改密码、注销登录等敏感操作后建议同时更换Token3. 核心代码落地SecurityFilterChain配置与JWT过滤器3.1 基础依赖引入新建一个Spring Boot项目引入Spring Security和JWT相关依赖。JWT选用了Java生态常用的jose库Nimbus JOSE JWTspring-security-oauth2-jose里已经集成了不需要额外引入太多东西。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-jose/artifactId /dependency3.2 核心配置类配置类是整篇文章的主角用SecurityFilterChain的写法实现。这里有个需要特别注意的地方Spring Security 6.0之后一些方法名做了调整比如antMatchers变成了requestMatchersand()链式调用被拆成了多个参数。我这里以Spring Security 6.x的写法为准。Configuration EnableWebSecurity EnableMethodSecurity public class SecurityConfig { Resource private JwtAuthenticationFilter jwtAuthenticationFilter; Resource private RestAuthenticationEntryPoint authenticationEntryPoint; Resource private RestAccessDeniedHandler accessDeniedHandler; Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .cors(Customizer.withDefaults()) .sessionManagement(session - session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) ) .authorizeHttpRequests(auth - auth .requestMatchers(/api/auth/login, /api/auth/register, /api/auth/captcha).permitAll() .requestMatchers(HttpMethod.GET, /api/articles/**).permitAll() .requestMatchers(/api/admin/**).hasRole(ADMIN) .anyRequest().authenticated() ) .exceptionHandling(ex - ex .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }这一段配置看起来不长但每一行背后都有说头挨个解释csrf(AbstractHttpConfigurer::disable)前后端分离项目中前端通过Token做身份认证CSRF Token的主要防护场景基于Cookie的Session认证已经不成立了。如果保持开启Post请求会被默认拦截前端还得额外去获取CSRF Token体验很差直接关掉。sessionManagement设置成STATELESS意思是Spring Security不再创建HttpSession。这样后端服务就不会因为用户登录产生Session数据对水平扩展很友好也符合无状态Token方案的定位。authorizeHttpRequests这段是接口访问规则。登录注册接口放行、公开的资讯接口放行、管理员接口限定角色、其余接口全部要走认证。这里需要注意requestMatchers的匹配规则千万不要把/api/auth/**直接全放行否则登出接口也暴露了。exceptionHandling这一段配置的是认证失败和权限不足的处理入口。前端分离项目里用户没登录访问受保护接口时不应该返回302重定向到登录页而是返回一个401状态码的JSON用户登录了但角色不够返回403状态码的JSON。这里的自定义实现放在下面的小节说明。3.3 JWT认证过滤器过滤器是整条链路上最重要的自定义组件。它做的事情是从请求头中提取Token解析Token拿到用户名加载用户信息并构建Authentication对象最后塞进SecurityContext。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Resource private JwtTokenProvider jwtTokenProvider; Resource private UserDetailsService userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token jwtTokenProvider.resolveToken(request); if (token ! null jwtTokenProvider.validateToken(token)) { String username jwtTokenProvider.getUsername(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); if (userDetails ! null) { UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }这里有一个关键点过滤器继承的是OncePerRequestFilter不是GenericFilterBean。前者可以保证一个请求在过滤器链中只被调用一次避免因为内外层过滤器嵌套导致重复执行。Token解析和校验的逻辑封装在JwtTokenProvider里面Component public class JwtTokenProvider { private final SecretKey key Keys.hmacShaKeyFor(your-secret-key-please-change-to-a-long-random-string.getBytes(StandardCharsets.UTF_8)); public String createToken(String username, ListString roles, long expirationMinutes) { Instant now Instant.now(); Instant expiry now.plus(expirationMinutes, ChronoUnit.MINUTES); JwtClaimsSet claims JwtClaimsSet.builder() .subject(username) .issuedAt(now) .expiresAt(expiry) .claim(roles, roles) .build(); JwsHeader jwsHeader JwsHeader.with(MacAlgorithm.HS256).build(); return new JwtEncoder(Jwks.ROTATION).??? // 具体编码方式下面给出 } }不好意思上面那段是思路草稿正式写的时候还是用最直观的JwtEncoder来实现代码更简洁Component public class JwtTokenProvider { Value(${app.jwt.secret}) private String secret; Value(${app.jwt.expiration-minutes}) private long expirationMinutes; private SecretKey key; PostConstruct public void init() { this.key Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String createToken(String username, SetString roles) { Instant now Instant.now(); Instant expiry now.plus(expirationMinutes, ChronoUnit.MINUTES); JwtClaimsSet claims JwtClaimsSet.builder() .subject(username) .issuedAt(now) .expiresAt(expiry) .claim(roles, roles) .build(); JwsHeader header JwsHeader.with(MacAlgorithm.HS256).build(); JwtEncoder encoder new NimbusJwtEncoder(new ImmutableSecret(key)); return encoder.encode(JwtEncoderParameters.from(header, claims)).getTokenValue(); } public String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } public boolean validateToken(String token) { try { NimbusJwtDecoder decoder NimbusJwtDecoder.withSecretKey(key).build(); decoder.decode(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } public String getUsername(String token) { NimbusJwtDecoder decoder NimbusJwtDecoder.withSecretKey(key).build(); Jwt jwt decoder.decode(token); return jwt.getSubject(); } public SetString getRoles(String token) { NimbusJwtDecoder decoder NimbusJwtDecoder.withSecretKey(key).build(); Jwt jwt decoder.decode(token); return Set.copyOf(jwt.getClaimAsStringList(roles)); } }JWT的密钥需要注意HS256算法下密钥长度至少要256位也就是32个字节。如果密钥太短启动时会直接报错而且不要用这种短密码。配置放application.yml里app: jwt: secret: a-very-long-random-secret-key-that-is-at-least-32-characters expiration-minutes: 120拿到Token解析出的用户名后调用UserDetailsService从数据库加载用户信息再构建一个UsernamePasswordAuthenticationToken放进SecurityContext。这里有个容易踩的坑有些人会直接把roles放进Authentication里不重新加载UserDetails。这种做法在Token有效期内如果用户角色在数据库里被修改旧Token依然保留旧角色而重新查库能拿到最新权限所以统一走loadUserByUsername。3.4 登录接口与Token签发整个登录流程走完后用户拿到Token的入口就是登录接口。这里直接给出登录接口的完整实现RestController RequestMapping(/api/auth) public class AuthController { Resource private AuthenticationManager authenticationManager; Resource private JwtTokenProvider jwtTokenProvider; Resource private UserDetailsService userDetailsService; PostMapping(/login) public ResponseEntityResultVoid login(RequestBody LoginRequest loginRequest) { Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword())); String username authentication.getName(); UserDetails userDetails userDetailsService.loadUserByUsername(username); SetString roles userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toSet()); String token jwtTokenProvider.createToken(username, roles); return ResponseEntity.ok(Result.success(token)); } }AuthenticationManager的实例需要在配置类中暴露出来。这里有个细节Spring Security 6.x里不能直接Autowired AuthenticationManager必须通过AuthenticationConfiguration获取Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception { return configuration.getAuthenticationManager(); }3.5 处理器认证失败与权限不足的JSON返回默认情况下Spring Security在认证失败时会返回302跳转到登录页权限不足时返回403页面前端分离项目里这显然是没法用的。改写方式就是自定义AuthenticationEntryPoint和AccessDeniedHandler。Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); } }Component public class RestAccessDeniedHandler implements AccessDeniedHandler { Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:403,\message\:\无权限访问\}); } }这两个处理器分别在什么情况下触发要分清楚。AuthenticationEntryPoint处理的是未认证的请求比如没登录就访问受保护接口AccessDeniedHandler处理的是已认证但没有对应角色的请求比如普通用户去访问管理员接口。两者触发的场景不同配置错会导致排查问题走弯路。4. UserDetailsService与用户认证流程过滤器里调用了UserDetailsService这个接口Spring Security用来加载用户信息。自定义实现时按照实际业务逻辑从数据库查询用户同时把用户角色一并加载出来。Service public class CustomUserDetailsService implements UserDetailsService { Resource private UserMapper userMapper; Resource private RoleMapper roleMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user userMapper.findByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } ListString roleCodes roleMapper.findRoleCodesByUserId(user.getId()); ListSimpleGrantedAuthority authorities roleCodes.stream() .map(code - new SimpleGrantedAuthority(ROLE_ code)) .collect(Collectors.toList()); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), user.getEnabled(), true, true, true, authorities ); } }注意角色权限的前缀。Spring Security在hasRole(ADMIN)这种写法下底层实际校验的权限名是ROLE_ADMIN所以数据库里存的角色编码如果不是带前缀的返回时就要把ROLE_拼接上。如果用的是hasAuthority(ADMIN)这种校验方式就不需要加前缀两种写法要选一套用。同个项目里混用两套命名会出现你明明给用户设了角色但访问时报403的情况。登录认证的完整流程走一遍是这样的前端把用户名密码以JSON格式POST到/api/auth/loginSpring Security的AuthenticationManager调用ProviderManager找到支持UsernamePasswordAuthenticationToken的DaoAuthenticationProviderDaoAuthenticationProvider调用UserDetailsService.loadUserByUsername拿到用户信息内部通过PasswordEncoder匹配前端传的明文密码和数据库存的加密密码匹配成功生成Authentication对象返回给ControllerController拿到用户信息生成JWT返回给前端这段流程里密码加密方式是绕不开的话题。现在新项目基本都用BCryptPasswordEncoder它是自适应哈希算法每次加密同一个明文得到的密文不同即使数据库泄露用彩虹表也撞不出来。Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }用户注册时加密密码user.setPassword(passwordEncoder.encode(user.getPassword()));登录时DaoAuthenticationProvider会自动调用matches方法做校验不需要手动调用。如果把SecurityConfig里的PasswordEncoder定义漏了启动时就会报There is no PasswordEncoder mapped for the id null这个坑特别常见。5. 跨域配置CORS与Spring Security的坑前后端分离项目里CORS跨域资源共享是绕不开的话题。前端跑在8080端口后端跑在8081端口两边一交互浏览器就把请求拦了。Spring Security的CORS配置有两个层面Spring MVC的CORS配置和Spring Security层面的CORS配置。只配其中一个有时候会出问题。推荐的做法是定义一个CorsConfigurationSource的Bean然后在SecurityFilterChain里通过.cors(Customizer.withDefaults())启用。这样Security过滤器链会把CORS处理交给这个Bean执行。Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config new CorsConfiguration(); config.setAllowedOrigins(List.of(http://localhost:8080)); config.setAllowedMethods(List.of(GET, POST, PUT, DELETE, OPTIONS)); config.setAllowedHeaders(List.of(*)); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return source; }setAllowedOrigins里要注意一个细节当setAllowCredentials(true)时setAllowedOrigins不能使用*通配符否则浏览器会直接拒绝请求。必须把允许的前端地址一项项写清楚。如果前端地址不固定比如多环境部署可以用allowedOriginPatterns(*)替代替。关于预检请求OPTIONS也是一个高频问题。浏览器在发送跨域的POST、PUT、DELETE请求前会先发一个OPTIONS请求探测服务器是否允许跨域。如果你的Spring Security配置把OPTIONS请求拦截了前端就会看到CORS error。处理办法有两种一是在authorizeHttpRequests里放行OPTIONS请求二是通过CORS配置让Spring Security在过滤器层面直接处理预检请求。上面给的CorsConfigurationSource配置方法已经能处理掉OPTIONS不需要额外放行。端口不一样、域名不一样、用Nginx转发导致的前后端地址变化都可能让CORS配置失效。建议排查时先看浏览器控制台的具体报错信息是No Access-Control-Allow-Origin header is present还是Invalid CORS request两者对应的处理方向完全不同。6. 接口权限精细化设计authorizeHttpRequests里的配置只是第一层保护真正的权限控制往往需要细化到具体接口。以典型的业务系统为例/api/auth/login、/api/auth/register完全公开/api/articles/**GET请求公开POST/PUT/DELETE需要登录/api/user/**需要登录登录用户本人可以访问/api/admin/**需要管理员角色/api/system/**需要管理员角色且部分接口还需要更细粒度的权限点这种场景下除了在SecurityConfig里做粗粒度的URL匹配再配合方法级的PreAuthorize注解RestController RequestMapping(/api/admin) public class AdminController { GetMapping(/statistics) PreAuthorize(hasRole(ADMIN)) public Result? statistics() { // ... } DeleteMapping(/user/{id}) PreAuthorize(hasAuthority(user:delete)) public Result? deleteUser(PathVariable Long id) { // ... } }注意使用PreAuthorize之前必须在配置类上加上EnableMethodSecurity注解Spring Security 5.6版本之前是EnableGlobalMethodSecurity(prePostEnabled true)。很多同学忘记加结果接口上的权限注解全部失效还以为是配置写错了。权限模型的设计上有一种常见的区分hasRole校验的是粗粒度的角色比如管理员、普通用户hasAuthority校验的是细粒度的权限点比如删除用户、导出报表。实际项目中合理的做法是把权限点挂在角色下面用户拥有多个角色角色拥有多个权限点这样权限的扩展性会好很多。做了权限点之后登录签发Token的时候可以把权限点也放进去也可以每次解析Token后从数据库重新加载两种方式各有取舍。JWT直接携带权限点省了一次查询但用户在Token有效期内权限变更旧Token依然有效要等Token过期才生效。从数据库重新加载权限变更立即生效但每个请求多一次查询。再提醒一次如果选择从数据库加载记得把用户状态纳入判断用户被禁用后loadUserByUsername里要做校验不能让他继续访问接口。7. 常见高频问题与排查经验这里整理一张排查表和几个亲身踩过的坑。现象原因解决方案请求被返回401前端没有收到JSON格式提示AuthenticationEntryPoint没有生效走了默认的Http403ForbiddenEntryPoint或跳转逻辑检查SecurityConfig中exceptionHandling是否配置正确返回403但明明用户有对应角色角色命名不带ROLE_前缀hasRole校验失败统一在返回authorities时加ROLE_前缀POST请求报403 CSRF没有关闭CSRF配置csrf(AbstractHttpConfigurer::disable)前端跨域请求被拦CORS配置没有接入Security过滤器链定义CorsConfigurationSource Bean并启用cors携带Token请求还是401Token没有被JwtAuthenticationFilter解析检查Token的header名称是否用了Authorization值是否正确带Bearer前缀使用WebSecurityConfigurerAdapter编译失败Spring Security 5.7已废弃该类改用SecurityFilterChain组件式配置权限注解不生效缺少EnableMethodSecurity注解添加该注解JWT创建失败提示密钥太短HS256算法密钥要求256位换一个32字节以上的随机字符串坑一Bearer前缀漏了前端传Token时请求头一般是Authorization: Bearer xxxxxx服务端解析时要先判断前缀是否存在再截取。有的前端开发会直接把Token裸放在header里没有Bearer前缀后端解析就失败。规范做法是前后端约定统一格式过滤器里兼容两种格式就更稳妥。坑二SecurityContext跨请求丢失无状态Token方案下请求结束后SecurityContext就没了。每个请求进来都通过JwtAuthenticationFilter重新构建Authentication。有同学把用户信息塞进Session里又同时用了无状态策略Session数据根本没存于是后续请求都拿不到用户信息。定位这类问题时先在JwtAuthenticationFilter里加一行日志打印解析出来的用户名确认过滤链是否走了再往下查。坑三放行规则写糙了requestMatchers(/api/**).permitAll()这种写法太粗暴等于全放行Security形同虚设。需要放行的接口要按粒度列清楚如登录、注册、验证码、公开资讯的读接口等。凡是拿不准该不该放行的一律不给放行。坑四鉴权顺序反了在过滤器里先判 Token 再走后续逻辑这个顺序不能乱。如果Token校验失败就直接放行受保护接口会在后续的过滤器里抛出异常但此时异常信息不是统一的JSON结构前端拿到的提示和登录接口返回的格式对不上。每个过滤器处理完自己的逻辑后记得调用filterChain.doFilter把请求继续往下传。坑五异常被全局处理器吞了在RestControllerAdvice里定义了拦截Exception的全局处理器时注意AuthenticationException和AccessDeniedException很可能在进入Controller之前就抛出了全局异常处理器未必能拿到。所以这两类异常的JSON返回放在Security层的EntryPoint和DeniedHandler是正确的位置不要混合使用。8. 登出逻辑与Token过期处理登出在前后端分离项目里和传统模式的做法完全不同。传统模式服务端有Session登出时销毁Session即可无状态Token方案下Token在客户端保存服务端登出只是“通知”客户端丢弃Token服务端自己并没有太多事情要做。一个合规的登出流程设计是这样前端调用/api/auth/logout将当前Token传给后端后端把这个Token加入黑名单缓存可用Redis实现黑名单有效期设置为Token剩余有效时间前端收到200后清空本地存储的Token后续请求中JWT过滤器先查黑名单发现Token在黑名单中直接按无效Token处理这里有一个取舍。如果项目对登出的即时性要求不高也可以什么都不做直接由前端删除Token因为Token本来就是无状态的服务端没有状态可清。但是安全性要求较高的系统里用户可能因为账号被盗等原因需要立即失效Token那就要做黑名单。黑名单实现起来也很简单在JwtAuthenticationFilter里增加一段判断if (token ! null !tokenBlacklistService.isBlacklisted(token) jwtTokenProvider.validateToken(token)) { // 正常认证逻辑 }Token过期的话前端请求时会收到401此时前端应该根据这个状态统一做跳转登录页的逻辑。比较好的做法是在前端请求拦截器里统一判断请求返回401时清除本地登录信息、跳转登录页并提示用户登录已过期。不要等到每个接口单独处理401会重复写很多代码。另一个实践细节业务系统中Token过期时间一般设置为120分钟左右有效期太短用户用起来烦太长又不安全。如果要做“记住我”的功能可以用双Token方案一个短期Access Token用于日常请求一个长期Refresh Token用于续期。Access Token过期后前端用Refresh Token调续期接口换新的Access Token。这样既保证了安全性又兼顾了体验。但双Token方案的复杂度比单Token高不少尤其要考虑Refresh Token要不要旋转、被重复使用时怎么处理项目起步阶段不建议直接上先用单Token跑通后续有需要再演进。9. 一个小技巧调试Spring Security过滤器链最后分享一个调试技巧。Spring Security的过滤器链是层层嵌套的出了问题不好定位是哪个环节拦截的。新建配置时可以把日志级别打开logging: level: org.springframework.security: DEBUG开启后能看到每个请求经过的过滤器、SecurityContext的创建和销毁信息对于快速定位“我这请求到底卡在哪一步”特别有效。还有SecurityContextHolderFilter、AnonymousAuthenticationFilter这些内置过滤器的执行都会被打印出来。排查完记得把日志级别调回INFODEBUG级别的日志量非常大生产环境开这个会刷屏。再有一个最直接的经验拿到401先确认过滤器顺序拿到403先确认角色命名。这两个判断能解决绝大多数Spring Security集成问题。开头说的那个用WebSecurityConfigurerAdapter编译失败的读者换成上面这种配置方式后项目半小时就跑通了。按这套思路走前后端分离项目接入Spring Security会顺很多。
阅读完成 · 觉得有帮助?