Spring Security实战:从零搭建安全认证系统
开始的契机

去年我接手了一个中小型企业的 SaaS 项目,主要目标是重构他们原来的平台后端服务。这个平台涉及用户管理、权限划分、数据隔离等典型场景。由于原有系统安全性堪忧,客户提出了一个硬性要求:所有接口必须经过统一的权限控制,防止越权操作和信息泄露。
说实话,刚接到这个需求的时候,我还是有点发怵的。虽然Spring生态我已经用得挺熟,但Security这一块在之前的项目里主要是“配个默认登录页”那种程度。这次不同了,要真真正正从头搭起一套完整、可扩展、性能又不差的认证授权体系。
今天就来聊聊我是怎么一步步把这个“安全防线”搭建起来的,以及过程中踩过的坑和一些经验教训。
真实挑战来了

我们当时的系统背景是基于 Spring Boot + MyBatis 的微服务架构,前端是 Vue.js,前后分离。业务上需要支持:
- 用户注册 / 登录
- 基于角色的权限控制(RBAC)
- RESTful 接口访问控制(例如 /api/user/** 只有管理员能访问)
- 登录状态维护(会话过期、并发登录限制等)
- 安全审计(谁在什么时间执行了哪些操作)
当时我们团队遇到的第一个问题就是:“现在有很多安全框架和组件,该如何选型并快速落地?”
比如:
- 用Shiro还是Spring Security?
- 是用Session还是JWT?
- 如何把认证和授权模块设计得既灵活又解耦?
我们最终选择了 Spring Security + JWT + 自定义权限注解 的方案。原因有几个:
- 团队对 Spring Boot 比较熟悉,迁移成本低;
- Security 功能完备且社区活跃,文档丰富;
- JWT 无状态特性适合未来的微服务拆分。
我的设计思路与实现过程
第一阶段:基础认证功能落地
首先我把整个认证流程划分为几个核心步骤:
登录 -> 认证 -> 生成Token -> 返回客户端 -> 后续请求携带Token -> 校验有效性 -> 放行/拦截
核心依赖引入:
<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>
登录接口 & Token发放
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthService authService;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
String token = authService.authenticate(request.getUsername(), request.getPassword());
return ResponseEntity.ok().header("Authorization", "Bearer " + token).build();
}
}
authService.authenticate() 内部调用了 AuthenticationManager.authenticate() 方法进行用户名密码验证,并通过自定义 UserDetailsService 查询数据库中的用户信息。

JWT工具类封装
public class JwtUtils {
private static final String SECRET_KEY = "your_secret_key_here";
private static final long EXPIRATION = 86400000; // 24小时
public static String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
}
public static String parseUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
}
实现 UserDetailsService
@Service
public class UserDetailsServiceImpl 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 User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}
这时候,基础的认证流程就跑通了。
第二阶段:权限控制
Spring Security 提供了非常强大的方法级权限控制功能,比如:
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/users")
public List<User> getAllUsers() {
return userService.findAll();
}
为了启用这个功能,别忘了加上配置:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.anyRequest().authenticated();
}
}
这里有个关键点,我们在过滤器链中添加了一个自定义的 JWT 过滤器,在每次请求到达 controller 前校验token是否合法,并填充 SecurityContextHolder,让后续逻辑能识别出当前用户身份。
第三阶段:日志与审计
为了满足客户的审计需求,我还设计了一张简单的安全日志表:
CREATE TABLE security_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64),
action VARCHAR(255), -- 如“访问了/users接口”
ip_address VARCHAR(128),
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
然后使用 AOP 技术自动记录安全事件:
@Aspect
@Component
public class SecurityAuditAspect {
@Autowired
private SecurityLogService logService;
@AfterReturning("execution(* com.example.controller.*.*(..)) && @annotation(secured)")
public void auditAccess(JoinPoint joinPoint, Secured secured) {
String methodName = joinPoint.getSignature().getName();
String username = SecurityContextHolder.getContext().getAuthentication().getName();
String ip = getCurrentRequestIpAddress();
logService.saveLog(username, "访问了方法:" + methodName, ip);
}
private String getCurrentRequestIpAddress() {
// 从RequestContextHolder获取HttpServletRequest
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
return attributes != null ? attributes.getRequest().getRemoteAddr() : "";
}
}
这样就能记录到谁在何时做了哪些敏感操作。
踩过的坑与解决之道
坑1:跨域(CORS)导致Token无法传递
现象: 浏览器控制台报错 “No 'Access-Control-Allow-Origin' header present”,并且 Authorization Header 没有被正确携带。
解决方法:
在 Security 配置中显式开启 CORS 支持,并在网关层做好域名白名单设置。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and()
// 其他配置...
}
同时配置 CORS 映射:
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:8080"));
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
configuration.setExposedHeaders(Collections.singletonList("Authorization"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
坑2:JWT签名不过期?
现象: 用户修改密码后,原来的token仍然有效!
解决方案:
- 设置合理的过期时间(通常不超过24小时);
- 使用 Redis 缓存黑名单机制,当用户登出或修改密码时,把对应token加入黑名单,并在每次请求时检查是否存在黑名单中;
- 在签发token时加入用户版本号(如用户每次修改密码,增加 version 字段),token中也包含该字段,验证时比对。
坑3:权限表达式配置错误导致接口全开放
这个问题差点酿成线上事故。
原本写的路径匹配规则是这样的:
.antMatchers("/api/user/**").hasRole("USER")
结果发现其他角色也能访问。后来才明白,hasRole("USER") 相当于自动加了 "ROLE_" 前缀,而我们的角色名是直接保存为 USER、ADMIN 的。应该改成:
.antMatchers("/api/user/**").hasAuthority("USER")
// 或者
.hasRole("USER") → 需要角色名是 ROLE_USER 才生效
这是一个非常容易混淆的细节,建议尽量使用 hasAuthority(),或者规范你的角色命名方式。
最终效果与收获

上线几个月后,整体运行稳定。安全方面没有出现过明显的漏洞,审计日志也帮助我们查到了几次异常访问行为。这套安全体系支撑了我们后续多个子系统的接入。
最让我欣慰的是:
- 登录流程流畅
- 权限控制粒度细
- 日志清晰可追溯
- 新人上手快,因为结构清晰、组件解耦做得不错
而且,由于采用了 JWT 和无状态设计,未来向微服务架构迁移也变得轻松不少。
给读者的一些建议
如果你正在构建自己的认证授权系统,以下几点是我的经验总结:
✅ 优先考虑系统演进方向
如果未来计划使用微服务,那一定不要使用 Session。JWT 是目前比较主流的做法,它天然适合分布式部署。
✅ 数据库设计很重要
权限模型不能太简单粗暴。用户、角色、权限、菜单这四张表的关联关系,直接影响到你能否方便地做权限配置。可以参考 RBAC 模型,合理设计中间表。
✅ 把 Security 拆成独立模块
如果你的项目比较大,可以把 Security 模块抽出来作为一个公共组件,比如 auth-center,其他服务通过 OpenFeign 或 RestTemplate 调用完成认证。
✅ 多留监控点
包括但不限于:
- 登录次数限制(防暴力破解)
- 异地登录通知(提升用户体验)
- 黑名单机制
- 审计日志保留期限
- 敏感操作二次确认(如删除数据)
✅ 保持学习更新意识
现在市面上已经有更高级的安全协议,比如 OAuth2、OpenID Connect,甚至 CAS、OAuth2 Resource Server 等标准协议的集成方案也越来越成熟。Spring Security 对这些都有良好的支持,值得花时间去研究。
结语
写这篇文章的过程,也是我在回顾过去一年成长的过程。记得刚开始接触Spring Security时,连基本的过滤器链都看得晕乎乎的。如今,已经能够根据实际业务需求定制化开发。
我希望通过分享我的经历,可以帮助到同样走在后端安全路上的朋友。真正的安全不是一句“配个拦截器”那么简单,它是一个长期投入的过程,是对业务、技术和人性的综合理解。
最后,感谢你耐心读到这里。如果你有任何疑问或者想要更详细的代码结构,欢迎留言交流,我会尽快回复。
共勉!

评论 0