Spring Security上手指南:别再裸奔你的API了!
上周五晚上十点半,实验室的灯还亮着。我一边盯着屏幕上疯狂报403错误的日志,一边在心里默默问候产品经理全家——“你早说要加登录认证啊!明天就要演示了才提?”
但骂归骂,活还得干。好在Spring Security这玩意儿我去年双11搞电商中台的时候啃过一轮,这次搭个基础认证系统,两小时搞定,临走前还顺手写了份README甩给组里本科生:“照着配,别动不动就喊我。”
作为211软工研二老油条,在实验室混了快两年,从写Python脚本处理数据,到如今主力搞云原生和K8s部署Spring Boot微服务,我深知一件事:没安全的后端,就像没锁的共享单车——谁都能骑,迟早被偷。
今天这篇技术分享,就带你用Spring Security快速搭一套基础但能打的安全认证系统。不整那些花里胡哨的OAuth2、JWT进阶玩法(那玩意儿下次讲),先解决“别让外人随便调我接口”这个基本需求。顺便也回答下最近面试被问烂的一个题:“Spring Security的核心原理是什么?”
为什么不能裸奔?
我们组有个本科生,上周交了个demo,直接把/api/deleteAllUsers暴露在外网,连Basic Auth都没加。测试同学一试,好家伙,数据库清空了。导师当场血压拉满:“你们是想上生产事故通报吗?”
其实很多同学(包括我刚入门时)总觉得“安全是运维的事”,或者“项目小不用搞那么复杂”。但现实很骨感:只要你的API能被公网访问,就一定会被扫描、爆破、注入。哪怕只是个内部管理后台,也得有最基础的身份校验。
Spring Security就是Spring生态里专门干这事的“门卫”。它能帮你自动拦截请求、验证用户、授权权限,而且和Spring Boot集成起来贼顺滑——毕竟亲儿子嘛。
五分钟,从零到有
假设你现在有个Spring Boot项目(我用的是3.x版本),想给所有接口加上用户名密码登录。怎么做?
第一步:加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
就这一行!不用额外装Redis、不用建用户表(先别急,后面会升级),Spring Security会自动生成一个随机密码(启动日志里能看到),用户名默认是user。
但这种“玩具级”配置显然不能上线。我们得自己控制用户体系。
第二步:写个简单的UserDetailsService
@Service
public class MyUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 实际项目这里查数据库 or 缓存
if ("admin".equals(username)) {
return User.builder()
.username("admin")
.password("{noop}123456") // {noop}表示不加密,生产环境必须用BCrypt!
.roles("ADMIN")
.build();
}
throw new UsernameNotFoundError("用户不存在");
}
}
注意那个{noop}!这是Spring Security 5+的新要求:密码必须带加密标识。如果你写123456,它会报错说找不到匹配的PasswordEncoder。虽然本地开发可以用{noop}绕过,但上线前务必换成BCrypt。
🐍 插一句:我们组之前有个Python后端转Java的兄弟,死活搞不懂为啥密码要加前缀,折腾半天才发现是文档没看全。所以啊,别迷信“经验复用”,每个生态都有自己的坑。
第三步:配置Security规则
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll() // 公开接口
.anyRequest().authenticated() // 其他都要登录
)
.formLogin(form -> form // 启用表单登录
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 别再用NoOp了!
}
}
这段代码干了三件事:
/public/**路径免认证(比如健康检查、验证码)- 其他所有请求必须登录
- 使用表单登录页(Spring Security会自动生成一个简易登录页,当然你也可以自定义)
启动项目,访问任意接口,自动跳转到/login,输入admin / 123456,搞定!
那些年踩过的坑
坑1:CSRF保护把你拦在家门口
Spring Security默认开启CSRF防护。如果你的前端是Vue/React这类SPA,用Axios发POST请求,可能会遇到403 Forbidden,日志显示Invalid CSRF token。
解决方案很简单:如果是纯API服务(无浏览器session),可以关掉CSRF。
http.csrf(csrf -> csrf.disable()); // 仅限无状态API!
但注意:千万别在有表单提交的传统Web应用里关CSRF,否则你的转账按钮可能被人伪造点击。
坑2:密码加密方式不对,用户登不上去
前面提到{noop}的问题。更常见的场景是:你在注册接口里用BCryptPasswordEncoder.encode(password)存了密文,但在UserDetails里又忘了指定PasswordEncoder,结果认证时拿明文去比对密文,自然失败。
正确做法:全局只用一个PasswordEncoder Bean,Spring Security会自动注入使用。
坑3:路径匹配顺序搞反了
Security的匹配是从上到下的!如果你这样写:
.anyRequest().authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN")
那/admin也会被第一行拦住,根本走不到角色校验。正确顺序应该是从具体到通用:
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
和Python比?各有各的痛
我们实验室一半人用Python(FastAPI/Django),一半用Java。经常有人问:“Spring Security是不是比Django Auth复杂多了?”
其实不是复杂,是粒度更细。Django Auth开箱即用,用户模型、登录页、权限都给你包好了;Spring Security则像乐高,给你一堆积木,你自己拼。
好处是灵活——你想集成LDAP、CAS、甚至自研SSO,都能插进去。坏处是新手容易懵。
但话说回来,复杂度是业务逼出来的。如果你只是做个内部工具,Django确实更快;但要是做金融级系统,Spring Security的细粒度控制和审计能力就值回票价。
面试题挑战:说说Spring Security的过滤器链
最近面试被问到:“Spring Security是怎么工作的?”
别背八股文!结合实战说:
“它本质是一条Filter Chain。当请求进来,会依次经过UsernamePasswordAuthenticationFilter、ExceptionTranslationFilter、FilterSecurityInterceptor等。比如用户提交登录表单,UsernamePasswordAuthenticationFilter捕获请求,调用AuthenticationManager做认证,成功后把Authentication对象塞进SecurityContext。后续FilterSecurityInterceptor会检查当前用户是否有权限访问目标资源。”
如果能再画个简图(虽然文章不能放图,但面试可以手绘),基本稳了。
生产环境注意事项
在实验室跑通只是开始,真上K8s集群还得考虑这些:
| 事项 | 开发环境 | 生产环境 |
|---|---|---|
| 密码存储 | {noop}明文 |
BCrypt + 盐值 |
| Session管理 | 内存Session | Redis集中存储 |
| 登录页 | 自带简易页 | 前端定制 + HTTPS |
| 日志审计 | System.out | ELK收集登录失败记录 |
| 并发控制 | 默认不限 | 配置maximumSessions |
我们组上个月就把Session从内存迁到Redis,避免K8s Pod重启导致用户掉线。配置也很简单:
spring:
session:
store-type: redis
再加个@EnableRedisHttpSession注解就行。云原生时代,无状态+外部存储才是王道。
最后叨叨两句
写这篇文章时,我又想起去年双11凌晨三点,因为漏配了antMatchers导致支付回调被拦,差点背锅。从那以后,我对安全配置再也不敢马虎。
Spring Security学起来有点陡峭,但一旦掌握,你会觉得它像瑞士军刀——小而精准,关键时刻能救命。
对了,组里新来的实习生问我:“学这个对找工作有用吗?”
我反手甩给他一道面试题:“如何防止Spring Boot应用被暴力破解登录接口?”
他愣住。
我说:“加失败次数限制、IP封禁、验证码……这些Spring Security都能做,就看你挖多深。”
技术这东西,用得浅是工具,钻得深是护城河。
好了,代码已上传到实验室GitLab(内网地址就不贴了),有问题欢迎来卷。
哦对,别再裸奔你的API了,真的会被黑的——我导师上周刚给我们看了某高校教务系统的漏洞报告,惨不忍睹 😅

评论 0