Spring Security上手指南:别再裸奔你的API了!

队列在排队
2025-12-26 18:37
阅读 607

上周五晚上十点半,实验室的灯还亮着。我一边盯着屏幕上疯狂报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了!
    }
}

这段代码干了三件事:

  1. /public/** 路径免认证(比如健康检查、验证码)
  2. 其他所有请求必须登录
  3. 使用表单登录页(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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