Spring Security基础:从零搭建安全认证系统的一次实战经历
去年我参与一个新项目,需要为公司构建一个新的企业级后台管理系统。这本身不算难事,但需求里明确提到了用户权限体系的建设必须支持多角色、多层级管理,并且具备高安全性。作为后端负责人,我一开始想着用传统的拦截器+自定义逻辑的方式处理权限控制,后来一想,这种方案虽然短期能搞定,但长期来看维护成本会很高。
于是我们决定引入 Spring Security 进行统一的安全治理,不仅能满足现有的权限结构需求,还能为以后可能接入 SSO、OAuth2 等功能打下基础。
这篇文章就是我在那段时间的实践总结,希望能给正在学习或准备使用 Spring Security 的同学一点参考价值。我会结合当时真实的项目背景、遇到的问题和解决方案,带大家一步步搭起一个基于 Spring Boot + Spring Security 的基础认证系统。
项目背景与挑战初现

我们的目标是打造一个企业级后台系统,核心模块包括:
- 用户管理(角色、权限配置)
- 操作日志审计
- 接口鉴权(如不同角色访问不同接口)
- 登录状态保持及 Token 失效机制
- 多租户架构预埋(暂未上线)
最开始的构想挺简单:用户登录之后返回 JWT token,后续请求携带 Token 访问,每次都要验证 Token 合法性、是否过期、是否有对应权限。但是随着业务发展,越来越多的安全细节暴露出来:
- Token 到底怎么生成?签名方式选哪种?
- 如何优雅地整合数据库中的角色信息?
- 权限粒度要细到接口级别,怎么设计才不乱?
- 登录失败次数多了要不要限流?防爆破攻击怎么做?
- 如何对接 LDAP 或未来的 OAuth2?
这些问题如果全靠自研,不仅工作量大而且容易出错。于是我决定直接采用 Spring Security 提供的标准安全框架,它提供了从认证、授权、CSRF防御到Session管理等完整能力,非常适合做系统级安全治理。
技术方案选择与设计思路

技术栈
- Spring Boot 2.6.x
- Spring Security 5.7
- MyBatis Plus
- MySQL
- Redis(用于 Token 缓存和黑名单管理)
- JWT(用于无状态认证)
总体架构设计
整个认证流程大致如下:
用户登录 --> 验证用户名密码 --> 成功则签发 JWT Token --> 客户端存储并后续携带 Token 访问资源
↓
Token 在每次请求时由 Security 自动校验 → 检查权限 → 放行/拒绝
在 Spring Security 中,关键组件包括:
AuthenticationManager:负责认证流程UserDetailsService:加载用户详情JwtFilter:JWT Token 解析和验证AccessDeniedHandler:权限不足的异常处理SecurityProperties:安全配置类
接下来,我会详细讲讲每一步是如何落地的。
核心代码实现与配置详解

Step 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>
Step 2:自定义 UserDetailsService 实现
为了把数据库中的用户信息读取进来,我们需要继承 UserDetailsService 并重写方法:
@Service
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("用户不存在");
}
List<GrantedAuthority> authorities = new ArrayList<>();
// 获取该用户拥有的角色列表
List<Role> roles = user.getRoles();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()));
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities);
}
}
这里有个小技巧:将数据库里的角色前缀统一加上 "ROLE_" 是 Spring Security 的标准做法,方便后面通过 @PreAuthorize("hasRole('ADMIN')") 做注解式鉴权。
Step 3:编写 JWT 工具类
为了减少重复代码,封装了一个 JWT 工具类:
@Component
public class JwtUtil {
private static final String SECRET = "your-secret-key";
private static final long EXPIRATION = 86400000; // 24小时
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getSubject();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private Boolean isTokenExpired(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody().getExpiration().before(new Date());
}
}
需要注意的是,这里的密钥建议放在外部配置文件中,比如 application.yml,不要硬编码在代码里。
Step 4:创建 JWT Filter 拦截 Token
为了让 Spring Security 拦截每个请求中的 Token 并进行验证,需要自定义一个 Filter:
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
filterChain.doFilter(request, response);
}
}
这个 Filter 会自动识别带有 Bearer 头的 Token,并解析验证,验证成功后设置 Security 上下文的 Authentication 对象,这样后续鉴权就能顺利执行。
Step 5:配置 SecurityConfig 类
最后在配置类中注册 Filter 和其他权限策略:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/login").permitAll()
.anyRequest().authenticated();
}
}
这段配置做了几件重要的事情:
- 禁用了 CSRF(因为是前后端分离的接口系统)
- 设置为无状态 Session,配合 JWT 使用更合适
- 添加了 Token 过滤器
- 开启了方法级别的权限控制(
prePostEnabled = true),方便后续使用@PreAuthorize注解控制权限
踩坑经验分享:那些让我掉头发的小问题

