Spring Security基础:快速搭建安全认证系统
引言:为什么需要这篇文章?

作为一名后端开发工程师,我在日常工作中接触到了大量的Java Web项目。在这些项目中,用户认证与权限控制是必不可少的一环。而在Java生态中,最成熟、最广泛应用的安全框架无疑是 Spring Security。
说实话,第一次接触Spring Security的时候我是懵的。面对它复杂的配置方式和一堆抽象的概念,比如Filter、Provider、EntryPoint、AuthenticationManager……我一度怀疑人生,心里默念“这玩意儿到底怎么用?”但经过几个项目的实战之后,我逐渐掌握了它的核心思想,并且总结出了一套快速上手+灵活扩展的使用方法。
今天我想通过一个真实的项目案例,带大家走一遍完整的Spring Security集成流程。从项目背景到实际挑战,从代码实现到踩坑经验,希望能帮助刚入门的同学少走弯路。
项目背景:我们遇到了什么问题?

这个故事发生在去年年底,公司要上线一个新的企业内部平台,叫做HRM(人力资源管理系统)。整个系统面向公司的HR部门,管理员工档案、薪资、考勤、假期等信息,属于典型的后台管理型应用。
作为一个管理敏感数据的系统,安全性自然是我们首要考虑的问题:
- 用户必须登录才能访问任何功能;
- 每个角色有不同的权限限制(比如HR专员只能查看员工信息,不能修改薪资);
- 系统必须支持单点登录(SSO)未来扩展;
- 前后端分离架构,使用JWT做无状态身份验证;
- 还有一些细节需求,比如密码复杂度校验、登录失败次数限制、账号锁定机制等。
虽然需求看起来不复杂,但实际操作起来才发现:我们要处理的是一个典型的身份认证+权限控制场景,而这正是Spring Security擅长的地方。
于是,我决定采用Spring Security来完成这一部分的功能设计和开发。
解决思路:我们需要怎么做?

总体架构设计
整个系统的后端基于Spring Boot 2.7 + Spring Security 5.6构建,数据库采用MySQL,使用JPA作为ORM工具。前端为Vue.js单页面应用,与后端完全分离,通过REST API通信。
整体的安全架构可以分为以下几个模块:
- 用户认证流程
- 权限控制逻辑
- JWT令牌生成与验证
- 登录失败策略与账号锁定机制
- 安全接口的设计与保护
接下来我会重点分享前面几个关键点的实现逻辑。
一、从零开始:搭建Spring Security基础结构

