从零搭建一个安全认证系统:我在Spring Security上的实战经验分享
大家好,我是一个有着多年Java后端开发经验的工程师。今天想和大家分享一次在项目中使用 Spring Security 实现用户认证与权限控制的经历。这个过程不仅让我重新梳理了 Spring Security 的核心机制,也让我意识到安全设计在项目初期就应该被重视。
这篇文章会结合我亲身经历的一个实际项目——某金融类管理平台的安全体系构建,来详细讲解如何快速上手 Spring Security,并实现一个基础但实用的安全认证系统。
项目背景与我的困惑

去年年初,我们公司接了一个内部管理系统重构的需求。这个系统是为公司多个业务部门服务的后台平台,涉及客户信息、账户审核、权限分配等敏感操作。原本的系统虽然功能齐全,但在安全性方面存在诸多漏洞:
- 用户登录没有加密
- 所有接口都不需要鉴权,可以随意访问
- 没有任何 RBAC(基于角色的权限控制)机制
这些问题让我们技术团队感到非常不安。作为项目的负责人之一,我决定趁这次重构的机会,把系统的安全架构好好设计一下。
当时我们选择了 Spring Boot + Spring Security 的方案。一方面是因为我们的主技术栈就是 Java,另一方面,Spring Security 作为老牌的安全框架,在社区和文档支持上都很成熟。但说实话,刚接手的时候我对它的理解还停留在“加个依赖就能用”的阶段,真正要深入去做,才发现这事儿并不简单。
遇到的挑战

1. 权限模型怎么设计?
我们要支持的角色包括:普通用户、业务管理员、风控审核员、超级管理员等。不同角色能操作的模块和资源各不相同。
一开始我打算直接通过角色名来判断是否有权限访问某个接口。比如:
if (user.getRole().equals("ADMIN")) {
// do something
}
这种方式写起来快,但很快就被测试同学打回来了:“你这么写,如果以后加新角色怎么办?改一堆代码吗?”后来我才明白,权限系统的设计必须具备良好的扩展性和可配置性。
2. 如何统一处理登录流程?
原有的登录逻辑是裸写的 Controller 接收用户名和密码,然后调数据库比对。这样的方式显然不符合安全规范,也无法复用 Spring Security 提供的强大功能,例如自动防爆破、并发限制、Token 登录等等。
我们需要一种既能满足登录流程标准化,又能灵活扩展的方式。
3. 多种认证方式并存的问题
随着项目上线时间临近,产品提出后续可能会支持短信验证码、OAuth 登录等功能。这让我更头疼了:怎么让 Spring Security 支持这些未来的扩展需求?难道每次都要重写整个安全配置?
解决思路与实现方案

我花了几天时间研究 Spring Security 的官方文档,并结合几个实际案例进行实践。最终,我们采用了以下这套结构清晰、可扩展的认证授权体系。
核心组件分工明确
我们将整个安全模块拆分为以下几个部分:
AuthenticationManager:负责身份验证的整体调度UserDetailsService:自定义用户加载逻辑PasswordEncoder:统一加密策略FilterChain:用于拦截请求,执行鉴权逻辑JwtUtils:用于 Token 的生成和校验(后面我们会用 JWT)
分层解耦设计
为了方便后期接入其他认证方式(如 OAuth、短信验证码),我们抽象出 AuthenticationProvider 层,这样不同的登录方式都可以注册进系统中,统一交给 Spring Security 处理。
比如,我们可以先支持账号密码登录,未来再添加 SmsCodeAuthenticationProvider。
统一的权限表达方式:基于 MethodSecurityExpression
我们没有直接用 .antMatchers() 来配置 URL 级别权限,而是启用了 @PreAuthorize 注解,通过 SpEL 表达式在方法级别上做权限控制。
这样做的好处是:
- 权限与业务逻辑绑定紧密,修改时不易遗漏
- 更适合前后端分离下的接口级权限粒度
- 易于配合 AOP 做审计日志等操作
数据库设计优化
我们在用户表的基础上,增加了三张关系表:
users (
id BIGINT PRIMARY KEY,
username VARCHAR,
password VARCHAR,
enabled BOOLEAN
)
roles (
id BIGINT PRIMARY KEY,
name VARCHAR
)
user_roles (
user_id BIGINT,
role_id BIGINT
)
并在查询时通过 LEFT JOIN 把用户的权限一并查出来,组装成 Spring Security 要求的 GrantedAuthority 对象。
关键代码实现与配置示例

