Spring Security基础:从踩坑到搭建一套实用的安全认证系统
引言:为什么选择Spring Security?

最近公司有一个新项目启动,需要搭建一个后台管理系统,其中用户权限和安全认证是核心模块之一。作为技术负责人,我第一时间想到的就是使用 Spring Security 来处理这块逻辑。
在以往的项目中,我经历过手动写登录、验证、权限控制的痛苦过程 —— 代码冗余、容易出错,维护起来非常头疼。而这次我们决定用 Spring Security,不仅是因为它是主流方案,更因为它提供了丰富而灵活的功能支持,比如 JWT、OAuth2、方法级别权限控制等,能够很好地支撑起系统的安全性需求。
但说实话,虽然 Spring Security 很强大,但它的学习曲线也不算太低,尤其是在实际项目落地时,很多细节并不是官方文档里几句话就能解释清楚的。这篇文章就结合我在真实项目中的经历,分享一下如何快速搭建一个实用且可控的安全认证系统。
项目背景与挑战:小团队的“大”需求

这个项目是一个面向企业的 SaaS 平台,主要为中小型企业提供数据管理服务。前端是 React 单页应用,后端基于 Spring Boot 构建 RESTful 接口,部署在 Kubernetes 上。
系统上线前的关键阶段,我们需要完成以下任务:
- 用户注册、登录
- 角色与权限管理(RBAC)
- 前后端分离下的跨域处理
- 使用 JWT 实现无状态鉴权
- 敏感接口的方法级权限控制(例如:只能管理员访问删除接口)
听起来功能很基础,但作为一个五人左右的小团队来说,要在两周内把这些模块搭好并交付测试,并不是一件轻松的事情。
特别是几个棘手的问题摆在眼前:
- 怎么让 Spring Security 快速上手?
- JWT 怎么集成进 Spring Security 的过滤链?
- 不同角色怎么动态控制 API 访问权限?
- 生产环境中有哪些常见问题需要注意?
带着这些疑问,我和团队开始了一场“实战演练”。
解决方案:一步步搭建 Spring Security 安全框架

第一步:选型与架构设计
我们最终确定使用 Spring Security + JWT + RBAC 权限模型,整个后端采用分层结构设计:
Controller → Service → Repository
同时,在数据库层建立如下几张关键表:
user:用户基本信息(用户名、密码、邮箱等)role:角色信息(如 ADMIN、USER、GUEST)permission:权限信息(如 CAN_DELETE, CAN_EDIT)user_role,role_permission:关联表实现多对多关系
有了这套基础的数据结构后,就可以进行权限的细粒度控制了。
第二步:引入 Spring Security
首先是在 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>
这里我们用了 JJWT 这个轻量级库来生成和解析 JWT token。
第三步:构建自己的 WebSecurityConfig 类
这是整个 Spring Security 配置的核心类。我们自定义了一个配置类继承 WebSecurityConfigurerAdapter(注意:如果你用的是 Spring Boot 2.7+,建议使用最新的方式配置,而不是继承该类)。
大致内容如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
private final UserService userService;
public WebSecurityConfig(UserService userService) {
this.userService = userService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
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(new JwtAuthenticationFilter(userService), UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated();
}
}
这段配置做了几件重要的事情:
- 禁用了 CSRF 保护(因为我们用的是无状态的 JWT,不需要 session)
- 设置会话策略为 STATELESS,表示不创建 session
- 添加了一个自定义的
JwtAuthenticationFilter,用于拦截请求并校验 token - 对
/login路径放行,允许未认证用户访问登录接口
第四步:实现 JWT 登录流程
我们的登录流程大致如下:
- 用户发送 POST 请求到
/login,携带 username 和 password; - 后端调用
AuthenticationManager.authenticate()进行身份验证; - 成功后返回一个 JWT Token;
- 前端将 token 存入 localstorage;
- 后续请求在 Header 中带上 token,由
JwtAuthenticationFilter拦截解析; - 成功解析后注入当前用户信息到 Spring Security 的上下文中。
这部分的核心在于构造一个实现了 UserDetailsService 的类,以及编写一个 JWT 工具类。
自定义 UserDetailsService 示例:
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
List<GrantedAuthority> 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);
}
}
JWT 工具类示例:
@Component
public class JwtUtils {
private String secret = "your-secret-key-here";
private Long expiration = 86400000L; // 24h
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.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) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername()) && !isTokenExpired(token);
}

