Spring Security 基础:快速搭建安全认证系统
引言:从一次权限失控说起

记得那是我刚加入一个电商平台创业团队的时候。我们开发了一个简单的后台管理系统,用户可以登录进去查看订单、管理商品和设置运营活动。初期为了快速上线,我们在安全性上做了一些妥协——比如直接把密码明文存到数据库里,权限控制也只是基于简单字段判断。
结果没多久就出问题了。有一次测试环境的数据库被误导出了生产数据,里面有用户的手机号和密码明文,差点造成大规模信息泄露。更糟的是,有同事不小心调用了错误接口,给某个普通管理员开了超级权限,导致他修改了核心配置,系统出现大面积异常。
这件事给我们敲响了警钟。是时候认真考虑系统的安全架构设计了。我们决定引入 Spring Security 来重构整个认证授权流程。也正是从这个时候开始,我真正意识到一个成熟的安全框架对于后端服务的重要性。
今天我就来分享一下那次实战经验,谈谈如何用 Spring Security 快速搭起一个安全可靠的认证系统。希望能帮大家少走弯路。
项目背景:我们需要怎样的权限体系?

我们的系统主要是面向 B 端商家的后台管理系统,涉及几个关键角色:
- 普通店员(只读)
- 运营人员(可新增商品和修改部分配置)
- 店长(有更多操作权限)
- 超级管理员(全权管理)
每个角色对应不同的菜单和接口访问权限。早期的做法是在每次请求进入业务逻辑前检查身份字段,但这种做法存在太多隐患:
- 权限校验散落在各个 Controller 中,维护成本高
- 接口可能被绕过或伪造调用
- 密码存储不安全,缺乏加密机制
- 缺乏统一的登出、Token 刷新等机制
这些痛点最终促使我们采用 Spring Security 构建统一的安全层。
遇到的挑战:安全框架不是拿来就能用

刚开始接入 Spring Security 的时候,遇到不少坑。虽然官方文档很详细,但面对实际业务场景时还是会手忙脚乱。特别是在以下几个方面卡了很久:
1. 认证流程和 Filter 链理解不清
Spring Security 是基于 FilterChainProxy 的,里面嵌套了很多内置的过滤器。一开始我们搞不清楚哪些是我们需要自定义的,哪些可以直接使用默认实现。
小插曲:曾经误删了
UsernamePasswordAuthenticationFilter,导致登录接口永远返回 403,调试了整整半天才发现问题所在。
2. Token 支持需要定制化改造
我们想使用 JWT 来做无状态认证,而 Spring Security 默认支持的是 Session 方式。这意味着需要替换掉默认的认证流程,并在拦截器链中添加 Token 解析和验证环节。
3. 数据库权限结构不够灵活
原有的用户表只有 role 字段,权限都是写死的常量值。为了更灵活地支持细粒度的权限控制,必须重新设计一张权限表,建立用户-角色-权限三级关系。
4. 登录失败处理、自动登出等功能缺失
一开始没有考虑异常情况的处理,比如登录失败次数限制、账户锁定、Token 到期提示等等。这些都是用户体验的关键点,不能忽视。
解决方案:分阶段构建安全系统

我们采取了分阶段的策略来接入 Spring Security。先解决基本认证和权限控制的问题,再逐步完善 Token 流程和安全防护细节。
第一阶段:实现基础的 Username/Password 认证
首先搭建最基础的认证能力,基于用户名密码登录。
1. 用户实体与数据库设计
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password; // 使用 BCrypt 加密
private boolean enabled;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
权限信息存储在数据库中,采用多对多的方式关联到角色表,这样便于后续扩展。
2. 自定义 UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true, true, true,
getAuthorities(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> getAuthorities(Collection<Role> roles) {
return roles.stream()
.flatMap(role -> role.getPermissions().stream())
.map(permission -> new SimpleGrantedAuthority(permission.getName()))
.collect(Collectors.toList());
}
}
这里我们把权限也一并加载进来,方便后续在接口级别做权限控制。
3. 配置 WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@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();
}
}
这部分代码看起来不多,但当时花了我们不少时间去调试和优化。尤其是 addFilterBefore 和 Session 管理策略的设置,直接影响到后续的 Token 实现逻辑。
第二阶段:引入 JWT,实现无状态认证
随着团队规模扩大,微服务架构逐渐成型,传统的 Session 模式已经不能满足需求。我们开始接入 JWT(JSON Web Token),实现无状态的认证机制。
1. 设计 Token 结构
我们选择使用 JJWT 库来生成和解析 Token:
String token = Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", authorities)
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION))
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
2. 自定义认证过滤器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && validateToken(token)) {
Authentication auth = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
// ... 省略提取和验证逻辑
}
这个类负责拦截请求、解析 Token,并将认证信息注入上下文。
3. 处理登录过程
登录成功后返回 Token:
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
String token = jwtUtils.generateJwtToken(authentication);
return ResponseEntity.ok().body(new JwtResponse(token));
} catch (AuthenticationException e) {
throw new RuntimeException("登录失败");
}
}
这个接口会触发 Spring Security 的认证流程,然后返回 Token。
第三阶段:精细化权限控制与异常处理
权限控制不只是“能访问”或“不能访问”,更重要的是细粒度的控制。我们做了以下几件事:
1. 启用方法级权限控制
@EnableGlobalMethodSecurity(prePostEnabled = true)
这样就可以在接口或者 Service 方法上加上注解:
@PreAuthorize("hasAuthority('ORDER_MANAGE')")
@GetMapping("/orders")
public List<OrderDTO> getAllOrders() {
return orderService.findAll();
}
这种方式非常灵活,权限变更不需要改动接口,只需要更新数据库中的权限配置即可。
2. 统一异常处理
我们还封装了一个全局异常处理器,捕获认证、授权过程中可能出现的异常,并统一返回格式:
@RestControllerAdvice
public class AuthExceptionHandler {
@ExceptionHandler(value = {InsufficientAuthenticationException.class})
public ResponseEntity<ErrorResponse> handleUnauthenticated() {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("未登录,请先登录"));
}

