Spring Security基础:快速搭建安全认证系统
从零开始搭建安全认证系统:我在Spring Security项目中的实战经验

我叫阿飞,是某一线互联网公司的一名后端开发者,主要负责用户中心和权限系统的维护与开发。虽然在日常工作中经常接触到各种认证授权机制,但真正让我对安全体系有深入理解的,是去年我们接手的一个新项目——一个面向企业用户的 SaaS 管理平台。
这个项目要求每个企业客户都能独立配置自己的权限体系,并且支持多角色、细粒度的访问控制,同时还要支持第三方应用集成。当时团队里没有专门的安全专家,一切都得靠自己摸索。而 Spring Security 成为了我们的首选工具。
这篇分享就记录了我们如何从零开始搭建一套基于 Spring Security 的安全认证系统,过程中遇到的问题以及一些踩过的坑。希望这些经验能给同样走在路上的你一点参考。
起步阶段:为什么选择 Spring Security?
项目初期选型时,摆在我们面前的几个选项是 Shiro、自研方案,还有 Spring Security。最终决定用 Spring Security 主要是出于以下几点考虑:
- 生态整合:我们的项目基于 Spring Boot,Security 天然无缝接入;
- 社区活跃:文档丰富,资料充足,遇到问题容易找到解决办法;
- 功能全面:开箱即用的基本认证机制、OAuth2 支持、CSRF 防护等基本涵盖了常见的安全需求;
- 可扩展性强:插件化设计允许我们在业务需要的时候灵活扩展。
不过说实话,刚上手的时候我也被它庞大的模块和繁杂的配置吓到了。尤其是官方文档,动辄几十页的英文内容,看完还是一头雾水。于是我们一边看文档一边写 Demo,慢慢摸清了套路。
搭建第一个安全接口:从最简单的登录认证开始
我们的第一个目标是实现基本的用户名密码登录验证。这听起来简单,但要在 Spring Security 中做到这一点,还是有很多细节需要注意的。
关键配置类
我们先写了一个基础的 SecurityConfig 类,用来定义整个项目的认证流程和 URL 访问规则:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
这段配置看起来简单,但背后涉及到了很多关键点:关闭 CSRF(因为我们打算用 JWT)、使用无状态会话管理(适配前后端分离架构)、添加我们自定义的 JWT 过滤器等等。
接下来我们又实现了一个简易的认证处理器:
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
buildAuthorities(user.getRoles())
);
}

