Spring Security实战:从零搭建一个安全认证系统
引言:为什么要重新设计权限认证系统?

去年,我在一家做在线教育平台的公司担任后端架构师。当时我们的用户系统还处于快速迭代阶段,早期为了赶进度,账号模块用了最简单的 Session 登录机制,甚至没有引入像 OAuth2 或 JWT 这样的标准方案。
随着业务的发展,我们开始接入第三方应用、开发 API 接口供外部调用,也遇到了越来越多的安全问题——例如:
- 用户登录信息存储方式不安全,容易被中间人攻击;
- 第三方平台调用时缺乏统一的令牌体系;
- 权限粒度控制得不够细,管理混乱;
- 没有清晰的接口访问控制策略,系统面临潜在风险。
于是我们决定重构整个认证授权模块,目标是建立一套既能满足当前需求,又能支持未来扩展的安全认证系统。而最终选择的技术方案就是基于 Spring Security + OAuth2 + JWT 的组合。这篇文章会以这次项目为背景,结合我在这个过程中踩过的坑和学到的经验,和大家一起过一遍如何从零搭建一个 Spring Security 基础的安全认证系统。
一、项目背景与实际挑战


系统现状
原来的用户登录逻辑非常简单粗暴:
// 登录接口伪代码
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
if (userService.authenticate(request.username, request.password)) {
String token = UUID.randomUUID().toString();
redisService.set("session:" + token, request.username);
return ok(token);
}
throw new AuthException("用户名或密码错误");
}
然后每个需要鉴权的接口都通过自定义注解去拦截 token:
@InterceptorType(Authenticated.class)
public class AuthInterceptor implements HandlerInterceptor {...}
虽然这种实现方式在初期还能凑合用,但到了中后期已经很难维护了,权限模型也不清晰,而且根本谈不上可扩展性。
新系统的期望目标
我们希望通过重构带来以下几个提升:
- 统一的身份认证入口
- 标准化的 Token 生成和验证机制(使用 JWT)
- 灵活的权限管理策略(包括 URL 级别的权限控制)
- 对接第三方服务的能力(OAuth2 Client / Resource Server 支持)
- 高性能且具备容灾能力的身份中心
二、技术选型与架构设计思路