private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getExpiration();
return expiration.before(new Date());
}
}
有了这两部分,就可以配合前面的 Filter 使用了。
第五步:添加权限控制
我们希望做到根据用户的角色,控制其能访问哪些接口。Spring Security 提供了两种方式:
- URL级别的权限控制(
.antMatchers("/admin/**").hasRole("ADMIN")) - 方法级别的注解控制(
@PreAuthorize("hasRole('ADMIN')"))
我们采用了后者,因为它更贴近业务逻辑,也更容易做细粒度控制。只需要在主类加上注解即可开启支持:
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Application {
...
}
然后在 Controller 或 Service 方法上直接加:
@PreAuthorize("hasRole('ADMIN')")
public void deleteSomething(Long id) {
// 只有拥有 ADMIN 角色才能执行此方法
}
这种方法简单高效,适合我们在开发过程中随时调整权限需求。
实施过程中的几个难点与解决方案
难点一:跨域问题处理
前后端分离之后,最头疼的莫过于跨域问题。一开始没有正确设置 CORS 导致所有请求都被浏览器拦截,前端小伙伴一度崩溃 😂。
后来我们在 WebSecurityConfig 中加入全局跨域配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().configurationSource(request -> {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowCredentials(true);
config.setAllowedHeaders(Arrays.asList("*"));
return config;
})
.and()
// 其他配置略...
}
同时,为了保证 cookie 和 token 的传递不受影响,前端也需要在请求中设置:
axios.defaults.withCredentials = true;
解决了这一问题后,后续一切正常。
难点二:登录成功后的 Token 返回格式
刚开始返回的是纯字符串,结果前端拿到后不知道怎么塞进 header。于是我们统一了响应体格式,返回 JSON:
{
"token": "xxxxx.yyyy.zzzz",
"username": "john_doe"
}
这样前端可以直接解析,并存储 token 到 localStorage。
难点三:JWT 的过期时间管理
我们给 JWT 设置了默认有效期为 24 小时。在前端侧我们通过定时检查 token 是否快过期的方式提醒用户重新登录。
同时,也可以考虑加入 Refresh Token 机制,但由于当时时间有限,再加上用户活跃周期较短,暂时没有接入。
最终效果与收益总结
经过一周的开发与优化,我们顺利完成了安全认证模块:
- 支持用户注册、登录、权限分级控制;
- 所有敏感接口都实现了角色限制;
- 后端不再关心具体的鉴权逻辑,完全交给 Spring Security 处理;
- 整套系统可以很方便地扩展成 OAuth2、SSO 等模式。
更重要的是,在上线后的两个多月中,没有出现一次因安全或权限导致的漏洞问题。
团队内部反馈良好,大家都觉得这是一次“值得”的重构尝试。
经验分享:给后来者的几点建议
1. 不要试图自己造轮子
安全相关模块是非常容易出问题的地方。与其花大量时间去写登录验证逻辑,不如直接使用 Spring Security 这样的成熟方案。它背后有很多安全专家在持续优化,比你我都靠谱得多。
2. 权限设计尽早规划,后期改成本高
RBAC 是经典的权限模型。在初期就应该设计好角色与权限之间的关系,避免后面接口越写越多,权限控制越来越乱。
3. 开发阶段关闭严格权限控制,方便联调
我们在本地环境会临时放开某些权限限制,只保留基本的身份认证。真正上线前再打开全部控制开关。这样可以减少调试阶段的繁琐工作。
4. 生产环境要监控安全日志
Spring Security 本身就提供了不少日志输出能力,可以通过日志系统收集异常登录行为,及时发现潜在攻击行为。
5. 定期更新密钥和加密方式
尤其是像 JWT 中的签名密钥,不要硬编码到代码里,可以用配置中心或环境变量管理。并且定期更换密钥,提升整体安全性。
写在最后:安全永远在路上
Spring Security 作为一个经典的安全框架,虽然功能强大,但也并不完美。特别是在面对复杂的业务场景时,仍然需要我们合理取舍和定制开发。
但我始终相信:安全是软件产品的基石,不能因为赶工或偷懒而牺牲。
通过这次项目实践,我对 Spring Security 的理解也更深入了一层,也更加体会到“用得好”比“知道得全”更重要。
如果你也在搭建自己的后台系统,不妨试试这套组合拳 —— Spring Security + JWT + RBAC + 方法权限,真的会让你事半功倍。
如果这篇文章对你有所帮助,或者你也有类似的技术实践,欢迎留言交流。我是老李,一名坚持“用工程思维解决实际问题”的 Java 开发者。我们下篇文章见!

评论 0