private Collection<? extends GrantedAuthority> buildAuthorities(List<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
这里我们从数据库中取出用户信息和对应的角色列表,构造成 Spring Security 所需的 UserDetails 对象返回。
到这一步,我们已经能够处理 /api/auth/login 接口的登录请求了。虽然代码量不多,但这一步其实非常重要,因为它是后续所有权限体系的基础。
真正的挑战:从单点登录到多租户授权模型
当基本认证跑通之后,真正的挑战才刚刚开始。
我们的 SaaS 平台需要满足“多租户”的需求,也就是说不同企业的用户可能有不同的资源访问权限和组织结构。这就意味着我们需要重新思考用户模型的设计:
- 用户属于某个租户(Tenant)
- 租户可以定义自己的角色(Role)
- 角色绑定具体的权限(Permission)
为此,我们设计了一套三级模型:
public class Tenant { ... }
public class UserRole {
private Long userId;
private Long roleId;
}
public class Role {
private String name;
private List<Permission> permissions;
}
同时在接口层面也进行了改造,比如为每个请求加上租户上下文识别逻辑,确保当前操作不会越界访问其他企业的数据。
安全性的保障:引入 JWT 做无状态鉴权
随着用户量增加和微服务拆分推进,Session 方案显然无法满足我们的需求,所以我们决定改用 JWT 做 Token 认证。
实现 JWT Filter
我们编写了一个继承自 OncePerRequestFilter 的过滤器,用于拦截请求、解析 Token,并将认证信息注入 Spring Security 上下文中:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String TOKEN_HEADER = "Authorization";
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && validateToken(token)) {
Authentication auth = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader(TOKEN_HEADER);
if (header != null && header.startsWith("Bearer ")) {
return header.replace("Bearer ", "");
}
return null;
}
private boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey("secret_key")
.parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
private Authentication getAuthentication(String token) {
Claims claims = Jwts.parser()
.setSigningKey("secret_key")
.parseClaimsJws(token)
.getBody();
String username = claims.getSubject();
List<String> roles = (List<String>) claims.get("roles");
Collection<? extends GrantedAuthority> authorities = roles.stream()
.map(SimpleGrantedAuthority::new).toList();
return new UsernamePasswordAuthenticationToken(username, null, authorities);
}
}
通过这种方式,我们将 Token 解析与认证信息注入紧密结合,在每次请求到达 Controller 前完成校验,大大提升了系统的安全性和灵活性。
不可忽视的性能优化:减少数据库查询压力
随着用户量逐渐上升,我们发现频繁的用户信息和权限查询对数据库造成了不小的压力。尤其是在并发高峰期间,响应时间明显变长。
我们做了几项优化:
- 缓存用户权限信息:使用 Redis 缓存每个用户的权限集合,避免每次都去查数据库。
- 懒加载权限判断:对于复杂的权限检查逻辑,我们采用了 lazy loading 的方式,只有在真正需要判断权限的时候才会触发一次数据库查询。
- 异步更新缓存策略:每次用户权限发生变更时,不是立即清除缓存或更新 Redis,而是采用消息队列异步推送的方式通知缓存刷新。
此外,我们还在 JWT 中加入了更细粒度的权限字段,这样部分接口可以直接从 Token 取出权限判断,无需再访问数据库。
这样的优化实施之后,权限相关的接口平均响应时间下降了 30% 左右,TPS 提升了约 25%,效果非常明显。
我们踩过的大坑
在整个搭建和迭代过程中,我们也遇到了不少“经典”问题,下面是我印象比较深的几个:
❌ 忽略 Session 并发带来的问题
最开始我们在测试环境使用默认的 Session 管理机制,结果在一个高并发接口压测时发现多次请求被强制登出,排查才发现是因为多个线程共享同一个 Session 导致 Token 被覆盖。后来我们果断切换为无状态方案。
❌ 忘记处理 OPTIONS 请求跨域预检失败
我们前端使用的是 Vue,默认请求携带 Authorization 头,但在某些接口上出现了 CORS 报错。这个问题其实是由于 Spring Security 默认不允许带 Authorization 头的跨域请求引起的。最终我们在安全配置里加上了如下内容才算解决:
http.cors(cors -> cors.configurationSource(request -> {
var corsConfig = new CorsConfiguration();
corsConfig.addAllowedOrigin("http://your-frontend.com");
corsConfig.addAllowedHeader("*");
corsConfig.addAllowedMethod("*");
corsConfig.setAllowCredentials(true);
return corsConfig;
}));
❌ 权限误判导致接口访问异常
有一次我们上线后发现某个角色的用户居然可以访问管理员接口。最后排查发现是因为我们在注解上用了 .hasRole("admin"),但实际上存储在数据库里的角色名称是 "ADMIN",大小写不匹配导致放行。
这个问题提醒我们要特别注意权限命名的一致性,最好统一规范为大写,并在初始化角色时做好清洗。
最终成果:安全系统稳定运行一年
如今这套安全系统已经在生产环境中平稳运行超过一年,支撑了数百家企业客户的使用,日均请求数百万次,权限配置非常灵活,还可以对接外部 OAuth 应用进行联合登录。
更重要的是,它的扩展性很好。我们现在正在做 RBAC 模块的升级,准备支持动态权限配置和可视化权限编辑器,底层只需要对角色模型稍加调整即可,不需要重构现有认证逻辑。
给开发者的建议和注意事项
结合我个人的经历和团队的实际项目经验,我想给刚开始使用 Spring Security 的朋友几点建议:
✅ 尽早明确认证模型和权限结构
安全系统的复杂度往往取决于业务本身的权限模型。建议在项目初期就确定好用户、角色、权限之间的关系,并画出清晰的数据表结构图。
✅ 分离业务逻辑和安全逻辑
不要把权限校验逻辑写进业务代码里。尽量使用 @PreAuthorize 或者切面来统一处理权限校验,保持核心业务干净整洁。
✅ 合理使用缓存提高性能
像用户权限这种读多写少的数据,非常适合缓存。但要记得设置合理的 TTL 和清理策略,避免出现“权限已修改,但缓存未更新”的尴尬场景。
✅ 多做权限测试 + 压力测试
权限系统的 bug 往往不容易察觉,建议写足够多的单元测试。特别是边界条件,比如一个用户多个角色、角色之间存在父子关系等。
✅ 学会定制 Spring Security 的组件
Spring Security 提供了很多可扩展点,例如 AccessDecisionManager、AuthenticationProvider、Filter 等。当我们有特殊需求时,不要试图绕过框架,而是合理地使用这些扩展能力。
写在最后:安全是个持续的过程
回顾整个项目的开发过程,其实最大的收获并不是学会了怎么写 JWT Filter 或者怎么配置权限表达式,而是明白了安全从来不是一个“做完就能一劳永逸”的东西。
它需要我们不断地根据业务变化进行调整,也需要我们时刻关注最新的安全漏洞和防护手段。尤其是作为后端工程师,我们承担着保护用户数据、防止系统入侵的第一道防线。
希望这篇文章能帮你少走弯路,顺利搭建起自己的安全认证体系。如果你在这个过程中有任何疑问或者想交流实战经验,欢迎留言,我们一起成长。
作者:阿飞,一名热爱编码、重视工程实践的一线 Java 开发者。

评论 0