ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring Security前后端分离认证授权实战:JWT+过滤器链完整指南

Spring Security前后端分离认证授权实战:JWT+过滤器链完整指南 Spring Security 超详细使用教程从零搭建前后端分离认证授权体系先聊点实在的。Spring Security 是 Java 生态里绕不开的一座大山很多人在初学阶段被它那套过滤器链和一堆抽象概念劝退尤其是在前后端分离的项目里默认的登录页、Session 跳转逻辑完全派不上用场配起来总觉得别扭。我自己也经历过那个阶段照着网上的教程复制粘贴结果启动就报错要么是过滤器顺序不对要么是放行的接口没生效最后好不容易跑起来了又发现返回的 JSON 结构跟后端统一格式对不上前端根本没法解析。这篇文章就以一套完整的 springboot spring security 前后端分离项目为例把认证授权的整个链路拆开揉碎来讲。内容覆盖了从依赖引入、数据库表设计、JWT 工具类编写到 SecurityConfig 核心配置、自定义认证过滤器、动态权限加载再到实际部署时常见的坑。适合刚接触 Spring Security 的初学者也适合已经用了但总是似懂非懂、想系统理一遍的开发者。这里不会有教科书式的理论堆砌全部是实操过、验证过、可以照抄的代码和思路。1. 整体设计与思路拆解1.1 为什么前后端分离项目要换一套认证方案传统的 Spring Security 使用方式基于 Session 和 Cookie。用户在浏览器提交用户名密码后服务端会创建 Session并把 Session ID 通过 Cookie 下发给浏览器后续请求浏览器自动携带 Cookie服务端据此识别身份。这种方式在小规模纯服务端渲染场景下非常成熟但在前后端分离架构里存在几个先天问题。首先前端应用Vue/React和后端 API 往往是分开部署的甚至不在同一个域名下。此时跨域请求要携带 Cookie需要额外配置 secure、SameSite 等属性浏览器策略一收紧Cookie 就传不过去了。其次服务端存 Session 意味着要维护会话状态集群部署时还得引入 Redis 做 Session 共享复杂度直接翻倍。最后移动端 App 压根没有 Cookie 这个概念。所以前后端分离项目的主流方案是 Token 认证更具体地说是 JWTJSON Web Token。服务端在用户登录成功后签发一个自包含的 Token客户端存储并在每个请求的 Authorization 请求头里携带服务端解析验证签名即可识别用户无需存储任何会话状态天然支持水平扩展。这就是 Spring Security 需要定制改造的核心原因默认的登录流程和会话管理机制跟 JWT 无状态认证的思路是冲突的。1.2 分层解耦把 Security 配置和业务逻辑分开做这套方案时我习惯把整体结构拆成几个层认证过滤器负责拦截请求并解析 TokenSecurityConfig 负责装配过滤器链、指定哪些接口放行哪些需要认证用户服务负责从数据库加载用户和角色权限JWT 工具类负责 Token 的生成与校验。各层之间只通过接口交互出问题时排查边界非常清晰。这种分层思路背后是一条关键原则SecurityConfig 不直接写业务逻辑。我看过不少新手代码把 SQL 查询直接写在过滤器里或者把用户状态校验写进登录接口里结果就是业务一改安全配置全崩。更好的做法是让 Spring Security 的各个组件各司其职AuthenticationManager 管认证AccessDecisionManager 管授权数据查询统统交给 Service 层。1.3 这套方案的技术选型和适用场景技术栈锁定为 Spring Boot 2.7.x Spring Security 5.7.x JWT。选这个组合的原因很务实Spring Boot 2.7 是目前存量项目最多的版本SS 5.7 的 API 风格与后来大改的 6.x 有明显差异网上资料最丰富踩坑方案也最齐全。如果你用的 Spring Boot 3.x代码会有部分差异比如 javax 改成 jakarta、lambda 风格的配置是强制性的但核心思路完全一致稍作调整即可迁移。适用场景包括Vue/React Spring Boot 的前后端分离项目、微信小程序/App 的后端 API 服务、微服务网关的认证中心雏形。不适合的场景也有如果你做的还是服务端渲染项目或者对实时踢人、会话吊销有强需求建议继续使用 Session Redis 方案JWT 在这类场景下反而更难处理。2. 核心细节解析与实操要点2.1 工程环境准备依赖和版本一个都不能错先搭一个最基础的 Spring Boot 工程我习惯用 Maven 管理依赖结构如下父 pom 用 spring-boot-starter-parent 固定版本子模块按功能拆 controller、service、mapper、security、util 等包。这样即使后面模块膨胀代码也不至于乱成一锅粥。Maven 依赖组合parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies特别注意 jjwt 的依赖选择0.11.5 版本编程式 API 用得顺手但解析 Token 时必须同时引入 jjwt-impl 和 jjwt-jackson前者负责解析实现后者负责 JSON 序列化缺一个运行时就报 ClassNotFoundException。有些教程喜欢用 0.9.1 老版本可那个版本有严重的安全漏洞新项目千万别选。数据库层面我准备了四张表用户表 sys_user 存储账号密码和状态角色表 sys_role用户角色关联表 sys_user_role菜单权限表 sys_menu同时用 sys_role_menu 把角色和权限关联起来。这就是经典的 RBAC 模型Spring Security 的 UserDetails、GrantedAuthority 恰好能一一映射后面动态权限就是从这些表里查出来塞给框架的。建表语句这里给一个精简版做参考CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, status tinyint NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, PRIMARY KEY (id) ); CREATE TABLE sys_menu ( id bigint NOT NULL AUTO_INCREMENT, menu_name varchar(50) NOT NULL, perms varchar(100) DEFAULT NULL COMMENT 权限标识如 sys:user:list, PRIMARY KEY (id) );2.2 JWT 的结构原理和完整工具类实现JWT 本质上是一段被签名过的 JSON 数据由三部分组成Header 头部声明算法和 Token 类型Payload 载荷存放自定义声明用户 ID、用户名、过期时间等Signature 签名是用 Header 里的算法对前两部分加盐做哈希的结果。服务端收到 Token 后重新计算签名跟 Token 里带的签名比对如果一致就说明数据没有被篡改过。JwtUtil 工具类我写了下面这些方法generateToken 生成 Token、parseToken 解析载荷、isTokenExpired 判断过期、从 Token 中提取 username。核心点有两个过期时间是必须的不然 Token 一旦泄露就永远有效签名密匙至少要 256 位否则 HS256 算法运行时会抛 WeakKeyException。Component public class JwtUtil { Value(${jwt.secret}) private String secret; Value(${jwt.expiration}) private Long expiration; public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expiration)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody() .getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { return false; } } }配置放在 application.ymljwt: secret: your-secret-key-please-change-it-to-at-least-256-bits-long expiration: 86400000注意 secret 这个配置在真实项目里一定要放到环境变量或者配置中心里不能写死在 git 仓库否则等于把认证系统的钥匙交了出去。expiration 我示例里给的是 86400000 毫秒也就是 24 小时实际项目建议按业务场景调整后台管理系统可以 12 小时客户端 App 可以 7 天如果做了刷新 Token 机制短一点反而更安全。2.3 自定义 UserDetailsService对接数据库用户Spring Security 内部做用户名密码认证时需要从某个地方加载用户信息放到 UserDetails 对象里。默认实现是从内存里读我们得替换成从 sys_user 表查询同时把角色权限一并封装成 GrantedAuthority 集合。Service public class UserDetailsServiceImpl implements UserDetailsService { Resource private SysUserMapper userMapper; Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser user userMapper.selectByUsername(username); if (user null) { throw new UsernameNotFoundException(用户不存在); } ListGrantedAuthority authorities userMapper.selectPermsByUserId(user.getId()) .stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); return new User(user.getUsername(), user.getPassword(), authorities); } }这里有三个细节要处理好。第一数据库里存的密码必须是 BCrypt 加密后的密文不能是明文否则后面 PasswordEncoder 校验永远过不了。第二authorities 列表里的权限标识可以是任意字符串推荐直接使用权限码比如sys:user:add这样跟 PreAuthorize 注解里的表达式天然对齐。第三如果用户被禁用了可以在 UserDetails 对象的 enabled 字段上做文章Spring Security 在校验时会自动拒绝 disabled 的用户。3. 实操过程与核心环节实现3.1 SecurityConfig 核心配置过滤器链的装配艺术接下来是整个改造的核心SecurityConfig。说实话刚接触 FilterChain 那会儿人都是蒙的这么多过滤器、谁先谁后、怎么排序、哪个拦截哪个放行全靠死记硬背。这几年写下来我总结了一套自己的理解方式认证过滤器在最前面它尝试从请求中提取 TokenTemporary 的 OncePerRequestFilter 之后就是异常处理过滤器专门处理认证失败和权限不足的情况再往后就是 Spring Security 原有的 UsernamePasswordAuthenticationFilter但在前后端分离场景下我会把它 disable 掉因为登录逻辑已经改成自定义接口了。Configuration EnableWebSecurity public class SecurityConfig { Resource private JwtAuthenticationFilter jwtAuthenticationFilter; Resource private RestAuthenticationEntryPoint authenticationEntryPoint; Resource private RestAccessDeniedHandler accessDeniedHandler; Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(auth - auth .antMatchers(/api/auth/login, /api/auth/register).permitAll() .antMatchers(/doc.html, /webjars/**, /swagger-resources/**).permitAll() .anyRequest().authenticated() ) .exceptionHandling(ex - ex .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } }CSRF 防护在严格意义上应该在 Token 方案下关闭因为 JWT 不存在依赖 Cookie 的会话机制CSRF 攻击的土壤已经不存在了这也是行业内的默认共识。Session 创建策略设置成 STATELESS 两者本质是配套的告诉框架不要创建 Session也不要读取 Session。3.2 自定义登录接口手动调用 AuthenticationManager登录接口的核心逻辑如果不展开讲很容易被忽略。简单来说就是要拿到 AuthenticationManager然后调用它的 authenticate 方法做校验。Spring Security 的默认 AuthenticationManager 实现 (ProviderManager) 需要 DaoAuthenticationProvider 支持才能调用 UserDetailsService。注意这个 Provider 需要在 Spring 容器中暴露出来否则 SecurityConfig 里的 AuthenticationManager 就是个空壳。DaoAuthenticationProvider 注入 UserDetailsService 和 PasswordEncoderBean public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { return config.getAuthenticationManager(); } Bean public DaoAuthenticationProvider authenticationProvider(UserDetailsService userDetailsService) { DaoAuthenticationProvider provider new DaoAuthenticationProvider(); provider.setUserDetailsService(userDetailsService); provider.setPasswordEncoder(passwordEncoder()); return provider; }登录接口可以写成这样PostMapping(/login) public Result? login(RequestBody LoginRequest loginRequest) { UsernamePasswordAuthenticationToken token new UsernamePasswordAuthenticationToken( loginRequest.getUsername(), loginRequest.getPassword()); Authentication authentication authenticationManager.authenticate(token); String jwt jwtUtil.generateToken(authentication.getName()); return Result.success(new LoginResponse(jwt)); }跟默认表单登录相比这种做法的好处一目了然接口路径、入参格式、返回结构全部由后端定义前端拿来即用。没有 Session没有重定向401 就是 JSON200 就是 JSON一套逻辑通吃浏览器、小程序、App。3.3 JwtAuthenticationFilter每个请求的身份识别自定过滤器装在哪一步是关键中的关键。Spring Security 的过滤器链里有一个位置叫 UsernamePasswordAuthenticationFilter它在链中的顺序是有讲究的addFilterBefore把我们自定义的 JWT 过滤器放在它前面这样所有请求到达认证授权决策之前就已经把身份信息解析出来放到了 SecurityContext 里。到了授权那一步框架直接看 SecurityContext 里有没有 Authentication 对象有就是已认证没有就去走匿名认证逻辑。Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Resource private JwtUtil jwtUtil; Resource private UserDetailsServiceImpl userDetailsService; Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String authHeader request.getHeader(Authorization); if (authHeader ! null authHeader.startsWith(Bearer )) { String token authHeader.substring(7); if (jwtUtil.validateToken(token)) { String username jwtUtil.getUsernameFromToken(token); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }这段代码每一行都有讲究继承 OncePerRequestFilter 是为了保证每个请求只过滤一次避免转发时被重复执行Token 前面加了Bearer前缀是业界惯例跟 OAuth 2.0 的 Authorization 头格式保持统一WebAuthenticationDetailsSource().buildDetails(request)作用不大但能留个请求来源的 IP 信息以后审计排查用得上SecurityContextHolder默认策略是 ThreadLocal这意味着只要请求线程内先把认证信息放进去后面的 Controller、Service 都能取到当前用户。3.4 自定义异常处理返回统一 JSON 结构默认的 Spring Security 在处理未认证和权限不足时返回的是 HTTP 状态码和一段简单的错误响应响应体格式跟咱们项目的 Result 完全对不上。所以我要重写两个组件AuthenticationEntryPoint 负责处理未认证访问受保护资源的情况401AccessDeniedHandler 负责处理已认证但权限不足的情况403。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(JSON.toJSONString(Result.error(401, 未登录或登录已过期))); } } 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(JSON.toJSONString(Result.error(403, 没有访问权限))); } }搭配上一节 SecurityConfig 里的 exceptionHandling 配置这两个类就会在对应场景被自动调用。还有一个容易被忽略的点如果项目用了全局异常处理器 RestControllerAdvice注意它不会捕获 Filter 层抛出的异常因为 Filter 在 Spring MVC 的 DispatcherServlet 之前执行。所以认证/授权异常必须在 Filter 链内处理这也是这个设计的意义所在。3.5 方法级权限控制PreAuthorize 的实战姿势配置好 URL 级别的权限后业务层还有更细粒度的控制需求。比如用户列表接口GET /api/users只有具备sys:user:list权限的人才能访问那就在 Controller 方法上加上 PreAuthorize 注解开启方式是在 SecurityConfig 上加 EnableGlobalMethodSecurity(prePostEnabled true)。RestController RequestMapping(/api/users) public class UserController { GetMapping PreAuthorize(hasAuthority(sys:user:list)) public ResultListSysUserVO list() { return Result.success(userService.list()); } PostMapping PreAuthorize(hasAuthority(sys:user:add)) public Result? add(RequestBody SysUser user) { return Result.success(userService.add(user)); } }这里我习惯用hasAuthority(sys:user:list)而不是hasRole(ADMIN)。虽然两者功能类似但框架对 hasRole 有隐含的前缀处理如果你的角色标识不是以ROLE_开头会踩一个非常隐蔽的坑。用权限码做控制粒度更细、更直观角色只要关联了相应权限码就能访问对应接口改动角色权限时不需要改代码只改数据库。3.6 动态权限加载从数据库读取接口访问规则如果项目里的接口权限是写死在代码里的那每次权限调整都要改代码、重新部署非常痛苦。更好的方案是动态加载把 URL 和权限的映射关系存到数据库在过滤器链中动态判断当前请求的 URL 需要哪些权限。这张表就是我们项目的接口权限表结构大概是id、url、method、perms。Spring Security 留了一个扩展点叫 FilterInvocationSecurityMetadataSource负责从请求中分析出所需的权限集合再配合 AccessDecisionManager 做最终决策。实现这三个扩展点的工作量不小但对于复杂系统来说一步到位很值。如果项目权限模型比较简单用 PreAuthorize 完全够了这一点大家要按需选择。4. 常见问题与排查技巧实录4.1 登录接口返回 401过滤器把登录请求也拦截了这是遇到频率最高的一个问题。原因通常是 SecurityConfig 里 authorizeHttpRequests 的配置写成了anyRequest().authenticated()且没有放行登录路径。配置放行时要注意antMatchers(/api/auth/login).permitAll()必须写在anyRequest().authenticated()之前顺序错了规则会被后面的全量拦截覆盖。如果路径里带了项目前缀 context-path要确保放行路径带上完整前缀否则又是个隐性错误。另一个排查点CORS 预检请求 OPTIONS 被 Spring Security 拦截了。跨域情况下浏览器会先发一个 OPTIONS 请求探路这个请求不应该携带 Token如果被anyRequest().authenticated()挡住前端看到的报错就是 401。解决办法是在 SecurityConfig 里加一行http.cors()允许跨域然后在放行规则里显式放行 OPTIONS 请求或者直接通过 CorsFilter 配合进行处理。4.2 密码永远验证失败BCrypt 加密方式不一致使用 DaoAuthenticationProvider 时密码校验是交给 BCryptPasswordEncoder 完成的。如果数据库里存的密码是 MD5 加密或者干脆是明文authenticate 方法必然抛 BadCredentialsException。这种情况下要检查两点一是注册时密码是否通过 passwordEncoder.encode() 处理二是系统中是否有多套加密算法比如老系统用 MD5 迁移过来的数据。对于第二种情况可以自定义一个 PasswordEncoder 比较器或者做一个兼容多算法的解密逻辑但最稳妥的做法还是统一重置密码并全部走 BCrypt。另外要提醒的是BCrypt 算法每次生成的密文都不相同这是正常的因为盐不同。不要在测试时因为没有规律的密文就怀疑系统出 bug。4.3 明明登录成功了但每次请求还是 401这个问题排查点集中在 JwtAuthenticationFilter 的执行位置上。过滤器必须在安全上下文建立之后再校验 Token也就是 addFilterBefore(UsernamePasswordAuthenticationFilter.class) 这个位置如果写错了比如 addFilterAfter(LogoutFilter.class)就会导致过滤器链执行顺序异常Token 还没解析就已经走到了鉴权逻辑。这种情况我习惯在 doFilterInternal 里临时打一条日志打印 Authorization 头是否存在、Token 解析是否成功、Authentication 对象是否被放入 SecurityContext。确认是 Token 没解析出来还是 UsernamePasswordAuthenticationToken 构造时传入的 authorities 为空。authorities 如果是空集合框架会认为这个用户没有任何权限后续接口校验授权时全部 403表现也很像 401。4.4 自定义登录接口抛 403CSRF 惹的祸前后端分离项目改造初期最容易遇到的现象关闭了表单登录和 Session自定义/api/auth/login接口却一直返回 403。关键原因往往不是权限配置而是 CSRF 拦截器默认被启用。Spring Security 默认会为 POST 等状态改变请求校验 CSRF Token如果没在配置里显式关闭自定义登录接口会直接被拦截在 CSRF 过滤器一关。我个人的实践经验是只要确定整个项目使用 JWT 且不依赖浏览器 Cookie 存储会话状态就果断http.csrf().disable()。这也是绝大多数前后端分离项目的标准配置。如果由于特殊原因不能关闭则必须在登录接口和所有表单提交接口中携带 CSRF Token并配置 CSRF TokenRepository 为 Cookie 模式否则前端集成成本极高。4.5 权限标识在 PreAuthorize 中不生效这个问题大多是注解用错了。Spring Security 中hasRole(ADMIN)会让框架自动加上ROLE_前缀去匹配 GrantedAuthority也就是要求权限标识必须叫ROLE_ADMIN而hasAuthority(ADMIN)才会精确匹配ADMIN。如果两者混用就会出现明明在用户上赋予了 ADMIN 权限hasRole 却始终返回 false非常坑人。解决方案就一条固定你的权限模型统一的规则。如果你用权限字符串就全线 hasAuthority如果你用角色就必须确保数据库角色字段里带 ROLE_ 前缀。团队人多的时候把这一条写进开发规范能省下大量在群里嚎叫的时间。4.6 多个过滤器同时生效导致重复认证如果项目中既有 Spring Security 的过滤器链也有 Spring MVC 的拦截器 HandlerInterceptor那么这两个体系的执行顺序不一样互不干扰。但如果在 SecurityConfig 中同时配置了多个认证过滤器比如自定义的 JWT 过滤器被 addFilterBefore 了后来又注册成一个 Filter Bean被 WebSecurityConfigurerAdapter 默认加载了一次那每个请求就会走两遍过滤器表现为日志重复打印、Token 被解析两次、严重时出现性能下降。这个问题实操中常见于 Spring Boot 2.x 环境一旦我们把 Filter 定义成 ComponentSpring Boot 会自动把它注册到 Servlet 容器里而 SecurityConfig 里又手工加了它双重注册就发生了。解决方法是让自定义过滤器不要标注 Component而是在 SecurityConfig 中通过 Bean 显示创建并设置过滤器的注册 bean 为 FilterRegistrationBeansetEnabled(false) 禁止 Servlet 容器自动注册。Bean public FilterRegistrationBeanJwtAuthenticationFilter registration(JwtAuthenticationFilter filter) { FilterRegistrationBeanJwtAuthenticationFilter registration new FilterRegistrationBean(filter); registration.setEnabled(false); return registration; }5. 扩展场景跟 Spring Boot 生态其他组件配合Spring Security 从来不是孤岛。真实项目里认证通过只是万里长征第一步后面还跟着文件存储、接口文档、消息队列、数据库访问等等一堆东西。这里挑两个跟安全最相关的扩展场景说一下。5.1 整合 MinIO 文件服务的权限控制MinIO 是现在挺流行的对象存储服务经常用它搭私有文件服务器。在 springboot 项目中接入 MinIO一般会用它的 Java SDK 做文件的上传下载、临时链接生成。单纯接入并不复杂但做细了会牵扯权限问题MinIO 有自己的 AccessKey/SecretKey 体系和 Bucket Policy这跟 Spring Security 的用户体系是两套东西如果混在一起管理权限模型会变得特别混乱。我的做法是把 MinIO 的凭证放在服务端不让前端直接持有 AccessKey。前端需要上传文件时先请求后端接口后端校验 Spring Security 认证信息和业务权限然后调用 MinIO SDK 生成一个带时效的预签名 URL 返回给前端。前端直接拿这个 URL 上传不需要暴露 MinIO 的密钥而且链接可以设置 10 分钟或 1 小时过期安全可控。这个方案实际上是把 Spring Security 当成权限闸门MinIO 只是底层存储两套体系互不干扰又完全打通。5.2 docker 部署 Spring Boot 项目时的安全配置注意事项容器化部署是现在的主流springboot 项目打成 jar 包丢进容器跑很普遍。部署时容易忽略的一个安全点是Spring Security 的放行规则和容器实例的存活探针冲突。Kubernetes 里 livenessProbe 和 readinessProbe 会定期访问 /actuator/health如果这个路径没有被 Spring Security 放行探针就会收到 401导致 Pod 被反复重启。处理方式是在 SecurityConfig 里显式放行健康检查相关的路径。另外一个部署常见坑是时区问题。JWT 的过期时间是用System.currentTimeMillis()计算的服务器时区如果设置不正确可能导致 Token 的签发时间和过期时间跟本地时间对不上触发过期判断异常。Docker 里启动容器时务必加上-e TZAsia/Shanghai确保生成的过期时间符合预期。6. 最终实操经验总结整套方案从零到一跑通之后我觉得真正决定项目质量的是几个容易被忽略的习惯。首先密码编码器一定要在项目早期就确定并固化下来。用 BCrypt 就是 BCrypt中途想换成别的算法迁移成本很大。其次Token 的过期策略要跟业务侧对齐不能一刀切全部 24 小时去查一下用户平均在线时长再定。最后SecurityContext 里既然存了用户就不要再从数据库查一次用户表了直接在 Controller 里从 SecurityContextHolder 取当前用户对象流水线上省一次 IO线上压力能小不少。如果后续要做更复杂的场景比如集成 Spring Cloud Gateway 做统一认证网关、用 Redis 做 JWT 黑名单、引入 OAuth 2.0 和第三方社交登录这整套认知框架一样能沿用下去。Spring Security 的核心模型其实就那么多东西把认证、授权、过滤链这三座大山翻过去后面的路就好走了。项目做到后期我再看当初刚接触 Spring Security 时被绕晕的过滤器链发现在前后端分离场景下它反而没那么可怕。我们不需要跟它的每一个默认组件较劲理解它的两把刷子——认证和授权——然后按自己的业务把这两个环节替换或扩展掉剩下的事情框架都替你处理好了。希望这篇教程能帮你把这条必经之路走顺少加几个通宵的班。
返回列表