1. 内容整体设计与思路拆解
1.1 为什么都要学Spring Security
做Java后端的人,迟早要过Spring Security这一关。不管你是在写多商户跨境商城、大学生就业推荐系统,还是给第三方提供接口,用户认证和权限控制永远是绕不开的课题。我最早接触Spring Security时也是一头雾水,因为它的过滤器链设计、前后端分离下的Token方案、以及那套抽象到让人怀疑人生的Bean配置,学习曲线确实不友好。但踩过坑之后再看,它其实是Spring Boot生态里最值得投入时间去啃的框架之一。
很多人会问:我自己写拦截器加Session不行吗?当然行,做一个Demo绰绰有余。但一旦系统膨胀到多商户、多角色、细粒度权限控制的规模,自己从头造轮子的代价会非常高。Spring Security的优势在于它把认证、授权、会话管理、密码加密、CSRF防护、跨域处理、OAuth2社交登录等安全能力都模块化了。你不需要全都懂,但你可以按需组合,并且它已经过大量生产环境的验证,坑比你自己的少得多。
1.2 学习路径要怎么规划
我见过的学习路径有两种偏差:一种是直接抄一段配置就跑,只求接口能调通;另一种是把官方文档从头啃到尾,结果越学越迷。前者的问题是换一个场景就不会写了,后者的问题是沉没在概念海洋里迟迟落不了地。
这里我给出一条更适合实际开发的路径,分四步走。第一步,先理清Spring Security的过滤器链模型。你可以把它理解成一道多层的安检闸机,请求进来之后层层过检,只要有一层不放行或认证失败,请求就到不了你的Controller。第二步,搞懂Authentication对象在认证通过前后分别长什么样。认证前它只有principal(用户标识)和credentials(密码凭证),认证通过后会被填充完整的用户信息、权限列表,Session或Token里存的就是这个对象。第三步,做一套最小可用的表单登录配置,理解认证管理器AuthenticationManager是如何协调ProviderManager和UserDetailsService一起工作的。最后一步,再拿JWT这种无状态Token方案去替换默认的Session机制。这一整套走下来,你对Spring Security的认知就会比较立体,而不是只有一堆注解碎片。
关于工具版本,我建议新项目直接上Spring Boot 3.x + Spring Security 6.x,这已经是当前的主线版本。网上大量的旧教程基于Spring Security 5.x甚至4.x,其中lambda配置风格和几个废弃API的差异不小,照着抄容易报错,看的时候要特别注意文章的版本标题。
2. 核心细节解析与实操要点
2.1 过滤器链机制到底是怎么一回事
Spring Security的核心是一条过滤器链,主线叫FilterChainProxy,它内部按顺序注册了多个SecurityFilter。每个过滤器负责一件事:有的处理CSRF校验,有的处理登录请求,有的生成或者校验Session,有的检查当前用户是否已经认证。链式设计的妙处在于,一个请求只要被一个过滤器拦住并抛出异常,整个链路就中断了,根本到不了后面的环节。
实际操作中,你不太需要关心链上所有过滤器的实现顺序,但有一个关键点必须记住:自定义过滤器必须知道自己在链上的相对位置。比如你要做Token认证过滤器,通常要加在UsernamePasswordAuthenticationFilter之前,因为你要先把Token解析成Authentication对象放进去,后面的授权过滤器才能判断用户有没有权限访问某个接口。
你可以把这条链想象成景区入园的几道闸机。第一道闸机检查你有没有带违禁品(CSRF校验),第二道闸机检查你的票是否有效(认证),第三道闸机检查你有没有预约某个特定场馆(授权),全部通过了才能进到园区内部,也就是你的Controller。这样理解过滤器链就会形象很多。
2.2 AuthenticationManager、ProviderManager与AuthenticationProvider的分工
这三个类的关系经常把人搞晕。AuthenticationManager是整个认证流程的入口接口,它的实现类是ProviderManager。ProviderManager内部维护了一个AuthenticationProvider列表,通过迭代这些Provider来尝试认证。每个Provider负责不同类型的认证逻辑:DaoAuthenticationProvider处理用户名密码登录,JwtAuthenticationProvider处理Token登录,OAuth2LoginAuthenticationProvider处理第三方授权码登录。一个Provider不行就换下一个,成功则返回一个已认证的Authentication对象,全部失败就抛出异常。
这里有个实践建议:如果你要对接多套用户来源,比如商户端和买家端分属不同数据表,不要在一个UserDetailsService里写一堆if-else。更好的方式是配置多个AuthenticationProvider,每套Provider绑定独立的UserDetailsService和密码编码器,这样扩展新用户源时只需要新增Provider,不需要动原有代码。
2.3 SecurityContext与SecurityContextHolder的关系
SecurityContextHolder是Spring Security的“全局背包”,默认基于ThreadLocal实现,每个线程持有自己的SecurityContext。SecurityContext里装的就是当前请求的Authentication对象。认证通过后,你就可以在任意地方通过SecurityContextHolder.getContext().getAuthentication()拿到当前的用户信息。
需要注意两点。第一,在异步处理的情况下,ThreadLocal不会自动传递,子线程里拿不到认证信息,需要手动做上下文传播。第二,在Web环境中,Spring Security的过滤器会在每次请求结束时清空SecurityContextHolder,避免线程池复用导致用户信息串到下一个请求中,这是默认的MODE_THREADLOCAL策略。如果你用了虚拟线程或者某些响应式场景,要特别留意上下文传播策略的调整。
3. 实操过程与核心环节实现
3.1 最小可用的依赖引入与基础配置
我直接给出一版Spring Boot 3.2 + Spring Security 6.2的最小依赖配置。先引入两个核心依赖,我这里以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>引入依赖后,只需要这样一段配置就能让所有接口进入认证保护状态:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/public/**").permitAll() .anyRequest().authenticated() ) .formLogin(form -> form.disable()) .httpBasic(basic -> basic.disable()); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails user = User.withDefaultPasswordEncoder() .username("admin") .password("admin123") .roles("ADMIN") .build(); return new InMemoryUserDetailsManager(user); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }这段配置的含义是:关闭CSRF,放行/api/public/**下的所有请求,其余请求全部需要认证。关闭表单登录和HTTP Basic认证,因为我们走的是接口认证方案。这个配置本身不完整,直接照搬到生产环境会有问题,因为还未配置实际的无状态Token认证过滤器,但作为学习第一步,它用最少的代码让你看到了SecurityFilterChain长什么样。
3.2 基于数据库的用户认证实现
内存用户只适合Demo,真实项目里用户信息必然在数据库。这里补充一套基于数据库的用户认证方案。首先定义用户表对应的实体类和Mapper,这里以Spring Boot + MyBatis组合为例,这也是目前国内Java开源项目中非常常见的搭配。
用户表结构设计上,我建议至少包含id、username、password、enabled四个字段。enabled字段对应UserDetails接口的isEnabled方法,用来实现账号禁用。角色和权限可以另外用user_role、role_permission关联表来维护,但学习阶段可以先在用户表里加一个role字段,用逗号分隔角色,比如ROLE_ADMIN,ROLE_USER。
然后实现UserDetailsService接口:
@Service public class CustomUserDetailsService implements UserDetailsService { private final UserMapper userMapper; public CustomUserDetailsService(UserMapper userMapper) { this.userMapper = userMapper; } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDO userDO = userMapper.findByUsername(username); if (userDO == null) { throw new UsernameNotFoundException("用户不存在"); } List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(userDO.getRole())); return new User(userDO.getUsername(), userDO.getPassword(), authorities); } }在过滤器中,DaoAuthenticationProvider会调用这个UserDetailsService来加载用户信息,然后用PasswordEncoder比对传入的密码和数据库中的密码哈希。这里最重要的是数据库存储的密码必须是用BCrypt这类算法加密过的,绝不能存明文。BCryptPasswordEncoder的encode方法每次生成的哈希值不同,因为内部会生成随机盐,验证时用matches方法比对即可。
3.3 前后端分离下的JWT无状态认证实现
现在前后端分离已经是标配,Session方案在跨域、移动端场景下越来越力不从心。JWT方案的基本思路是:认证成功后服务端签发一个签名Token,客户端后续请求在Header中携带这个Token,服务端通过自定义过滤器解析并校验Token,将用户信息放入SecurityContext中。
先引入JWT库依赖,我常用的是jjwt:
<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> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency>然后写一个Token生成和校验的工具类。密钥我这里用HMAC-SHA256方式生成签名,实际生产环境密钥要通过环境变量注入,不能硬编码在代码中:
@Component public class JwtTokenUtil { private final SecretKey key = Keys.hmacShaKeyFor("your-256-bit-secret-key-change-it".getBytes(StandardCharsets.UTF_8)); private final long expiration = 86400000L; public String generateToken(String username, List<String> roles) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expiration); return Jwts.builder() .setSubject(username) .claim("roles", roles) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(key, SignatureAlgorithm.HS256) .compact(); } public String getUsernameFromToken(String token) { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody() .getSubject(); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }接着是核心的Token认证过滤器。这个过滤器需要继承OncePerRequestFilter,确保每个请求只执行一次:
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); if (jwtTokenUtil.validateToken(token)) { String username = jwtTokenUtil.getUsernameFromToken(token); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } } filterChain.doFilter(request, response); } }最后把这个过滤器注册进SecurityFilterChain中:
http .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/login", "/api/public/**").permitAll() .anyRequest().authenticated() );注意addFilterBefore是将自定义过滤器插入到UsernamePasswordAuthenticationFilter之前执行。这个位置选择是有讲究的,因为后续的授权过滤器要依赖当前SecurityContext中是否有Authentication对象,所以解析Token的操作需要提前完成。如果放得太靠后,授权过滤已经执行完,你的认证信息就无法生效。
3.4 登录接口与认证入口的具体实现
有了过滤器之后,还需要一个实际的登录接口来签发Token。这里我们直接注入AuthenticationManager来驱动认证流程:
@RestController @RequestMapping("/api/auth") public class AuthController { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) { try { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword()) ); SecurityContextHolder.getContext().setAuthentication(authentication); // 从认证对象中提取角色 List<String> roles = authentication.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); String token = jwtTokenUtil.generateToken(loginRequest.getUsername(), roles); return ResponseEntity.ok(new LoginResponse(token, loginRequest.getUsername(), roles)); } catch (BadCredentialsException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("用户名或密码错误"); } } }为了让AuthenticationManager这个Bean可用,需要在SecurityConfig中配置:
@Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); }AuthenticationConfiguration会读取你配置的UserDetailsService和PasswordEncoder,自动组装好ProviderManager。如果你的用户来源是唯一的,用这个默认逻辑就够了。
3.5 基于注解的方法级权限控制
在接口配置之外,Spring Security还支持方法级权限控制。开启全局方法安全校验:
@EnableGlobalMethodSecurity(prePostEnabled = true)Spring Boot 3.x中这个注解的位置也可以放在SecurityConfig上,但实际上Spring Security 6.x里更推荐使用@EnableMethodSecurity。可以用以下注解做细粒度控制:
@Service public class OrderServiceImpl implements OrderService { @PreAuthorize("hasRole('ADMIN')") public void deleteOrder(Long orderId) { // 只有管理员才能删除订单 } @PreAuthorize("hasAnyRole('ADMIN', 'MERCHANT')") public void updateOrder(Order order) { // 管理员和商户都能更新订单 } @PostAuthorize("returnObject.ownerUsername == authentication.name") public Order getOrder(Long orderId) { return orderMapper.findById(orderId); } }@PreAuthorize在执行方法前校验权限,适合删除、修改等操作。@PostAuthorize在执行方法后校验,适合数据归属类的权限控制,比如普通用户只能看自己的订单,方法是先查出数据再判断归属,不满足就抛AccessDeniedException。
实际项目中我一般优先使用@PreAuthorize,因为它的语义更清晰,执行前就拦住了,少一次无谓的数据库查询。@PostAuthorize那种场景,如果能在SQL层面加归属条件,其实比代码层面二次校验效率更高。
3.6 多商户跨境商城场景下的权限模型扩展
结合热词中提到的多商户跨境商城,这类系统的权限模型有其特殊性。普通单用户系统只需区分管理员和用户,但多商户系统里还存在商户、店员、物流商等多个主体,每个主体管理不同的资源。
比较常见的方案是RBAC模型扩展一层“租户”维度。用户表增加tenant_id,角色表也跟租户关联。Spring Security的authority只表现角色,不表现数据归属。为了在接口层区分数据归属,我会额外写一个自定义的权限表达式:
public class CustomSecurityExpressionRoot implements MethodSecurityExpressionOperations { // 自定义hasTenantAccess方法,校验当前用户和当前请求资源是否属于同一租户 }在Controller里可以这样用:
@PreAuthorize("@tenantSecurity.check(#merchantId)") public ProductVO getProduct(Long merchantId) { // 当前登录用户必须属于这个商户才能查看商品 }这种方式比把所有商品查询都放在一个接口中再通过if-else判断要清晰得多。权限校验的前置化,不仅让代码更安全,也让接口语义更干净。
4. 常见问题与排查技巧实录
4.1 登录接口反复重定向或返回403
这是新手遇到最多的问题。出现这个现象通常有两个原因:一是CSRF防护没有关闭,表单登录模式下Spring Security默认会校验CSRF Token,你的Ajax请求没有携带Token就会被拦截。二是未认证的请求被默认重定向到了/login页面,但你的前端并不存在这个页面。
排查步骤先看浏览器Network面板的请求状态码。如果是302,说明是认证入口重定向;如果是403,优先检查CSRF配置。前后端分离项目中,我通常建议在无状态接口认证方案下直接关闭CSRF,因为CSRF防护的核心机制依赖会话Cookie,一旦改成Token方案,CSRF的意义就大幅削弱了。
API的安全不能只靠一个机制兜底,但CSRF在无状态场景下的确不是主要防线,更重要的还是Token的加密强度、过期策略和HTTPS传输。如果你的接口涉及Cookie认证又要跨站请求,CSRF还是不能随便关的。
4.2 明明数据库密码正确却提示用户名或密码错误
这个坑十有八九出在密码编码器不匹配上。Spring Security 6.x默认使用DelegatingPasswordEncoder,它要求数据库中的密码带有编码器前缀,比如{bcrypt}明文哈希。如果你的数据库密码是直接存储的{bcrypt}前缀的BCrypt哈希,而你没有显式配置PasswordEncoder,或者你配置的编码器与存储格式不一致,验证就会失败。
解决方法是明确指定全局唯一的密码编码器:
@Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }确保在UserDetailsService中返回的密码就是BCrypt哈希,而注册用户时用passwordEncoder.encode(rawPassword)存储。我这里再强调一遍,自己写代码注册用户的逻辑中,一定不能直接存原始密码。我见过不止一个项目上线后数据库密码全明文的事故,被脱库之后相当于把用户账号拱手送人。
4.3 自定义过滤器不生效或者重复执行
过滤器不生效最常见的原因是SecurityFilterChain中的配置没有生效。Spring Boot 3.x中如果你不小心定义了多个SecurityFilterChain Bean,某些路径会被不同链路处理,配置隔离起来很麻烦。确保你只需要一条链路的时候,所有相关配置都放在同一个SecurityFilterChain中。
重复执行的问题则多半出在Filter的注册方式上。如果你既在SecurityFilterChain里addFilterBefore,又在WebMvcConfigurer或Servlet容器里注册了Filter Bean,同一个请求可能会被过滤两次,JWT解析逻辑也就执行两次。虽然幂等设计下没有大问题,但浪费不必要的CPU和内存。我的做法是:需要被Spring Security管理的过滤器,只加在SecurityFilterChain中,不加@Component注解。不要两种方式混用。
4.4 使用@PreAuthorize但注解不生效
注解不生效,先在启动类或者配置类上确认是否加了解锁方法级安全的注解。Spring Security 6.x使用@EnableMethodSecurity,旧版是@EnableGlobalMethodSecurity。如果你的项目还停留在旧版本依赖上,同时写的是新注解,自然没反应。升级Spring Boot到3.x之后,注意检查所有旧的security配置类是否还能编译通过,废弃API会直接暴露出来。
另一种情况是方法调用来自类内部,比如一个Service方法调用同类中的另一个被@PreAuthorize标注的方法,注解不会触发,因为Spring AOP代理默认只拦截外部调用。解决办法是把需要注解保护的方法拆分到不同的Bean中,或者通过AopContext.currentProxy()获取代理对象来调用。
4.5 JWT过期后用户还在页面上“看似已登录”
这是JWT方案容易忽略的体验问题。Token无状态意味着服务端无法主动注销它,过期之前客户端一直能用。如果你希望用户登出后Token立刻失效,单靠JWT办不到。
实际项目中通常配合Redis做短期Token黑名单,或者改用短Token加RefreshToken双Token方案。AccessToken设短过期时间(比如15分钟),RefreshToken设长过期时间(比如7天),AccessToken过期后用RefreshToken换新。这样既控制了Token泄露的风险窗口,又不用太频繁地让用户重新登录。这个方案会增加一些实现复杂度,但多商户商城这类生产级系统,这点复杂度是值得承受的。
4.6 常见问题速查表
为了方便查阅,我把上面的高频问题整理成了一张速查表:
| 问题现象 | 可能原因 | 排查/解决方向 |
|---|---|---|
| 请求302到/login | 未认证请求触发了表单登录入口 | 确认是否调用login接口;无状态认证下关闭formLogin |
| 请求403无响应 | CSRF拦截或权限不足 | 检查CSRF配置;检查当前用户authorities是否满足要求 |
| 密码正确但认证失败 | PasswordEncoder不匹配 | 统一使用BCryptPasswordEncoder,密码带编码器前缀检查 |
| 自定义过滤器不生效 | 过滤器没有加入链中 | 确认在filterChain中addFilterBefore方法未遗漏 |
| @PreAuthorize不生效 | 缺少@EnableMethodSecurity | 检查注解,并确认方法是外部调用 |
| Token能解析但接口401 | 过滤器未将认证信息写入SecurityContext | 检查SecurityContextHolder赋值逻辑是否被异常打断 |
| 权限数据修改后不生效 | 用户权限被缓存或存在ThreadLocal残留 | 重启或确认清理逻辑;避免在请求间复用SecurityContext |
5. 扩展:Spring Boot Admin监控中的安全集成
热词列表中出现了Spring Boot Admin,这个工具也和安全配置强相关。很多人集成Spring Boot Admin时遇到一个典型问题:监控页打不开,或者打开后需要认证但不知道账号密码。
Spring Boot Admin自身就是一个Spring Boot应用,服务端可以接入Spring Security来保护监控端点。被监控的客户端需要暴露actuator端点,同时设置管理端和客户端的认证信息。如果监控页面要显示完整的JVM、线程、日志等敏感信息,这些端点绝不能裸奔。建议在客户端配置:
management.endpoints.web.exposure.include=* management.endpoint.health.show-details=when-authorized spring.security.user.name=admin spring.security.user.password=admin123同时服务端依赖spring-boot-admin-server-ui和spring-boot-admin-server,并在配置中加入客户端的凭据信息。在实践中,我习惯额外引入spring-boot-starter-actuator并配置好账号,否则监控页能显示但接口数据加载不出来,排查起来容易走弯路。
如果你在Spring Security配置中给所有请求加上了认证保护,需要放行Spring Boot Admin的监控端点,否则监控服务连不上客户端。这里需要你单独定义一套匹配规则来放行相关路径。
6. 实操中的避坑清单与学习方法建议
6.1 我踩过的坑
先说说密码加密的坑。有一个阶段我把DaoAuthenticationProvider默认使用的PasswordEncoder与数据库里存的密码哈希算法搞混了,导致线上用户无法登录,排查了整整半天。后来我养成了一个习惯:注册用户时显式指定PasswordEncoder,登录认证路径上确保全局只有一个编码器Bean。这样即使出错,问题也就锁死在Bean配置上。Spring Security的DelegatingPasswordEncoder默认行为会检查密码哈希的前缀,比如存储格式是{bcrypt}xxxx,如果你删除前缀或换算法,匹配逻辑就完全变了。
再说一下跨域的坑。前端服务跑在8000端口,后端跑在8080端口,直接请求接口会报跨域错误。Spring Security的CORS配置和Spring MVC的CORS配置容易混淆。最佳实践是在SecurityFilterChain中配置cors:
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))自定义CorsConfigurationSource来放行允许的域名、Header和方法。CORS配置不会浪费你太多时间,但不配置的话,前后端联调阶段会让你怀疑人生。
6.2 给新手的三个学习建议
第一个建议,先实现一套完整的用户认证过程,哪怕只是InMemoryUserDetailsManager,也要把注册、登录、修改权限、退出登录这个流程跑通。只有跑通了完整闭环,你才会真正理解Token是从哪里来的,SecurityContext什么时候被写入,什么时候被清理。
第二个建议,每次看别人项目的代码,不要只盯着配置类看,要看过滤器链中的每一个关键节点。比如别人加了一个JwtAuthenticationFilter,你要追一下它是在哪个过滤器之前插入的,为什么要放在那里。理顺这个逻辑之后,你遇到需求新增过滤器也不会发怵。
第三个建议,学会读官方文档的迁移指南。Spring Security 5.x到6.x的变更版本里,很多类名、方法签名都变了。你不会永远停留在一个版本上,企业项目迟早要升级。平时养成看迁移指南的习惯,升级的时候会少掉特别多头发。
6.3 从学习到落地的路径
如果你现在的目标是完成课程设计或者一个真实项目,我建议从Spring Boot脚手架生成器创建一个最小项目,然后按顺序做四件事:先把用户表和角色表建好,用Spring Boot + MyBatis做一套UserDetailsService实现;接着配置基于数据库的登录认证,跑通Session模式;然后升级到JWT无状态认证,写一个自定义过滤器;最后接入方法级权限控制和Swagger,让接口文档也能识别Token。
这套路径做下来,你不仅掌握了Spring Security的基础核心,还顺手掌握了一整套前后端分离项目的安全标配。以后不管写大学生就业推荐系统,还是参与多商户跨境商城源码的开发,都不需要再从零学起,更多只是数据模型层面的调整。
我自己在写完JWT方案后又把Session方案和Redis缓存方案各做了一遍对比测试,体会很深。安全设计不是越复杂越好,而是越合适越好。小系统用Session加简单拦截器就能用,多租户、高并发系统则需要Token加细粒度权限模型。Spring Security给了你选择的空间,关键在于你理解每条技术路线背后的本质。