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

一键启动人生
2025-12-16 03:49
阅读 660

大家好,我是小张,在老家县城远程办公的“小镇做题家”。白天在一家中小厂当后端开发,晚上刷 LeetCode 准备跳槽简历,周末偶尔研究点底层原理——比如 JVM 的 GC 算法、Netty 的 Reactor 模型,或者 Redis 的持久化机制。最近被领导安排了一个“紧急又不急”的任务:给公司内部管理系统加上一套靠谱的身份认证和权限控制。说白了,就是用 Spring Security 搞个安全认证系统。

坦白讲,之前我对 Spring Security 的理解还停留在“加个 @EnableWebSecurity 就完事了”的水平。但这次不行,因为产品经理上周五下午 5:47 在群里甩过来一句话:“老张,咱们的管理后台现在谁都能进,太危险了,得加登录和角色权限。”然后第二天就去三亚团建了。我看着钉钉消息,心里一万个草泥马奔腾而过——这不就是典型的“需求提得快,人跑得更快”吗?

更离谱的是,测试同学昨天还报了个线上 bug:某个接口没做鉴权,实习生误删了生产数据(还好有 binlog 回滚)。运维大哥一边骂一边帮我们恢复数据,临走前丢下一句:“你们再不做权限控制,我就把你们服务 IP 加到黑名单。”行吧,看来这事儿真的不能再拖了。

于是,我咬咬牙,关掉 B 站上的《Go 语言并发编程实战》(是的,我也在偷偷学 Go,毕竟现在简历上不写点 Go 好像都不好意思投大厂),打开 VSCode,插件栏里 Spring Boot Extension Pack、Lombok、GitLens 全都亮着,深吸一口气,开始啃 Spring Security 的官方文档。


为什么不用自己手写 Token 鉴权?

其实一开始我想偷懒——直接搞个拦截器,校验 JWT Token,查数据库看用户有没有权限,完事。这种“土法炼钢”我在上家公司就干过,代码简单,逻辑清晰,调试也方便。但这次项目要上线到客户私有化部署环境,安全审计要求很高,自己造轮子风险太大。万一哪天被人绕过 Token 直接调接口,锅还是我的。

而且,Spring Security 不是花架子。它背后有一套完整的过滤器链(Filter Chain) 架构,从 UsernamePasswordAuthenticationFilterExceptionTranslationFilter,每个环节都经过工业级验证。更重要的是,它和 Spring 生态无缝集成——比如方法级权限 @PreAuthorize("hasRole('ADMIN')"),简直不要太香。

所以,别再想着“我手写一个更轻量”了。安全这事,交给专业框架,省心又省命。


快速搭建:三步走战略

第一步:引入依赖,配置基础结构

新建一个 Spring Boot 项目(我用的 3.2.0),pom.xml 加上:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>pring-boot-starter-web</artifactId>
</dependency>

注意:Spring Boot 3.x 默认用的是 Jakarta EE 9+,包名从 javax.* 变成了 jakarta.*,如果你是从旧项目升级,可能会遇到兼容问题。我上周就踩了这个坑,启动直接报 ClassNotFoundException,差点以为是我电脑中病毒了。

然后,创建一个最简单的 SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .permitAll()
            )
            .logout(logout -> logout
                .permitAll()
            );
        return http.build();
    }
}

这时候启动项目,访问任意接口(比如 /api/user/list),会被自动重定向到 /login 页面——Spring Security 默认会生成一个丑到爆的登录页。别慌,这是正常现象,说明安全框架生效了。

开发心得:很多人第一次看到这个默认登录页都会懵,以为配置错了。其实不是,它只是 Spring Security 的“兜底行为”。你可以通过 .formLogin().loginPage("/your-login") 自定义,也可以直接禁用表单登录,改用 JSON 认证(后面会讲)。


第二步:自定义用户认证逻辑

默认情况下,Spring Security 会用内存中的用户(用户名 user,密码随机生成在日志里)。这显然不能用于生产。

我们需要实现 UserDetailsService 接口,从数据库加载用户:

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found");
        }
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword()) // 注意:这里应该是 BCrypt 加密后的
            .roles(user.getRoles().toArray(new String[0]))
            .build();
    }
}

同时,记得配置密码编码器:

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

重点来了:千万别存明文密码!我见过太多小公司为了“调试方便”直接存明文,结果某天数据库泄露,全员账号被盗。BCrypt 是 Spring Security 官方推荐的,加盐、慢哈希、防彩虹表攻击,一行代码搞定安全底线。


第三步:改成前后端分离的 JSON 登录

我们公司的前端是 Vue + Axios,根本不需要 Spring Security 的表单跳转。所以得改成 JSON 认证。

思路是:前端 POST /auth/login,携带 {username, password},后端验证成功后返回 JWT Token。

为此,我们要自定义一个 AuthenticationFilter,并替换掉默认的 UsernamePasswordAuthenticationFilter

先写个登录控制器:

@RestController
public class AuthController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @PostMapping("/auth/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest request) {
        try {
            Authentication auth = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                    request.getUsername(), 
                    request.getPassword()
                )
            );
            // 生成 JWT Token(这里简化,实际用 JJWT 库)
            String token = JwtUtil.generateToken(auth.getName());
            return ResponseEntity.ok(Map.of("token", token));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body("Invalid credentials");
        }
    }
}

