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

Jenkins流水工
2025-12-18 20:31
阅读 2097

上周五晚上十一点半,我还在公司改一个认证相关的线上 Bug。测试同学在群里@我说:“登录接口又 403 了,大佬快看看是不是你改的?”——其实根本不是我改的,但谁让我是后端里唯一碰过 Security 的人呢?没办法,只能打开 IDE,一边骂骂咧咧一边 debug。

说起来,我在网易游戏已经待了三年多,做过两款上线的手游服务端,从最初只会写 CRUD 到后来被逼着搞高并发、分布式缓存、微服务治理……现在看着自己写的代码能跑在几百万玩家的手机上,多少还是有点成就感的(虽然工资没涨多少)。最近在考虑换个环境,于是开始整理一些平时用得比较多的技术栈,顺便写点博客记录下开发心得。今天这篇,就聊聊 Spring Security ——这个让人又爱又恨的安全框架。


为什么又来折腾 Security?

其实我们项目早期用的是自研的 Token 认证逻辑,简单粗暴:前端传个 token,后端查 Redis,校验通过就放行。那会儿团队只有三个人,PM 还天天催功能上线,哪有时间搞什么 OAuth2、JWT 标准流程?结果去年双11活动期间,因为没做权限粒度控制,某个运营接口被误调,差点把全服玩家的经验值清零……运维大哥当场血压飙升,拉着我们开了三天复盘会。

从那以后,领导拍板:“安全模块必须重构,标准方案,别再自己造轮子了。”于是我就被“委以重任”——毕竟组里没人愿意碰 Security,都说它配置复杂、文档晦涩、报错信息像天书。

说实话,一开始我也挺抗拒的。但跳槽面了几家公司后发现,几乎每家都问 Spring Security 的原理和扩展点。为了不被 HR 当成“只会写业务逻辑的 CRUD Boy”,我咬牙啃了两周官方文档 + StackOverflow,总算搭出了一套能跑的认证系统。


架构设计:资源、权限、角色怎么分?

在游戏行业,资源(Resource) 的概念特别重要。比如:

  • /api/player/profile:玩家个人信息(普通玩家可读)
  • /api/gm/banPlayer:封禁玩家(仅 GM 可调用)
  • /api/admin/reloadConfig:热更配置(运维+策划白名单)

这些接口背后代表的是不同的受保护资源。而 Spring Security 的核心思想就是:访问资源前,先验证身份和权限

我最初的误区是把“角色”和“权限”混在一起。比如给 GM 赋予 ROLE_GM,然后在接口上写 @PreAuthorize("hasRole('GM')")。但很快发现这不够灵活——有些 GM 只能封玩家,不能发奖励;有些策划需要临时获得某些管理权限。

所以最后我们采用了 RBAC(基于角色的访问控制)+ 权限粒度到接口级别 的混合模型:

模型层级 说明 示例
用户(User) 系统中的实体 player_123, gm_zhang
角色(Role) 一组权限的集合 ROLE_PLAYER, ROLE_GM, ROLE_ADMIN
权限(Permission) 具体的操作能力 player:read, gm:ban, admin:reload

数据库设计上,我们用了四张表:

user (id, username, password, enabled)
role (id, name)
permission (id, code, description)  -- code 如 'gm:ban'
user_role (user_id, role_id)
role_permission (role_id, permission_id)

这样,后续加新权限只需要在 permission 表里插入一条记录,再关联到对应角色,完全不用动代码。


快速搭建:别被配置劝退

很多人一看到 WebSecurityConfigurerAdapter 就头大。其实只要理解几个核心组件,搭起来并不难:

  1. AuthenticationManager:负责“你是谁”
  2. AccessDecisionManager:负责“你能干什么”
  3. UserDetailsService:从 DB/Redis 加载用户信息
  4. PasswordEncoder:密码加密(千万别用明文!)