首先是最简单的Security配置类,用于开启Spring Security功能并做一些基本设置。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}
这段配置做了几件事情:
- 禁用了CSRF攻击防护(因为前后端分离)
- 设置了无状态会话管理(因为要用JWT)
- 添加了一个自定义的JWT过滤器到UsernamePasswordAuthenticationFilter之前
- 配置了URL路径的权限访问策略
此时还只是一个骨架,还需要实现具体的用户认证逻辑、JWT令牌的签发与验证、以及角色权限的判断。
二、用户认证体系设计
我们先看用户表的设计,简化后的SQL如下:
CREATE TABLE `user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(100) NOT NULL,
`status` TINYINT DEFAULT 1, -- 是否启用 1:正常,0:禁用
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE `role` (
`id` INT PRIMARY KEY AUTO_INCREMENT,
`name` VARCHAR(20) NOT NULL
);
CREATE TABLE `user_role` (
`user_id` BIGINT NOT NULL,
`role_id` INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (role_id) REFERENCES role(id)
);
对应的Java实体类这里就不贴了,大家可以根据自己的ORM方式构造POJO对象即可。
然后是用户登录接口的核心逻辑:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
);
String token = jwtUtils.generateToken(authentication);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
return ResponseEntity.ok()
.header("Authorization", "Bearer " + token)
.body(Map.of("token", token, "user", userDetails));
}
是不是很熟悉?这里的authenticationManager.authenticate()就是触发Spring Security认证流程的关键步骤。如果用户名或密码错误,会抛出异常,我们可以在全局异常处理器中捕获并返回友好的错误提示。
而jwtUtils.generateToken(authentication)则是将认证成功的用户信息封装成JWT令牌返回给客户端。
三、实现JWT令牌签发与验证逻辑
为了更清晰地管理JWT相关的逻辑,我封装了一个工具类 JwtUtils.java,主要包含两个核心方法:
generateToken():将用户认证信息转换为签名后的JWT字符串;parseToken():解析请求头中的JWT,获取用户身份信息。
核心代码示例如下(已做简化):
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
然后还有一个自定义过滤器 JwtAuthenticationFilter.java,负责拦截请求、解析Token、填充认证信息到Security上下文中:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.getUsernameFromToken(token);
UserDetails userDetails = userService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}

注意这里继承了 OncePerRequestFilter,这样每个请求只会执行一次,避免重复处理。这个类非常重要,如果你漏掉了某个判断条件或者没正确填充认证信息,会导致后面所有权限判断失效。
四、权限控制的实现方式
Spring Security 提供了多种方式进行权限控制,比如:
- 方法级别的注解如
@PreAuthorize("hasRole('ADMIN')") - URL路径配置
.antMatchers("/admin/**").hasRole("ADMIN")
但在HRM项目中,我们采用了较为灵活的 基于业务模型的角色控制机制。
比如说,在查询员工列表时,我们希望不同角色看到的数据范围不同:
- HR Manager:可查看全量员工;
- HR专员:仅能查看自己所属团队的员工;
- 普通用户:无权访问。
为此,我们在服务层加了一个简单的权限校验逻辑:
@GetMapping("/employees")
@PreAuthorize("hasAnyRole('HR_ADMIN', 'HR_MANAGER')")
public List<Employee> getAllEmployees() {
// 实际查询前,判断是否是特定角色
if (SecurityContextHolder.getContext().getAuthentication().getAuthorities()
.stream().map(GrantedAuthority::getAuthority)
.noneMatch(role -> role.equals("ROLE_HR_ADMIN"))) {
Long userId = getCurrentUserId();
return employeeService.findByTeamId(teamService.findTeamByUserId(userId));
} else {
return employeeService.findAll();
}
}
这种方式结合了Spring Security的注解权限控制和业务逻辑上的细粒度判断,灵活性更高,也更容易扩展。
五、遇到的挑战与踩过的坑
说了这么多顺利的部分,再来讲讲我在实际开发过程中踩过的一些坑。
坑1:Spring Security默认的PasswordEncoder不对
我们一开始没有配置密码编码器,导致用户注册的时候直接把明文密码存进了数据库。结果登录时永远失败!
后来意识到,我们需要在配置类中注入一个 PasswordEncoder bean,一般推荐使用BCrypt算法加密:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
并在用户注册时使用它进行密码加密存储:
String encodedPassword = passwordEncoder.encode(rawPassword);
否则的话,Spring Security会在认证时使用BCrypt去匹配你传进来的原始密码,当然不可能一致。
坑2:跨域请求被CORS阻断
因为我们是前后端分离架构,前端运行在 http://localhost:8080,而后端API跑在 http://localhost:8081,所以不可避免地会遇到跨域请求问题。
解决办法是在Security配置中放行CORS:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors();
// ...
}
同时添加一个全局配置类用于提供跨域规则:
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}
记得设置 allowCredentials(true),否则携带JWT的请求会被浏览器拒绝。
坑3:未处理JWT过期的情况
早期我们在前端每次发起请求都会带上JWT Token。但是如果Token过期了,后端应该返回401 Unauthorized,前端收到这个状态码就可以跳转到登录页重新登录。
但我们一开始没在Security中配置这个逻辑,导致出现以下情况:
- Token有效 → 正常处理;
- Token无效 → 默认返回403 Forbidden,前端不知道如何处理。
为了解决这个问题,我们引入了一个全局异常处理器,并实现 AuthenticationEntryPoint 接口:
@Component
public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized: Authentication token was missing or invalid");
}
}
并在Security配置中指定该入口点:
.and()
.exceptionHandling().authenticationEntryPoint(jwtAuthEntryPoint)
这样一来,当Token校验失败(比如过期、篡改、不存在等),就会返回统一的401响应。
六、效果总结:这套方案带来的收益
整个项目上线后运行稳定,HR部门反馈良好。回顾一下我们的实践成果:
- 统一的安全认证体系:通过Spring Security整合JWT,实现了前后端分离下的无状态认证;
- 灵活的权限控制能力:既有URL级别也有方法级别的权限控制;
- 良好的扩展性:未来接入OAuth2、多租户、SSO等都更容易;
- 运维友好:由于采用了标准化的日志格式和鉴权逻辑,排查问题方便很多;
- 性能表现不错:没有引入额外的Session存储,也没有频繁访问数据库来做权限判断。
特别是对于我们这种资源有限的小团队来说,这样的方案既实用又高效。
经验分享:给后来者的建议
最后我想给刚开始学习Spring Security的开发者们几点建议:
✅ 1. 不要怕复杂,搞懂核心概念才是关键
Spring Security确实有一定的学习曲线,但它背后的思想非常清晰:过滤链+委托模式+责任链模式。一旦你理解了这个内核逻辑,剩下的只是各种适配器和实现方式罢了。
✅ 2. 多动手,少看文档理论
光看书或者教程很难真正掌握。最好的方式是找个小项目练练手,比如写一个简单的博客系统,加上登录和权限控制。遇到问题就查官方文档、Stack Overflow,甚至反编译源码。
✅ 3. 安全不是万能的,但至少不要犯低级错误
比如:
- 不要明文保存密码;
- 不要用HTTP传输Token;
- 不要在生产环境暴露堆栈信息;
- 不要忽略日志记录。
✅ 4. 跟上技术趋势,尝试新特性
比如现在Spring Security已经全面支持Reactive编程,你可以尝试在Spring WebFlux项目中使用Security;还有对OAuth2的支持也越来越完善,适合微服务架构下的权限管理。
结语:安全感,也是程序员的一种追求
在这次项目实践中,我深刻体会到:一个合格的后端开发者,不仅要写好业务逻辑,更要守护系统的安全边界。Spring Security就像一道看不见的门卫,默默保障着每一个请求的合法性。
希望这篇文字能让更多人少一些恐惧,多一些信心地走进Spring Security的世界。别忘了,写代码不只是写功能,更是写安全感。
如果你正在学习Spring Security,欢迎留言交流,一起进步 🙌
文章作者:一枚热爱技术的后端开发者,专注Java生态与系统安全领域。欢迎关注我的GitHub & 微信公众号。

评论 0