@ExceptionHandler(value = {AccessDeniedException.class})
public ResponseEntity<ErrorResponse> handleForbidden() {
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("权限不足,禁止访问"));
}
}
效果总结:系统更加健壮安全
完成这一轮重构后,我们收获了不少好处:
- 权限集中管理:不再散落在各处,全部通过数据库和注解进行控制。
- 登录和认证流程标准化:统一使用 Token,所有服务都能复用这套流程。
- 增强安全性:BCrypt 存储密码 + Token 有效期控制 + 黑名单登出机制。
- 易于扩展:权限变更只需修改配置,无需改动代码。
- 异常反馈友好:用户知道哪里出错了,运维也能更快排查问题。
最重要的是,团队成员在后续开发中也不再担心权限逻辑的问题,可以专注于业务本身。
经验分享:我在项目中总结的几点建议
以下是我在这次 Spring Security 实战中学到的一些宝贵经验,分享给大家:
1. 别一开始就追求完美,边试边改更高效
我们最初以为要一步到位搞定一切,结果光是理解 Filter 链就花了一周多。后来调整思路,先跑通基本流程,再逐步替换 Token、加异常处理,反而进展更顺利。
2. 把安全当作系统架构的一部分,而不是附加功能
很多项目前期忽略安全设计,后面补救起来成本翻倍。应该在系统规划阶段就把认证、鉴权作为核心模块来考虑。
3. 权限控制要尽量“声明式”而非“命令式”
不要在业务逻辑里手动 if-else 判断权限。而是使用 Spring Security 的注解和表达式方式,降低耦合度。
4. 多注意生产环境的安全防护
比如:
- 登录接口增加滑块验证码或图形验证码
- 限制登录失败次数(可以用 Redis 记录尝试次数)
- Token 设置合理有效期 + 自动刷新机制
- 所有通信启用 HTTPS
- 定期轮换密钥(如 JWT_SECRET)
5. 要记录日志,方便追踪异常登录行为
我们在系统中加入了认证相关日志埋点,记录登录、登出、Token 校验失败等行为,这对排查问题很有帮助。
6. Spring Security 的集成需要谨慎测试
别忘了:
- 单元测试认证流程是否完整
- 测试不同权限用户的访问差异
- 测试并发登录、多设备同时在线等场景
总结:Spring Security 不只是工具,更是安全思维的体现

回过头来看,在那段时间我们不仅仅是在接入一个安全框架,更是在建立起一套完整的权限模型和安全理念。Spring Security 提供的是一个强大的骨架,但最终能跑多稳、多灵活,还是看我们怎么用它来组织自己的业务。
对于开发者来说,学会使用 Spring Security 已经是一个基本要求;而真正掌握它的原理、灵活运用它的机制,才能在构建复杂系统时游刃有余。
如果你现在正在搭建新项目,不妨尽早引入 Spring Security。哪怕是从最基本的用户名密码认证做起,也比日后亡羊补牢要好得多。
希望这篇文章对你有所帮助。如果有疑问或者想交流具体实现,欢迎留言讨论!
如果你觉得这篇内容对你有价值,不妨点个赞,关注我的技术博客,一起交流进步 🙌
(全文约 3878 字)

评论 0