下面是我简化后的配置类(省略了异常处理器、CORS 等细节):

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(); // 别再用 MD5 了,求你了
    }

    @Bean
    public UserDetailsService userDetailsService(UserMapper userMapper) {
        return username -> {
            User user = userMapper.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
            // 注意:这里要加载用户的权限列表
            List<GrantedAuthority> authorities = loadAuthorities(user.getId());
            return new org.springframework.security.core.userdetails.User(
                user.getUsername(),
                user.getPassword(),
                user.isEnabled(),
                true, true, true,
                authorities
            );
        };
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable() // 前后端分离项目一般关掉 CSRF
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/login").permitAll()
                .requestMatchers("/api/player/**").hasAuthority("player:read")
                .requestMatchers("/api/gm/**").hasAuthority("gm:ban")
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

关键点在于:

  • 使用 hasAuthority() 而不是 hasRole(),因为我们权限是细粒度的
  • 关闭 Session,用 JWT 做无状态认证(适合游戏后端高并发场景)
  • 自定义 JwtAuthenticationFilter 在请求头里解析 Token 并设置 Authentication

踩坑实录:那些让我想砸电脑的瞬间

1. 权限没生效?检查 GrantedAuthority 的前缀!

Spring Security 默认会给 hasRole('GM') 自动加上 ROLE_ 前缀,变成 ROLE_GM。但如果你用的是 hasAuthority('gm:ban'),那就不能加前缀!我一开始在数据库里存的是 ROLE_GM,结果权限校验永远失败,debug 了整整一个下午……

2. 密码加密方式不一致

本地开发用 BCryptPasswordEncoder,但测试环境用的旧系统是 SHA256。结果测试同学登录一直失败,还以为是我接口写错了。后来统一了加密策略,并写了数据迁移脚本才解决。

3. Filter 顺序问题

自定义的 JWT Filter 必须放在 UsernamePasswordAuthenticationFilter 之前,否则 Security 根本不会走你的逻辑。这个坑我踩了两次,每次都是“本地好好的,上测试环境就不行”。


性能与运维:上线后的心得

虽然 Security 功能强大,但在高并发场景下也要注意性能:

  • UserDetails 缓存:每次请求都查 DB 加载权限?达咩!我们在 UserDetailsService 里加了 Redis 缓存,key 是 user:{id}:authorities,TTL 30 分钟。权限变更时主动删除缓存。
  • 避免频繁权限校验:对于高频接口(如心跳包),直接放行,不做权限检查。
  • 日志埋点:记录所有 403 请求,方便事后审计。曾经靠这个日志抓到一个越权调用的外挂。

另外,千万别在线上开 DEBUG 日志!Security 的日志量巨大,有一次不小心打开了,磁盘瞬间爆满,运维直接冲进办公室吼我名字……


写在最后:技术分享的意义

回看这三年,从只会用 MyBatis 写 SQL,到现在能独立设计认证体系,其实最大的收获不是技术本身,而是系统性思维。以前觉得“能跑就行”,现在会考虑:权限模型是否可扩展?性能瓶颈在哪?出了问题怎么快速定位?

写这篇博客,一是给自己留个笔记(下次跳槽面试能吹),二是希望帮到正在被 Security 折磨的兄弟。毕竟,谁还没在深夜对着 AccessDeniedException 发过呆呢?

对了,如果你也在网易(或者别的大厂)做游戏后端,欢迎交流~说不定哪天就成了同事(或者竞争对手 😏)。


开发心得总结

  • 安全不是功能,是基础设施,越早引入越好
  • 权限模型要灵活,别被“角色”框死
  • 配置复杂?多看源码,少信博客(包括这篇,记得验证!)
  • 生产环境务必压测 + 监控,别等炸了才后悔

好了,不说了,产品又在群里喊:“这个需求很简单,明天上线!”——我先去改代码了。

评论 0

最热最新
暂无评论
Jenkins流水工Lv.1
0
影响力
0
文章
0
粉丝