下面我挑几个关键代码片段,让大家直观感受下 Spring Security 的使用方式。
自定义 UserDetailsService
@Service
public class UserDetailsServiceImpl 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("用户不存在");
}
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities
);
}
}
安全配置类 SecurityConfig
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsServiceImpl;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsServiceImpl).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/**").permitAll()
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
使用注解控制权限
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('ADMIN') or #id == authentication.principal.id")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
// ...
}
}
这段代码的意思是:要么是管理员,要么是你自己,才能访问这个接口。
是不是比 .antMatchers() 的方式更有针对性?
开发过程中踩过的一些坑
1. 忘记启用 @EnableGlobalMethodSecurity 注解
刚开始我只用了 @PreAuthorize,但是发现不起作用,调试了半天才发现是因为没有加上那个注解。建议新手一定要记得启用!
2. 密码加密问题导致登录失败
一开始我没有设置 PasswordEncoder,结果登录一直失败。后来才知道 Spring Security 在比较密码时默认会调用 PasswordEncoder.matches() 方法,如果没有指定,它就会抛异常或者始终不匹配。
3. CORS 和 CSRF 冲突
我们在前端引入了 Vue,并跨域访问后端接口。起初没关掉 CSRF,结果每次 POST 请求都被拦下来了。这个问题折腾了好久,最后才想到去安全配置里关闭 CSRF。
上线后的效果与收益
项目上线后,整体效果还是挺不错的。
- 用户登录更加安全,所有密码都通过 BCrypt 加密,避免明文存储
- 权限控制更精细化,可以自由组合角色和菜单项
- 可扩展性强,后面我们只花半天就接入了短信验证码登录
- 运维反馈良好,日志中能看到完整的登录行为记录,便于审计排查
更重要的是,我们在后续迭代过程中也没有因为权限问题返工太多。相比之前那种“哪里出错修哪里”的模式,现在整套安全体系已经形成了一个相对稳定的内核。
我的经验总结
如果你也在考虑使用 Spring Security 构建认证授权系统,以下是几点我真心建议的 Tips:
✅ 提前规划好权限模型
权限不是靠写 if 判断来控制的,而应该从数据结构、接口设计、甚至 UI 级别统一规划。推荐使用 RBAC 或者 ABAC 结构,越早搭好底座,越不容易翻车。
✅ 尽量用注解代替 URL 匹配
URL 变化大、维护成本高。使用 @PreAuthorize 更加灵活,特别是在 API 很多的情况下。
✅ 认证流程要预留好扩展口子
不要一开始就把自己局限在账号密码登录这种单一方式里。提前设计好 Provider 的注册机制,未来对接第三方登录就会轻松很多。
✅ 用 JWT 替代 Session(前提是有移动端需求)
如果是前后端分离项目,Session 不太友好。JWT 是一个更现代的选择,而且兼容性更好。当然,要注意 Token 过期、续签、黑名单等机制。
最后一点感悟
其实,做安全这件事情说难也不难,说容易却也有门槛。Spring Security 为我们提供了很好的轮子,但能不能用得好,还是要看对整个认证和授权体系的理解深度。
在我参与过的多个项目中,只有这一次是从头开始就把安全架构做好了的。事后看来,虽然前期花了不少时间调研和设计,但从长远来看是值得的。
如果你还在用裸写的 Controller 做登录,不妨停下来想想:你的用户数据真的足够安全吗?你的系统有没有可能被人绕过接口权限访问?
希望这篇文章能给大家一些启发,少走点弯路。
下次我会分享我们是如何用 Redis 实现 Token 黑名单机制的,有兴趣的朋友可以关注一下。
共勉!

评论 0