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

热情狗
2025-12-18 18:57
阅读 1741

昨晚十一点半,窗外的上海下着小雨,我还在公司附近的出租屋里敲代码。咖啡已经凉了第三杯,但效率奇高——也不知道是不是因为明天就是项目交付 deadline,还是单纯地“深夜才是程序员真正的黄金时间”。作为一名 211 软件工程研二的学生,我在实验室接了个横向项目,说是“科研”,其实干的活跟一线后端开发差不多:写接口、调性能、怼测试、陪产品经理画饼……

最近被安排搞一个内部管理后台的权限控制模块,领导轻飘飘一句:“用 Spring Security 吧,成熟稳定。”
我表面点头称是,心里却在翻白眼:Spring Security 这玩意儿配置复杂得堪比考研政治,文档又臭又长,连官方 demo 都像是从 2008 年穿越过来的。更离谱的是,团队里居然没人真正用过它做过生产级项目——大家都是“听说过,没用过,但感觉很牛”。

于是,上周五晚上,我一边啃着泡面,一边硬着头皮开干。目标很明确:三天内,搭出一个能跑、能登录、能鉴权的基础认证系统。不求花里胡哨,但求稳如老狗。


为啥非得是 Spring Security?

其实一开始我想上 Shiro,轻量、简单、中文文档友好。但组长一句话把我打回现实:“Shiro 社区快凉了,新项目别碰。Security 是 Spring 官方亲儿子,集成 OAuth2、JWT、多租户都方便,以后扩展也省事。”

行吧,亲儿子就亲儿子。但问题是,Security 的“约定大于配置”风格真的劝退。你要是不按它的套路来,分分钟给你报 AccessDeniedException,还附带一行灵魂拷问:

"Authentication object cannot be null"

那一刻我真的想砸电脑。

不过吐槽归吐槽,Security 的优势确实没法忽视:

  • 深度集成 Spring Boot,自动装配省心
  • 权限模型灵活(GrantedAuthority、Role、Expression)
  • 支持方法级注解(@PreAuthorize 真香)
  • 内置 CSRF、Session 固定攻击防护等安全机制

对于我们这种既要赶工期又要保安全的小团队来说,稳定 > 创新。虽然我喜欢折腾新技术(比如前阵子偷偷在本地跑了个 Quarkus 项目),但上线的东西,还是得用“祖传配方”。


第一步:别一上来就搞 JWT,先让表单登录跑起来

很多教程一上来就教你怎么集成 JWT + Redis + OAuth2,搞得像在造火箭。但对我们这个内部系统来说,用户就几十个管理员,根本不需要无状态!直接用最传统的基于 Session 的表单登录,反而更简单、调试更直观。

于是,我新建了一个 Spring Boot 3.2 项目(对,我们已经升级到 Jakarta EE 9 了,别再用 javax.* 了兄弟们),然后只加了两个依赖:

<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>

启动!访问任意接口,果然弹出了那个经典的丑到爆的登录页。用户名是 user,密码在控制台打印了一长串 UUID。这显然不能交差。

接下来,我自定义了用户体系。

用户表设计:别偷懒,密码必须加密!

数据库里建了张 sys_user 表(MySQL 8.0):

字段 类型 说明
id BIGINT 主键
username VARCHAR(50) 唯一,用于登录
password VARCHAR(100) BCrypt 加密后存储
role VARCHAR(20) 如 ADMIN, OPERATOR
enabled TINYINT 账号是否启用

重点来了:千万别存明文密码!我见过实习生把密码明文存进去,还理直气壮说“反正内网”。结果某天运维误操作把数据库 dump 文件发到了群里……那场面,至今难忘。

在代码里,我用 BCryptPasswordEncoder 处理密码:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

注册时加密,登录时框架自动比对,完全不用手动处理。Spring Security 在这点上做得非常体贴。


自定义 UserDetailsService:让系统认识你的用户

Security 默认从内存加载用户,我们需要对接数据库。于是实现 UserDetailsService 接口:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser user = userMapper.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        // 注意:role 字段要加 ROLE_ 前缀,Security 才认
        List<GrantedAuthority> authorities = 
            AuthorityUtils.createAuthorityList("ROLE_" + user.getRole());
        
        return new org.springframework.security.core.userdetails.User(
            user.getUsername(),
            user.getPassword(),
            user.isEnabled(),
            true, true, true, // accountNonExpired, credentialsNonExpired, accountNonLocked
            authorities
        );
    }
}

