Spring Security基础:快速搭建安全认证系统
Spring Security 基础:快速搭建安全认证系统

引言:从一次重构说起
大概是一年多前,我参与了一个公司内部的后台管理系统的重构项目。原来的老系统是用 PHP 写的,架构老旧、安全性差得一塌糊涂,连用户权限都只能靠前端判断,后端几乎没有做任何校验。更别说登录安全、会话管理这些现代 Web 应用的标准操作了。老板对这套系统非常不满,要求我们在 3 个月内完成全部重构,并且“必须做到真正的安全认证”。
接到任务的时候,我心里其实挺没底的。虽然之前在其他项目里用过一些简单的认证逻辑,比如基于 token 的 JWT 验证,但真正系统性地落地一个安全体系,我还是第一次。
那段时间我查了很多资料,也踩了不少坑。直到最后,我们用了 Spring Security 搭建起了一套完整的、满足业务需求的安全认证机制。这篇文章就是想结合那次实战经验,讲讲我是怎么一步步搭建出这个系统的。
项目背景与挑战
新系统是典型的 Java 后台管理平台,使用 Spring Boot + Vue 的前后端分离结构。主要功能包括:
- 用户管理(创建、编辑、删除)
- 角色权限控制
- 接口访问控制
- 日志审计
- 登录认证和退出机制
安全方面的需求有几个关键点:
- 所有接口必须做身份认证和权限控制;
- 支持用户名密码 + 短信验证码双重登录;
- 用户登录失败次数超过一定限制要封号;
- 登录 session 要有过期机制,支持主动登出;
- 权限需要细粒度控制(RBAC 模型);
当时我们评估了多个方案,最终决定用 Spring Security 作为整个安全框架的核心。
技术选型与实现思路
为什么选择 Spring Security?
虽然 Shiro 也是一个很流行的 Java 安全框架,但从以下几个角度考虑,我们还是选择了 Spring Security:
- 更加成熟稳定,社区活跃,文档丰富;
- 对 RESTful 接口天然支持良好;
- 可高度定制,扩展性强;
- 提供丰富的开箱即用功能(如登录限制、CSRF 防护、OAuth2 集成等);
- 与 Spring Boot 整合极为顺畅;
总体设计思路
我们的整体认证流程大致如下:
用户登录请求 → 自定义 AuthenticationProvider 处理认证 → 成功后生成 Token 或设置 Session →
→ 请求携带凭证(Header/Bearer/Session) → FilterChainProxy 过滤链验证权限 →
→ 访问目标资源或返回 401/403
整个过程涉及几个核心组件:
UserDetailsService:用于加载用户信息;PasswordEncoder:密码加密解密;AuthenticationManager:负责认证流程;SecurityFilterChain:过滤器链配置;AccessDecisionManager:权限决策;LogoutHandler和LogoutSuccessHandler:注销处理;ExceptionTranslationFilter:处理异常情况(如未认证/无权限访问);
实战代码实践
接下来我会结合具体代码片段来说明如何搭建这样一个系统。
1. 添加依赖项
<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>
我们这里用到了 JWT,所以额外引入了 JJWT 相关的依赖。
2. 定义用户实体与权限模型
我们的用户表设计大致如下:
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
enabled BOOLEAN DEFAULT TRUE
);
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL
);
CREATE TABLE sys_user_role (
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
FOREIGN KEY (user_id) REFERENCES sys_user(id),
FOREIGN KEY (role_id) REFERENCES sys_role(id)
);
-- 权限粒度到菜单级别
CREATE TABLE sys_menu (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
menu_name VARCHAR(50),
permission VARCHAR(100) -- 如 system:user:read
);
这样我们就可以通过 RBAC 模型进行权限控制。
3. 配置 Security 核心类
我们定义了自己的 UserDetailsService 实现类:
@Service
public class UserDetailsServiceImpl 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<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
);
}
}
注意这里的权限格式要符合 Spring Security 的标准格式,如 ROLE_ADMIN 或自定义的权限字符串。
4. 自定义认证逻辑:支持多类型登录
我们实现了自己的 AuthenticationProvider:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserService userService;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginType = (String) authentication.getDetails();
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// 可以根据 loginType 判断是手机号登录还是普通账号登录
// 在这里调用不同服务去校验验证码或者密码
// 示例中假设只走用户名密码方式
User user = userService.loadUserByUsername(username);
if (!passwordEncoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("密码错误");
}
List<GrantedAuthority> authorities = new ArrayList<>();
user.getRoles().forEach(r -> authorities.add(new SimpleGrantedAuthority(r.getName())));
return new UsernamePasswordAuthenticationToken(username, password, authorities);
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
同时注册进 Security 中:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authenticationProvider(customAuthenticationProvider); // 注册自己的 provider
http.formLogin(formLogin -> formLogin.disable())
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationToken.class)
.authorizeHttpRequests(auth -> auth.anyEndpoint().authenticated());
return http.build();
}
5. JWT 校验过滤器

