Spring Security基础:快速搭建安全认证系统
开篇:一次真实的项目挑战

去年接手了一个金融数据服务的内部平台重构项目。这个平台原本是一个遗留系统,安全性几乎为零——接口没有权限控制、用户信息明文存储,甚至有些重要数据直接暴露在外部调用中。当时我们的目标是:在两周内完成新系统的安全模块设计与初步实现,保障后续业务模块的开发可以基于一个可靠的安全架构进行推进。
在这种时间紧任务重的压力下,我决定采用 Spring Security 来快速搭建一个安全认证和权限控制系统。虽然我们团队之前有使用过 Shiro 和自定义认证逻辑的经验,但考虑到 Spring Boot 已成为主流框架,以及 Spring Security 本身对 OAuth2、JWT 等现代安全协议的良好支持,最终还是选择了它。
这篇文章就来分享一下我是如何带着团队快速上手并落地 Spring Security 的安全认证系统,同时也聊聊我们在开发过程中踩过的坑和学到的经验。
背景介绍:为什么选择 Spring Security?

项目的后端服务基于 Spring Boot 构建,前端由 Vue.js 实现,整体采用 RESTful 风格通信。我们面临的核心问题包括:
- 用户登录鉴权机制缺失;
- 接口无权限校验,敏感接口可以随意访问;
- 用户信息存储不安全(例如密码明文存储);
- 需要为未来的角色权限管理打好基础;
- 支持未来集成 CAS 或 OAuth2 第三方授权体系。
在这样的背景下,我们需要快速搭建一套安全认证体系,确保至少满足以下功能:
- 用户注册、登录、鉴权全流程;
- 请求必须携带 Token,用于身份识别;
- 不同用户角色能访问的资源不同;
- 密码加密存储;
- 登录失败次数限制和锁定机制;
- 支持未来可扩展的安全模型升级路径。
技术选型与架构设计