综合我们团队对 Java 技术栈的熟悉程度,以及目前 Spring 生态圈的发展情况,我们最终选择了:
- Spring Boot + Spring Security 作为核心框架;
- JWT 用于 Token 的生成与校验;
- OAuth2 协议 提供开放 API 的接入能力;
- 数据库选用 PostgreSQL 存储用户、角色、权限等配置数据。
整体架构分为三个层级:
1. 授权服务器(Auth Server)
职责包括:
- 处理用户登录、注销请求;
- 颁发 JWT Token;
- 提供 OAuth2 Client 认证流程;
- 管理权限分组(Role-Based Access Control, RBAC);
部署上采用双节点加 Nginx 负载均衡,Token 使用 Redis 缓存保存吊销状态(Revoked Tokens),并配合自动刷新机制。
2. 安全网关(Security Gateway)
这部分不是本篇重点,不过值得一提的是我们在 Zuul/Feign 上做了统一鉴权处理,确保所有服务间的调用都要带上有效的 Token,并在网关层完成 URL 级别的权限拦截,减少下游服务的负担。
3. 各个微服务(Resource Server)
这些服务不再自己处理登录逻辑,而是依赖 Auth Server 提供的 Token 做身份识别和权限判断。
三、快速上手:从零搭建 Spring Security 基础认证系统
接下来我会一步步带大家搭一个最基础的 Spring Security 安全认证原型,让你了解它是怎么工作的,同时也会讲清楚一些关键点和注意事项。
示例源码托管于 GitHub:spring-security-demo(请替换成自己的链接)
Step 1: 初始化 Spring Boot 工程
使用 start.spring.io 创建一个 Spring Boot 项目,添加以下依赖:
- Spring Web
- Spring Security
- Spring Data JPA
- PostgreSQL Driver(或者其他你习惯用的 DB)
Step 2: 用户表结构设计
这里我采用了经典的 RBAC 模型:
用户表 users
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigserial | 主键 |
| username | varchar | 用户名 |
| password | varchar | 加密后的密码 |
| enabled | boolean | 是否启用 |
角色表 roles
| 字段名 | 类型 | 描述 |
|---|---|---|
| id | bigserial | 主键 |
| name | varchar | 角色名称,如“ROLE_ADMIN” |
用户-角色关系表 user_roles
| user_id | role_id |
|---|
这样设计可以灵活地给用户赋予多个角色,也可以根据角色来做权限分级。
Step 3: 实现基本的登录逻辑
我们先让 Spring Security 能跑起来,然后逐步增强功能。下面是 SecurityConfig 的一个初始版本:
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(withDefaults())
.logout(withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService(UserRepository userRepository) {
return username -> {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true,
true,
true,
AuthorityUtils.createAuthorityList("ROLE_USER"));
};
}
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这个例子展示了最基本的认证过程:用户登录之后,默认分配了一个 ROLE_USER 权限,并允许访问非 /public/** 开头的路径。
这时候你可以运行项目,访问任何页面都会跳转到默认的登录页。当然我们现在还没有注册逻辑,所以需要手动插入一条用户记录进数据库:
INSERT INTO users (username, password, enabled) VALUES ('alice', '$2a$10$IH9T9tqZcBQ7JhRj8zXKCeO2VvL6yS5nFzPdWzYfXeZbU7l5gkGGO', true);
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
INSERT INTO user_roles(user_id, role_id) VALUES(1, 1);
其中 $2a$... 是用 BCryptPasswordEncoder.encode() 方法生成的测试密码。
四、加入 JWT 和无状态认证
随着 RESTful API 的普及,我们希望实现一种无状态的身份认证机制,也就是不依赖 Session,而是通过 Token 完成身份验证。
什么是 JWT?
JSON Web Token(JWT)是一种轻量级的身份凭证格式,它由三部分组成:
- Header:说明签名算法等;
- Payload(Data):包含用户信息,比如 ID、姓名、权限;
- Signature:用于防止篡改的签名字段。
使用 JWT 的优势包括:
- 无状态:适用于分布式部署;
- 跨域友好:比 Cookie 更适合前后端分离项目;
- 可扩展性强:可以通过 Claims 添加额外信息;
- 性能较高:不需要每次都查库验证 Token;
JWT 登录流程示意图
客户端
↓
POST /login → 返回 JWT
↓
在后续请求 Header 中带上 Authorization: Bearer xxxxx
↓
资源服务器验证 Token 合法性,提取权限信息
↓
判断是否允许访问该接口
现在我们就来改造一下刚才的系统,支持 JWT 登录流程。
Step 1:创建 JWT 工具类
我们可以基于开源的 jjwt 库实现 Token 的生成和解析:
<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>

@Component
public class JwtUtils {
private final String secret = "this_is_a_very_secret_key";
public String generateToken(String username, Collection<? extends GrantedAuthority> authorities) {
Map<String, Object> claims = new HashMap<>();
claims.put("authorities", authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + 3600000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String parseToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public Collection<SimpleGrantedAuthority> getAuthoritiesFromToken(String token) {
List<String> authorities = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.get("authorities", List.class);
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
Step 2:自定义过滤器链
我们需要替换掉默认的登录流程,改为返回 JWT Token。
创建一个继承 OncePerRequestFilter 的类,在每次请求的时候检查是否存在 Token:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = getTokenFromHeader(request);
if (token != null && jwtUtils.validateToken(token)) {
String username = jwtUtils.parseToken(token);
Collection<SimpleGrantedAuthority> authorities = jwtUtils.getAuthoritiesFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromHeader(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
并在 SecurityConfig 中将其添加进过滤器链:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/login").permitAll()
.anyRequest().authenticated());
return http.build();
}
Step 3:创建登录接口返回 Token
再写一个 Controller,用来处理登录请求并返回 Token:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtils jwtUtils;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
Authentication authenticate = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())
);
String token = jwtUtils.generateToken(authenticate.getName(), authenticate.getAuthorities());
return ResponseEntity.ok(token);
}
}
别忘了注册 AuthenticationManager:
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.build();
}
五、权限控制:从 Role 到 URL 级别细粒度限制
有了 JWT 之后,权限控制就可以通过 Token 里的 authorities 来实现了。不过很多时候我们还需要更细粒度的权限控制,比如只有特定角色才能访问某个 API。
这就要用到 Spring Security 的方法级别和接口级别的权限控制。
1. 接口级别控制(URL)
在 SecurityConfig 中,我们之前只是写了 .anyRequest().authenticated(),其实可以用 Ant Path 匹配更精确地控制路径:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/api/student/**").hasAnyRole("STUDENT", "TEACHER")
.requestMatchers("/api/teacher/**").hasRole("TEACHER")
.anyRequest().authenticated());
注意,这里有个小技巧:.hasRole() 会自动加上前缀 ROLE_,所以我们数据库里角色命名也要统一以 ROLE_ 开头,比如 “ROLE_ADMIN”。
2. 方法级别权限控制(AOP)
有时候我们要控制某一个方法能否执行,而不是某个路由。比如:
@PreAuthorize("hasRole('ADMIN') or #userId == authentication.principal.id")
public void deleteUser(Long userId) {
// 只有 ADMIN 或者 自己才能删除用户
}
要让这个生效,必须在启动类上加上:
@EnableGlobalMethodSecurity(prePostEnabled = true)
这个特性是基于 AOP 实现的,非常强大,推荐在 Service 层使用。
六、遇到的问题与解决经验分享
Q:用户修改密码后,已签发的 Token 如何失效?
这是 JWT 的一个常见痛点:因为 Token 是无状态的,签发出去就不能主动收回。解决方案有几种:
- 黑名单机制:将失效的 Token 存入 Redis 缓存,验证时先查缓存;
- 短生命周期 + Refresh Token:Token 生效时间设短一点(如 5 分钟),搭配 Refresh Token 延长有效期;
- 黑名单 + 缓存 Key TTL 设置:避免无限期保存黑名单,设置和 Token 相同的过期时间即可;
我们最终选择了第一种 + 设置 Key 的过期时间,这样既保障安全性又节省资源。
Q:如何做到不同服务之间 Token 可共用?
如果多个微服务共享同一个授权中心(Auth Server),只要他们使用相同的密钥(Secret),就能互相验证 Token 的合法性。因此建议在生产环境将 Secret 配置集中化,比如使用 Vault 或者 Spring Cloud Config 统一管理。
七、总结:这套架构带来的收益与思考
从最初的一个简单登录逻辑,到如今具备权限控制、OAuth2、JWT 等能力的完整认证系统,这个过程并不轻松,但也让我们收获颇丰:
- 提升了安全性:统一的 Token 标准,更强的身份校验机制;
- 增强了扩展能力:RBAC 模式 + 接口细粒度权限,方便未来扩展新业务;
- 降低了耦合度:各个服务只需要关心认证结果,无需参与认证过程本身;
- 提高了运维效率:通过黑白名单、监控日志、集中注销等功能,提升了排查问题效率。
最后:几点经验建议给后来者
- 不要重复造轮子,能用 Spring Security 就别自己写拦截器;
- 权限模型要尽早设计清楚,否则后期改起来代价很高;
- JWT 不等于万能钥匙,要考虑 Token 失效、续期等问题;
- 多做单元测试 & 集成测试,尤其是权限边界测试;
- 关注社区变化,Spring Security 从 5.x 到现在的 Boot 3.x,不少组件都有变化,升级需谨慎。
如果你也在做一个需要认证授权功能的 Spring Boot 项目,不妨试试上面这套方案,亲测可行,而且性能和扩展性都不错。欢迎留言交流你们的落地经验和遇到的难题 😊
附录资源推荐:
如有需要,我会持续更新配套示例项目源码及文档,欢迎 Star 关注!

评论 0