在实际开发过程中,我也踩了不少坑,下面几个问题是比较典型的:
❌ Token 验证失败:时间戳超前?
刚开始测试的时候,发现生成的 Token 经常提示“已过期”。调试才发现:
Date expiration = new Date(System.currentTimeMillis() + EXPIRATION);
这个时间其实比服务器系统时间快了一点点,结果导致验证的时候报错了。解决办法很简单:生成 Token 的时候留一点容错空间。
后来改成这样:
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION - 10000))
减去10秒,避免因网络延迟或时间同步问题导致的误判。
❌ 角色权限不起作用:忘记 ROLE_ 前缀?
在使用 @PreAuthorize("hasRole('ADMIN')") 的时候一直不生效,后来发现是因为我们在构造 SimpleGrantedAuthority 的时候没加 ROLE_ 前缀。正确写法应该是:
new SimpleGrantedAuthority("ROLE_ADMIN")
或者你也可以在注解中写成:
@PreAuthorize("hasAuthority('ADMIN')")
区别在于,前者是角色(Role),后者是权限(Authority)。而 Spring Security 的 hasRole 会默认加上 ROLE_ 前缀查找。
❌ Filter 顺序错误导致无法生效?
Spring Security 的过滤器链是有严格顺序的,如果你把 Token Filter 放在错误的位置,可能就完全失效了。例如:
http.addFilterBefore(jwtRequestFilter, SomeOtherFilter.class);
应该确保放在 UsernamePasswordAuthenticationFilter 之前,才能保证在认证之前先进行 Token 检查:
.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
实施后的效果与收益
项目上线后运行稳定,基本满足所有安全需求:
| 功能 | 实现情况 |
|---|---|
| 登录认证 | ✅ 完善 |
| 角色分级控制 | ✅ 支持接口级别权限 |
| Token 生效/失效控制 | ✅ Redis 黑名单机制 |
| 密码加密 | ✅ 使用 BCrypt 加密 |
| 多租户预留 | ✅ 可扩展 |
性能方面,由于采用了 JWT 无状态机制,系统压力相对较小。通过 Redis 缓存 Token 黑名单,也有效防止了 Token 被恶意复用。
运维上我们也加了一些日志埋点,在网关层记录每次请求的来源、用户ID和操作类型,方便后续审计。
一些经验和建议
这是我第一次大规模使用 Spring Security,回头看确实有很多可以改进的地方,但也积累了一些宝贵的经验:
💡 1. 不要一开始就追求完美,先跑起来再说
很多同事刚开始会觉得 Security 的结构太复杂了,不知道从哪儿下手。我的建议是:先搞通一个最小可运行流程,再逐步扩展功能。比如先实现登录+ Token 认证,再加入角色权限、黑名单、登出等功能。
💡 2. 结合日志和 Debug 看上下文变化
Security 的流程很复杂,可以通过打印 SecurityContext 的内容来观察认证是否成功:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
System.out.println(auth);
同时记得在开发阶段打开 DEBUG 日志:
logging:
level:
org.springframework.security: DEBUG
💡 3. 做好 Token 过期与刷新机制
目前我们还没有做 Token 的自动刷新逻辑,下次打算结合 Refresh Token 实现双 Token 机制。这也是很多线上系统的常见做法。
💡 4. 注意生产环境的安全加固
以下是一些生产环境中推荐的做法:
- 禁用默认的 /error 页面,防止泄露敏感信息;
- 限制登录失败次数并锁定账户,防范暴力破解;
- 使用 HTTPS,避免中间人攻击;
- 定期更换 JWT 秘钥,增强 Token 安全性。
写在最后:安全感来自于扎实的基本功
通过这次项目的实践,我对 Spring Security 的整体架构有了更深的理解。它不是一个简单的拦截器工具包,而是一个真正意义上的安全框架。它的设计哲学强调“开箱即用”与“高度可定制”,既适合快速搭建,也能支撑复杂的企业级安全需求。
如果你刚开始接触 Spring Security,别担心它的复杂性,多动手调试、结合真实项目练习,你会发现它其实非常强大也很友好。
最后我想说,安全从来不是“加个过滤器”那么简单的事。它是一种思维,一种对未知风险的敬畏。而我们能做的,就是在每一次开发中多考虑一步,让系统更加健壮可靠。
希望这篇文章对你有帮助,也欢迎留言交流~
如有任何技术问题,欢迎关注我并在评论区留言讨论!

评论 0