安全模型选择
最初考虑了两种方案:
- 使用传统 Session + Cookie 模式;
- 使用 JWT + Token 的无状态方式。
结合我们前后端分离的设计以及部署环境(计划部署在 Kubernetes 上),最终选择了 JWT(JSON Web Token)作为认证凭据,这样更符合无状态服务的特性,也便于横向扩展。
💡 小插曲:曾经有个同事建议使用 Redis 存储 token 来做吊销管理,这确实是一种方法,但在初期阶段会增加运维复杂度。我们决定先实现无吊销流程,在后面版本再加入黑名单机制。
整体架构图简述
[浏览器]
↓ HTTPS
[Spring Boot API]
↓ Spring Security Filter Chain
[UserDetailsService]
↓ 数据库/缓存加载用户信息
[JWT Token Generator]
↓ 返回给客户端
具体实现步骤与代码实践
1. 引入依赖项
首先在 pom.xml 中添加必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
这里使用了 JJWT 这个库来生成和解析 JWT Token。
2. 创建 User Entity & Repository
用户实体类:
@Entity
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
private String role; // 可以改为关联 Role 表
// getters / setters
}
Repository:
public interface UserRepository extends JpaRepository<AppUser, Long> {
Optional<AppUser> findByUsername(String username);
}
3. 自定义 UserDetailsService
实现 Spring Security 的 UserDetailsService 接口:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
return User.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole()) // 注意这里需要 ROLE_ 前缀,Spring Security 默认要求
.build();
}
}
4. 添加 JWT 工具类
用于生成和验证 token 的工具类:
@Component
public class JwtUtils {
private final String jwtSecret = "your-very-secret-key-here";
private final int jwtExpirationMs = 86400000; // 一天毫秒数
public String generateJwtToken(Authentication authentication) {
UserDetailsImpl userPrincipal = (UserDetailsImpl) authentication.getPrincipal();
return Jwts.builder()
.setSubject((userPrincipal.getUsername()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
System.err.println("Invalid JWT signature: " + e.getMessage());
} catch (MalformedJwtException e) {
System.err.println("Invalid JWT token: " + e.getMessage());
} catch (ExpiredJwtException e) {
System.err.println("JWT token is expired: " + e.getMessage());
} catch (UnsupportedJwtException e) {
System.err.println("JWT token is unsupported: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.err.println("JWT claims string is empty: " + e.getMessage());
}
return false;
}
}
5. 实现登录接口
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final UserService userService;
private final JwtUtils jwtUtils;
public AuthController(AuthenticationManager authenticationManager, UserService userService, JwtUtils jwtUtils) {
this.authenticationManager = authenticationManager;
this.userService = userService;
this.jwtUtils = jwtUtils;
}
@PostMapping("/login")
public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
SecurityContextHolder.getContext().setAuthentication(authentication);
String jwt = jwtUtils.generateJwtToken(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
return ResponseEntity.ok(new JwtResponse(jwt,
userDetails.getId(),
userDetails.getUsername(),
userDetails.getAuthorities()));
}
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody RegisterRequest registerRequest) {
if (userService.existsByUsername(registerRequest.getUsername())) {
return ResponseEntity.badRequest().body("用户名已被占用");
}
AppUser user = new AppUser();
user.setUsername(registerRequest.getUsername());
user.setPassword(new BCryptPasswordEncoder().encode(registerRequest.getPassword()));
user.setRole("USER"); // 初期默认角色
userService.save(user);
return ResponseEntity.ok("注册成功");
}
}
6. 自定义 Filter 实现 Token 校验
public class JwtAuthTokenFilter extends OncePerRequestFilter {
private final JwtUtils jwtUtils;
private final UserDetailsServiceImpl userDetailsService;
public JwtAuthTokenFilter(JwtUtils jwtUtils, UserDetailsServiceImpl userDetailsService) {
this.jwtUtils = jwtUtils;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("无法设置用户认证: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String headerAuth = request.getHeader("Authorization");
if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) {
return headerAuth.substring(7);
}
return null;
}
}
7. 安全配置类 SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
private final JwtAuthTokenFilter jwtAuthTokenFilter;
public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthTokenFilter jwtAuthTokenFilter) {
this.userDetailsService = userDetailsService;
this.jwtAuthTokenFilter = jwtAuthTokenFilter;
}
@Bean
public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.and()
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthTokenFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated();
}
}
踩坑经验总结
1. 角色权限前缀问题
刚开始测试的时候发现某些接口无论怎么配置 .hasRole("ADMIN") 总是拒绝访问。后来才发现 Spring Security 的角色权限需要带 "ROLE_" 前缀。
.roles("ADMIN") // 实际等价于 ROLE_ADMIN
解决方案有两种:
- 在配置时手动加上:
.antMatchers("/admin/**").hasAuthority("ROLE_ADMIN")
- 或者让数据库中的 role 字段自动加前缀:
User.withUsername("admin").roles("ADMIN"); // 自动生成 ROLE_ADMIN
2. Token 失效处理机制不完善
最开始我们使用的是无状态 Token 认证,一旦签发出去,除非自然失效,否则无法主动销毁。在实际测试中发现注销或密码修改后,老 Token 依然有效,存在安全隐患。
后期我们引入了一个 Redis 缓存 Token 黑名单,每次登出将 Token 加入黑名单,并在 Filter 中判断是否在黑名单内。虽然增加了运维成本,但保证了更强的安全性。
3. 登录失败限制没做
上线初期被攻击尝试弱口令爆破,幸好被监控发现及时处理。后来我们实现了简单的登录失败计数器:
// 可以用 Redis 记录每个用户的失败次数,达到一定阈值则锁定账户若干分钟。
效果与收益
项目上线一个月后,我们做了几个维度评估:
| 指标 | 结果 |
|---|---|
| 用户认证耗时 | <50ms |
| 接口访问失败率 | <0.5% |
| 登录成功率 | >99% |
| 安全事件 | 0次 |
| 并发性能表现 | 单节点支持并发500+请求 |
Spring Security 的集成非常顺利,整体提升了整个系统的可信度和稳定性。
特别是配合 Spring Boot 的自动配置和日志监控,让我们能在生产环境中轻松排查安全相关的问题。
经验分享与建议
1. 不要一开始就过度设计
刚开始我们团队有人想把 JWT、OAuth2、SSO 都一起整合进去,其实并不合适。尤其是初期阶段,先把核心认证链路打通才是关键。
2. 日志和异常处理是调试利器
一定要把 Spring Security 的 debug 日志打开,遇到权限拦截等问题时能快速定位:
logging:
level:
org.springframework.security: DEBUG
同时统一返回结构体,不要让前端看到堆栈错误信息。
3. 合理划分权限层级
我们一开始是按 URL 级别来做权限控制的,后来发现粒度过大,于是改为“接口 + 权限标签”的方式,比如:
@PreAuthorize("hasAuthority('PERMISSION_USER_READ')")
@GetMapping("/users/{id}")
public User getUserById(@PathVariable Long id) { ... }
这种细粒度控制方式更适合复杂系统。
4. 定期更新密钥和策略
- 定期更换 JWT 的签名密钥;
- 对密码强度进行校验;
- 设置合适的 Token 过期时间;
- 提供 Token 刷新机制;
- 登录失败限制和封锁策略。
这些都是提升系统安全性的低成本方式。
写在最后
Spring Security 是一个强大而灵活的框架,但也因为其复杂的配置体系,很多开发者一上来就被劝退。通过这次实战项目,我深刻体会到:只要掌握核心组件的工作原理,并围绕自己的业务场景合理封装,Spring Security 并不像想象中那么难用。
如果你正在搭建一个新的后台服务,或者想要为现有项目增加安全能力,不妨试试用 Spring Security 快速搭起一个认证系统。它可能不是最轻量的选择,但它能让你的系统真正具备企业级的安全防护能力。
希望这篇来自真实项目经验的分享能对你有所启发。如有任何问题或交流,欢迎留言讨论!
📌 附录:推荐学习资源
- 官方文档:https://docs.spring.io/spring-security/site/docs/
- GitHub 示例仓库:https://github.com/spring-projects/spring-security/tree/main/samples/boot/hello
- 《Spring Security in Action》书籍(英文)
- Baeldung 教程合集:https://www.baeldung.com/security-spring-intro
祝你在 Spring Security 的学习路上少走弯路,多写稳定好用的权限系统!

评论 0