这里有个坑:角色必须带 ROLE_ 前缀!否则你在 @PreAuthorize("hasRole('ADMIN')") 时会失效。我一开始没加,调试了两个小时,最后发现日志里权限列表是空的……血泪教训。


配置 SecurityFilterChain:别再用 WebSecurityConfigurerAdapter 了!

Spring Security 5.7+ 已经废弃了 WebSecurityConfigurerAdapter,改用函数式配置。新的写法更清爽:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/login", "/css/**", "/js/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login") // 自定义登录页
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/login?logout")
                .permitAll()
            )
            .csrf(csrf -> csrf.disable()); // 内部系统,暂时关掉 CSRF(生产慎用!)

        return http.build();
    }
}

注意几个细节:

  • hasRole("ADMIN") 会自动匹配 ROLE_ADMIN
  • 我关掉了 CSRF,仅限内部管理系统!对外系统千万别这么干
  • 登录成功后跳转到 /dashboard,失败则留在登录页

前端用 Thymeleaf 写了个极简登录页,表单字段名必须是 usernamepassword,否则 Security 不认。这也是“约定”的一部分,改字段名就得重写 UsernamePasswordAuthenticationFilter,没必要。


方法级权限:@PreAuthorize 真香警告

表单登录跑通后,产品经理突然提了个需求:“普通操作员只能看自己创建的数据,管理员能看全部。”

这时候 URL 级权限就不够用了,得上 方法级鉴权

首先在主类加个注解:

@EnableMethodSecurity(prePostEnabled = true)
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

然后在 Service 方法上加注解:

@Service
public class OrderService {

    @PreAuthorize("hasRole('ADMIN') or #creatorId == authentication.principal.username")
    public List<Order> getOrdersByCreator(String creatorId) {
        // 查询逻辑
    }
}

这里的 authentication.principal.username 就是当前登录用户名,可以直接和参数比较。动态权限校验就这么简单

不过要注意:方法级权限默认只对 Spring Bean 生效,且必须通过代理调用(不能 this.method())。另外,性能开销略大,高频接口慎用。


踩过的坑 & 生产建议

1. Session 管理别忽视

我们用的是默认的内存 Session,但多实例部署时会出问题。后来加了 Spring Session + Redis:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

一行配置搞定分布式 Session,Security 自动接管。但要注意 Redis 连接池配置,别让 Session 把 Redis 打挂了。

2. 日志太吵?关掉 DEBUG

Security 默认日志级别是 INFO,但登录失败会疯狂刷日志。建议在 application.yml 里调整:

logging:
  level:
    org.springframework.security: WARN

3. 密码策略别太弱

虽然 BCrypt 很安全,但如果用户密码是 123456,照样被撞库。我们后来加了自定义 PasswordEncoder 包装层,强制密码长度 ≥8,包含大小写+数字。

4. 测试账号别硬编码

开发时经常需要不同角色账号测试。我写了个 DevUserInitializer,启动时自动创建测试用户(仅 dev profile):

@Profile("dev")
@Component
public class DevUserInitializer {
    // 创建 admin / operator 账号
}

上线时用 prod profile,自动失效,安全又方便。


效果如何?Deadline 前两小时搞定!

周一上午十点,我把 PR 提交了。测试同学跑完冒烟测试,说“登录、权限、登出都正常”。组长看了一眼代码,点点头:“结构清晰,注释到位,可以 merge。”

那一刻,泡面的余味还在嘴里,但心里踏实了。虽然只是个基础认证系统,但安全无小事。以前总觉得“反正没人黑我们”,直到听说隔壁组因为没做权限校验,导致测试数据被删光……


写在最后:代码人生,安全先行

在实验室做项目这一年,我越来越意识到:炫技不如稳字当头。Spring Security 可能不是最酷的,但它像一把老式但可靠的锁——不显眼,却能守住底线。

深夜 coding 的时候,我常想:我们写的每一行代码,最终都会变成某个系统的一部分。可能是银行交易,可能是医疗记录,也可能是我正在做的这个不起眼的后台。安全不是功能,而是责任

所以,哪怕只是个学生项目,我也坚持把密码加密、权限校验、日志脱敏这些事做到位。因为我知道,未来的某一天,这些习惯会让我成为一个值得信赖的工程师。

共勉。

P.S. 下周准备接入 OAuth2.0 对接公司统一认证中心,估计又要掉头发。如果有踩坑经验,欢迎评论区交流!
P.P.S. 别信“三天速成安全架构”,真正的安全,藏在每一个细节里。

评论 0

最热最新
暂无评论
热情狗Lv.1
0
影响力
0
文章
0
粉丝