用Spring Security快速搭建安全认证系统 —— 一位全栈工程师的实战分享
引子:为什么选了Spring Security?

还记得去年我在公司接了一个新项目,任务是从零开始搭建一套面向企业的内部管理系统。用户群体是公司员工,所以权限控制和安全性成了重中之重。一开始我们考虑过Shiro、或者自研权限框架,但权衡之后,最终决定使用 Spring Security 来构建整套认证授权机制。
说来惭愧,在此之前我对 Spring Security 的认识仅限于“听说过”,真正用起来才体会到它功能的强大与灵活。这篇文章就以我实际工作中的经验为主线,分享一下我们是怎么在两周内用 Spring Security 快速搭建起一个基础认证系统,并逐步演进到支持 RBAC 模型的过程。
背景描述:从简单到复杂的安全需求演进

刚开始系统的需求很明确:
- 用户登录后能访问自己的资源;
- 不同角色(管理员、普通用户等)看到的内容不同;
- 系统需要记录用户的操作日志;
- 后续可能会接入OAuth2或第三方认证;
看起来这些要求并不难,但如果要兼顾可扩展性和维护性,就需要提前做好设计准备。
我们在技术选型上选择了 Spring Boot + MyBatis Plus + MySQL 的组合。认证和权限部分则直接上了 Spring Security。虽然一开始有些学习成本,但越往后越觉得这个选择非常明智。
遇到的问题:入门门槛高、配置繁琐

刚开始集成 Spring Security 的时候,遇到几个典型问题:
1. 默认拦截太“霸道”了怎么办?
默认情况下,Spring Security 会对所有请求都进行认证检查。但我们有些静态资源(比如 /css, /js)其实是想放行的。如果不处理好,浏览器会不停地弹出登录框,用户体验极差。
解决办法其实也不复杂,在 SecurityConfig 配置类中加入下面这段代码即可:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
}
这样就可以让 Spring Security 不再对这些路径做任何处理。
2. 登录页被重定向搞懵了
我们自己写了个登录页面,但一访问就被跳转到默认的 /login 页面去了。这是因为在默认配置下,未认证访问受保护资源时会被自动引导到内置登录页。
为了解决这个问题,我们需要覆盖默认行为:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login") // 指定自己的登录页
.defaultSuccessUrl("/dashboard")
.failureUrl("/login?error")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login?logout")
.permitAll();
}
这样一来,用户访问 /login 就会进入我们自定义的登录页面,而不是 Spring Security 自带的那个。
3. 用户数据存哪?数据库怎么设计?
一开始为了图快,我们用了内存中的用户配置:
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin").password("{noop}123456").roles("ADMIN");
}
不过很快我们就发现,这种方式根本不适合生产环境。于是转向了基于数据库的用户存储方案。
我们的数据库结构如下:
CREATE TABLE `users` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL UNIQUE,
`password` VARCHAR(100) NOT NULL,
`enabled` BOOLEAN DEFAULT TRUE
);
CREATE TABLE `authorities` (
`username` VARCHAR(50) NOT NULL,
`authority` VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users(username)
);
然后通过实现 UserDetailsService 接口,从数据库加载用户信息:
@Service
public class CustomUserDetailsService 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("用户不存在");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user)
);
}
private Collection<? extends GrantedAuthority> getAuthorities(User user) {
List<GrantedAuthority> authorities = new ArrayList<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
}
这样整个用户系统的生命周期就能很好地管理起来了。
实战演练:一步步搭建认证系统