然后在 SecurityConfig 中关闭表单登录,放行 /auth/login

http
    .csrf(csrf -> csrf.disable()) // 前后端分离通常关 CSRF
    .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
    .authorizeHttpRequests(auth -> auth
        .requestMatchers("/auth/login").permitAll()
        .anyRequest().authenticated()
    )
    .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

最后,写一个 JwtFilter 从 Header 中解析 Token 并设置 SecurityContext

public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
        String token = extractToken(request);
        if (token != null && JwtUtil.validateToken(token)) {
            String username = JwtUtil.getUsernameFromToken(token);
            UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken auth = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }
}

这样,整个认证流程就跑通了:前端拿 Token,每次请求带 Authorization: Bearer <token>,后端验证后自动填充用户上下文。


权限控制:从 URL 到方法级

光有登录还不够,还得控制谁能看到什么。

Spring Security 支持两种粒度:

  1. URL 级别:在 SecurityConfig 里配

    .requestMatchers("/admin/**").hasRole("ADMIN")
    .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN")
    
  2. 方法级别:用注解

    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(Long id) { ... }
    

后者需要开启全局方法安全:

@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {}

生产建议:URL 级别适合粗粒度控制(比如整个模块),方法级别适合细粒度(比如“只有本人能修改自己的资料”)。两者结合使用最稳。

另外,权限字段设计也得讲究。我们数据库里用户表有个 roles 字段,存的是 ["ROLE_ADMIN", "ROLE_USER"] 这样的 JSON 数组。Spring Security 会自动去掉前缀 ROLE_ 做匹配,所以 hasRole('ADMIN') 实际匹配的是 ROLE_ADMIN

血泪教训:千万别把权限字符串硬编码在代码里!我们之前有个同事写了 if (role.equals("管理员")),结果测试环境角色叫“超级管理员”,直接权限失效。后来统一改成枚举 + 数据库配置,才避免这种低级错误。


性能与运维考量

虽然 Spring Security 功能强大,但也有性能陷阱。

  • 每次请求都查数据库?
    不会!UserDetails 默认会被缓存到 SecurityContext 中,一次认证,全程有效(只要你不手动清理)。但如果用了 JWT 且是无状态服务,那每次都要解析 Token 并查用户信息。这时候可以考虑:

    • Token 里直接嵌入角色信息(减少 DB 查询)
    • 用 Redis 缓存用户权限(TTL 和 Token 过期时间一致)
  • 日志监控怎么做?
    我们在 AuthenticationSuccessHandlerAuthenticationFailureHandler 里加了日志埋点,记录登录成功/失败的 IP、时间、用户名。配合 ELK,能快速发现暴力破解行为。

  • 如何应对 Token 泄露?
    虽然 JWT 无法主动失效,但我们加了“黑名单”机制:用户登出时,把 Token 存入 Redis,设置 TTL 为剩余有效期。每次请求先查黑名单,存在则拒绝。


跳槽视角:为什么简历要写 Spring Security?

说实话,很多初级开发者觉得“会用 Spring Boot 就行了,安全有中间件挡着”。但面试官恰恰喜欢问 Security 的底层原理——比如:

  • 过滤器链的执行顺序是怎样的?
  • AuthenticationManagerProviderManager 的关系?
  • 如何自定义 AccessDecisionManager 实现动态权限?

我在刷面经时发现,懂 Security 的人,往往对 Spring AOP、代理模式、责任链模式也理解更深。这正是大厂看重的“技术纵深”。

而且,现在微服务架构下,OAuth2、SSO、RBAC 都是标配。哪怕你主语言是 Go(没错,我又在学 Go 了),理解这些概念也能让你在跨语言协作时不掉队。


写在最后

折腾了三天,终于把这套安全系统上线了。测试同学跑完回归测试,给了我一个大拇指;运维大哥默默把我从“高危开发者名单”里划掉了;产品经理在群里发了个红包,虽然只有 8 块钱……

但最开心的是,我再也不用担心实习生误删数据了

回头看看,Spring Security 其实没那么可怕。只要你愿意沉下心读文档、看源码(强烈建议 debug 跟一下 FilterChainProxy 的调用栈),就能把它变成你的护城河,而不是拦路虎。

对了,如果你也在县城远程办公,边工作边刷题,准备跳槽——欢迎留言交流。也许下次我们就能在杭州或深圳的某家大厂工位上,一起吐槽新公司的产品经理呢。

共勉。


附:常用配置对比表

配置项 表单登录(传统) JSON + JWT(前后端分离)
Session 有状态(Cookie) 无状态(Header Token)
CSRF 防护 默认开启 通常关闭
登录入口 /login 页面 /auth/login API
权限传递 Session 存储 Token 携带
适合场景 后台管理系统 移动 App / SPA 前端

关键依赖版本参考

Spring Boot: 3.2.0
Spring Security: 6.1.5
JJWT: 0.11.5

记住:安全不是功能,而是基础设施。别等事故发生了才想起它。

评论 0

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