我们实现了一个轻量级的 JWT 校验 Filter:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = jwtUtil.resolveToken(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.getUsernameFromToken(token);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, new ArrayList<>());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
踩过的坑和解决方法
问题 1:跨域请求被拦截导致无法登录
由于我们用的是前后端分离架构,登录请求从 Vue 前端发送过来,一开始总被 Spring Security 拦截。调试半天发现是因为 CSRF 保护默认开启。
解决方案:
如果你是 REST API 架构,而且用了 Token 或者 Session 方式,可以直接关闭 CSRF。
.csrf(csrf -> csrf.disable())
当然如果你要用 Cookie + Session 的传统方式登录,那就不能关闭 CSRF,而是要在前端每次请求带上 _csrf token。
问题 2:登录成功不跳转或返回 JSON 错误
Spring Security 默认是面向浏览器页面的,登录成功会重定向。但我们这里是纯后端接口,需要返回 JSON。
解决方案:
自定义 AuthenticationSuccessHandler 和 AuthenticationFailureHandler:
.successHandler((request, response, authentication) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"message\":\"登录成功\"}");
})
.failureHandler((request, response, ex) -> {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write("{\"code\":401,\"message\":\"" + ex.getMessage() + "\"}");
});

问题 3:权限表达式写错导致 403
一开始我们在 controller 上写了类似 @PreAuthorize("hasAuthority('system:user:list')"),结果总是提示 403。
排查原因:
- 用户实际加载的权限不是字符串 "system:user:list",而是被自动加上了
ROLE_前缀; hasAuthority()和hasRole()的区别没有搞清楚;
修正方法:
- 明确使用
hasAuthority("system:user:list"); - 或者统一用
hasAnyAuthority(...); - 如果你非要用 Role 的方式,确保传入的权限是带
ROLE_前缀的字符串。
实施效果与收获
在项目上线后,我们这套安全体系表现得非常稳定。以下是我们总结的一些成果:
- 系统安全性得到了大幅提高,所有接口都必须通过认证;
- 权限粒度细化到菜单级别,管理员可以灵活配置;
- 登录失败限制机制有效防止了暴力破解;
- 使用 JWT 代替了传统的 Session,更适合分布式部署;
- 日志清晰记录了用户的登录行为和访问路径;
- 新增用户时自动继承角色权限,减少配置成本;
开发过程中我也更加深刻地理解了 Spring Security 的运作机制,尤其是:
- FilterChain 是怎样一步一步处理请求;
- 认证流程中的责任链是如何串联起来的;
- 方法级别的权限控制是怎么落地的;
经验分享:给后端同学的一些建议
如果你也在准备搭建一个类似的认证系统,我可以给你几点建议:
✅ 把 Spring Security 当作一个可插拔的模块来看待
它不是“黑盒”,也不是“万能钥匙”。你可以按需替换每一个组件。比如:
- 替换掉默认的
PasswordEncoder使用你自己的算法; - 替换掉登录失败处理器自己写逻辑;
- 修改默认的 Filter 顺序,加入你的 Token 校验逻辑;
这正是 Spring Security 的强大之处。
✅ 认真处理好权限模型的设计
很多系统之所以后面改不动,都是因为一开始权限模型没设计好。建议从三个维度考虑:
- 用户维度:是否启用、是否锁定、登录历史;
- 角色维度:角色之间是否有继承关系;
- 权限维度:权限划分是否合理,是否存在冗余。
✅ 提前规划好日志与审计功能
不要等到出了问题才想着补审计日志。Spring Security 提供了很好的监听机制,你可以轻松实现:
- 用户登录/退出事件监听;
- 权限变更追踪;
- 接口访问频率统计;
- 敏感接口访问记录等。
✅ 关注性能与并发问题
特别是在高并发场景下,频繁查询数据库可能会成为瓶颈。你可以:
- 缓存权限信息(如 Redis);
- 使用异步的方式记录审计日志;
- 分布式锁控制登录频率限制;
- 使用本地缓存避免重复查询。
结语:安全从来不是可选项
写到这里,我想起一句话:“系统安全就像穿衣服,平时可能觉得麻烦,关键时刻却至关重要。”
在那个项目之后,我越来越意识到:安全从来不是锦上添花的功能,而是一个产品最基础的生命线。
Spring Security 不完美,但它给了我们构建坚实安全体系的良好起点。只要你肯花时间去理解它的设计理念和实现机制,就一定能打造出既安全又高效的服务。
希望这篇文章能帮助你少踩点坑,更快地上手 Spring Security,建立起属于你自己的安全防线。如果有什么疑问,欢迎留言交流。技术路上一起成长 🙌
(全文约 3272 字)

评论 0