第一步:初始化Spring Boot项目
使用 start.spring.io 创建 Spring Boot 工程,引入以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
第二步:实现核心接口
创建 User 和 Role 实体类,并完成 UserService 和 Mapper 层的开发。
关键步骤是实现前面提到的 UserDetailsService 接口,并将其注册成 Bean。
第三步:配置SecurityConfig
这是一个典型的配置类,实现了基本的认证流程、权限控制和异常处理:
@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()
.headers().frameOptions().disable()
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.successHandler((request, response, authentication) -> {
// 自定义成功处理器,记录日志或其他操作
response.sendRedirect("/dashboard");
})
.failureHandler((request, response, exception) -> {
response.sendRedirect("/login?error");
})
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.addLogoutHandler((request, response, authentication) -> {
// 可以记录登出日志
})
.logoutSuccessHandler((request, response, authentication) -> {
response.sendRedirect("/login?logout");
});
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/css/**", "/js/**", "/images/**");
}
}
第四步:添加操作日志记录
我们利用 Spring AOP,在用户登录/登出前后做一些审计记录:
@Aspect
@Component
public class AuthLoggerAspect {
private static final Logger logger = LoggerFactory.getLogger(AuthLoggerAspect.class);
@AfterReturning("execution(* org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter.attemptAuthentication(..))")
public void logSuccessfulLogin(JoinPoint joinPoint) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info("用户 {} 成功登录", auth.getName());
}
@After("execution(* org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler.logout(..))")
public void logUserLogout(JoinPoint joinPoint) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()) {
logger.info("用户 {} 成功登出", auth.getName());
}
}
}
这段代码虽然简单,但在日常运维中非常实用,尤其在排查问题时能省去很多沟通成本。
踩坑经验总结:那些年我们一起掉过的坑
坑1:密码加密方式不统一导致始终登录失败
最初我们没有统一密码加密方式,有的地方用的是 {noop},有的地方用了 BCryptPasswordEncoder。结果就是——明明用户名和密码是对的,却死活登录不进去。
后来我们彻底清除了旧密码,确保所有用户密码都走同一个加密策略,问题才得以解决。
坑2:跨域问题引发的403异常
我们系统前端部署在另一个端口上,当发起登录请求时,总是返回 403 Forbidden。查了很久才发现是 CSRF 保护机制作祟。
虽然我们最后选择关闭 CSRF(因为使用 JWT 方案后不再需要),但如果是传统 Session 模式,一定要合理配置 CORS 策略和 CSRF Token 校验。
坑3:Session超时处理不合理影响体验
系统上线初期,经常有用户反馈“点击按钮没反应”。最后发现问题在于 Session 超时后没有及时提醒用户重新登录,而是静默地中断了请求。
解决方案是在 Spring Security 中添加一个自定义的 ExpiredSessionStrategy,并在 Session 失效时返回 JSON 响应或重定向到登录页。
效果和收益:安全、可控、易维护
最终我们用不到两周时间,就完成了一个完整的基础认证系统。其带来的好处包括:
- 所有用户必须登录才能访问业务模块;
- 角色控制清晰,权限配置灵活;
- 登录日志、登出记录完整,便于审计;
- 后期方便拓展多因素认证、OAuth2、SSO等功能;
- 架构可插拔,未来升级 JWT 或其他方案也较为平滑。
这套机制上线后运行稳定,几乎没有出现因权限引起的线上问题,极大地提升了整体系统的可用性和安全性。
经验建议:给正在起步的朋友几点忠告
如果你也在尝试用 Spring Security 搭建认证系统,这里有几个小建议供你参考:
- 别怕学得慢:Spring Security 功能强大,但也确实有点陡峭。建议从最简单的
inMemoryUserDetailsManager开始实践,先理解认证流程。 - 重视数据库设计:权限模型不要急于求成,RBAC 是个不错的起点。
- 关注安全性细节:比如密码存储要用加密算法,避免明文;注意 Session 超时机制等。
- 善用日志和工具链:比如 Spring Boot Actuator 提供的
/actuator/security端点能帮你实时观察安全状态。 - 提前预留扩展能力:像后面要加JWT、微信登录、钉钉扫码登录等,最好在架构设计之初就预留足够的灵活性。
写在最后
作为一名全栈开发者,我觉得 Spring Security 是后端安全方面绕不开的一道坎。刚开始可能觉得复杂,但一旦掌握,它的抽象设计、可插拔架构和社区生态会让你欲罢不能。
回头看看这短短两周的搭建过程,不只是完成了工作任务,更是一次对权限体系、安全机制的深入理解和认知。希望这篇文章能帮你在 Spring Security 的路上少踩几个坑,早点跑起来!
如有疑问,欢迎留言交流~ 🚀

评论 0