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

端口被占用
2025-12-17 03:51
阅读 3401

上周五晚上十点半,我还在公司改一个“临时加的”登录页。产品经理说:“就加个用户名密码登录,很简单吧?” 我差点一口老血喷在键盘上——这需求简单?那得看用什么框架了。

作为一个在外包公司混了四年的“码农民工”,我已经见过太多次那种“明天上线、今晚给方案”的骚操作。这次项目是个内部管理系统,后端是 Java + Spring Boot,前端 Vue。客户死活不让用 OAuth2,非得自己搞一套账号密码认证。行吧,反正也不是第一次从零搭权限体系了。

为什么又选了 Spring Security?

其实我内心是抗拒的。毕竟我最近沉迷 Rust,连写点小工具都开始用 axum + jsonwebtoken 自己拼 auth middleware 了。但现实很骨感:客户要求 Java 技术栈,团队里没人会 Go,运维也只认 Tomcat。

说到 Go,我其实偷偷试过用 Gin 写了个 demo,配合 gorm + JWT,代码清爽到飞起。但一想到要给整个团队培训、文档重写、CI/CD 流程改造……算了,还是别给自己找麻烦。毕竟咱是外包,不是创业公司,稳定交付比技术洁癖重要多了。

至于 JavaScript?前端用 JS 没问题,但后端用 Node.js 做企业级权限控制?我上次见这么干的项目,三个月后就因为 token 刷新逻辑崩了被甲方骂到重构。所以,还是老老实实用 Spring Security 吧——虽然配置像迷宫,但胜在成熟、文档多、出了问题 Stack Overflow 上八成有人踩过。

吐槽一句:Spring Security 的学习曲线,简直像爬珠穆朗玛峰——你刚以为登顶了,发现前面还有个北坳。

快速搭个能跑的认证系统

废话不多说,直接上干货。目标很明确:用户输入用户名密码 → 后端校验 → 返回 JWT → 后续请求带 token 访问受保护接口。

1. 依赖安排

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>

别忘了加数据库(比如 MySQL)和 MyBatis-Plus,用户信息总得存吧。

2. 用户实体设计

public class User {
    private Long id;
    private String username;      // 唯一
    private String password;      // BCrypt加密
    private String role;          // ROLE_ADMIN / ROLE_USER
}

重点:密码一定要用 BCryptPasswordEncoder 加密!别学某些同事直接存明文,去年双11期间有个项目就这么被审计揪出来,差点背锅。

3. 核心配置:SecurityConfig

这才是重头戏。我踩过最大的坑就是忽略 http.cors().and().csrf().disable(),结果前端 Axios 死活跨域失败。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

    @Bean
    public AuthenticationManager authenticationManager(
            AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors().and()
            .csrf().disable()  // 前后端分离,关掉CSRF
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/auth/login").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public JwtAuthenticationFilter jwtFilter() {
        return new JwtAuthenticationFilter();
    }
}

4. 登录接口 & JWT 生成

@RestController
@RequestMapping("/auth")
public class AuthController {

    @Autowired
    private AuthenticationManager authManager;

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody LoginRequest req) {
        try {
            Authentication auth = authManager.authenticate(
                new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword())
            );
            // 生成JWT
            String token = JwtUtil.generateToken(auth.getName());
            return ResponseEntity.ok(new JwtResponse(token));
        } catch (BadCredentialsException e) {
            return ResponseEntity.status(401).body("用户名或密码错误");
        }
    }
}

这里的关键是 AuthenticationManager 会自动调用我们实现的 UserDetailsService 去查库。记得重写 loadUserByUsername 方法!

5. JWT 拦截器

自定义一个 OncePerRequestFilter,从 Header 里拿 Authorization: Bearer xxx,解析后塞进 SecurityContext:

public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
        String token = extractToken(req);
        if (token != null && JwtUtil.validateToken(token)) {
            String username = JwtUtil.getUsernameFromToken(token);
            UserDetails user = userDetailsService.loadUserByUsername(username);
            UsernamePasswordAuthenticationToken auth = 
                new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res);
    }
}

性能与架构考虑

虽然这是个“快速搭建”的方案,但在生产环境还是得注意几点:

  1. JWT 不要存敏感信息:payload 是 base64 可解的!
  2. 设置合理过期时间:我们一般设 2 小时,配合前端做静默刷新。
  3. 黑名单机制:登出时把 token 加入 Redis 黑名单(虽然 JWT 本意是无状态,但业务需要就得妥协)。
  4. 密码爆破防护:加个 Redis 计数器,5 次失败锁 10 分钟——这招是从我刷 LeetCode “LRU Cache” 题目得到的灵感,算法不只是面试用啊兄弟们!

说到算法,其实权限系统底层就是图论:用户-角色-权限,典型的多对多关系。如果你要搞 RBAC + 数据权限,那复杂度直线上升。不过外包项目一般到 ROLE 级别就停了,再往下?产品经理自己都搞不清需求。

对比一下其他语言方案

为了满足文章关键词要求,顺便列个表对比下不同技术栈搞认证的体验:

技术栈 开发速度 学习成本 生产稳定性 适合场景
Spring Security 中 高 ⭐⭐⭐⭐⭐ 企业级 Java 项目
Go + Gin 快 低 ⭐⭐⭐⭐ 微服务、高并发 API
Node.js + Passport 快 中 ⭐⭐ 小型项目、快速原型
Rust + Axum 慢 极高 ⭐⭐⭐ 性能敏感、安全关键系统

我个人觉得:如果你团队 Java 技术栈成熟,Spring Security 虽然啰嗦但最省心。Go 更优雅,但国内企业 Java 生态根深蒂固,想推新技术?除非你是架构师,否则别碰。

最后一点感悟

写这篇文章的时候,我其实正在刷 LeetCode 准备跳槽。每天下班回家两小时,Rust 和算法轮着来。但白天上班,还是得面对这些“老旧但稳定”的 Java 项目。外包四年,我学会了:技术理想主义要藏好,交付才是王道。

Spring Security 虽然配置反人类,但它扛得住甲方爸爸的审计,经得起运维大叔的拷打。有时候,不是技术不够新,而是业务场景不允许你秀肌肉。

对了,上周那个登录页,周一顺利上线了。测试妹子说“这次没出现无限重定向”,我回她:“那是因为我把 successHandler 和 failureHandler 都删了,直接返回 JSON。” —— 看,有时候解决问题的方法,就是别按官方文档来 😏

共勉。

评论 0

最热最新
暂无评论
端口被占用Lv.1
0
影响力
0
文章
0
粉丝