Spring Security 初探:从 Android 开发者视角搭建认证系统

Jenkins流水工
2026-03-09 18:27
阅读 3276

上个月,我还在用 Flutter 搞一个跨平台 App 的交互动画,结果团队突然接到一个内部管理后台的需求。产品经理说:“这个后台要支持多角色权限控制,下周上线。”我当时正用 Vim 调着 AnimatedContainer 的 curve 参数,听到这话差点把机械键盘砸了——我可是个前端出身、半路转 Flutter 的人啊,后端安全这种东西,上次碰还是大学选修课。

但远程办公的好处就是没人看得见你崩溃的表情。深吸一口气,我打开 Cursor(没错,就是那个带 AI 补全的编辑器,虽然我是 Vim 党,但在 Java 项目里它确实香),开始研究 Spring Security。这篇不是什么高深理论,纯粹是“被逼上梁山”后的实战记录,希望能帮到和我一样从移动端/前端转过来、突然要写后端安全逻辑的兄弟。


为啥不直接抄个现成的?

一开始我想偷懒,直接 copy-paste 一个 GitHub 上的 Spring Boot + Security 模板。结果跑起来发现:登录页样式丑得像 2003 年的 IE6,而且默认用的是内存用户(InMemoryUserDetailsManager),这玩意儿连数据库都不连,上线第一天就得被运维兄弟骂死。

更惨的是,前端同事用 JavaScript 发了个 POST 到 /login,结果返回 403 —— 因为 Spring Security 默认开启了 CSRF 防护,而我们的前端是纯 API 调用,根本不需要表单提交那一套。那一刻我深刻体会到:安全框架不是开箱即用,而是开箱即坑。


从零搭建:先让登录能跑通

我决定从最基础的开始:用户认证 + JWT 令牌。为什么选 JWT?因为我们的 Flutter App 和 Web 管理后台都要用同一个后端,无状态认证更合适,省去 session 同步的麻烦。

第一步:依赖别漏了

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

注意:别忘了加 jjwt-impl 和 jjwt-jackson,否则运行时会报 NoClassDefFoundError。这坑我踩过,半夜三点对着日志抓狂。

第二步:关掉那些“贴心”的默认行为

Spring Security 默认开启一堆安全策略,对 RESTful API 来说反而是障碍。我建了一个配置类:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable() // 前端用 JS 发 AJAX,不需要 CSRF
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/api/auth/**").permitAll()
                .anyRequest().authenticated()
            )
            .addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

这里的关键是:

  • csrf().disable():否则前端用 fetch 或 axios 发请求会被拦截
  • STATELESS:告诉 Spring 别存 session,我们自己管 token
  • addFilterBefore:插入自定义的 JWT 校验过滤器

第三步:实现 JWT 工具类

这部分其实和前端加密逻辑很像。我在 Flutter 里也用过 crypto 包生成 token,Java 这边用 jjwt 库差不多:

public String generateToken(String username) {
    return Jwts.builder()
        .setSubject(username)
        .setIssuedAt(new Date())
        .setExpiration(new Date(System.currentTimeMillis() + 86400000)) // 24h
        .signWith(SignatureAlgorithm.HS512, secret)
        .compact();
}

secret 一定要从环境变量读!别硬编码在代码里,否则哪天代码泄露就全完了。我们团队用的是 .env 文件 + Docker secrets,上线前运维专门检查过。


数据库设计:别再用明文密码了!

作为曾经写过 Android SharedPreferences 存密码的“黑历史”拥有者,这次我坚决不用明文。Spring Security 提供了 PasswordEncoder 接口,直接用 BCrypt:

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

用户注册时:

String encodedPassword = passwordEncoder.encode(rawPassword);
userRepository.save(new User(username, encodedPassword));

数据库表结构很简单:

字段 类型 说明
id BIGINT 主键
username VARCHAR(50) 唯一
password VARCHAR(100) BCrypt 加密后
role VARCHAR(20) 如 "ADMIN", "USER"

注意:password 字段至少留 100 字符,BCrypt 输出长度固定为 60,但保险起见多留点。


前端对接:JavaScript 怎么传 token?

我们的 Web 管理后台用 Vue3 + Axios,Flutter App 用 http package。两者逻辑一致:

  1. 登录接口 /api/auth/login 返回 { token: "xxx" }
  2. 后续所有请求在 Header 里加 Authorization: Bearer xxx

Axios 拦截器示例:

// request interceptor
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

Flutter 里也类似:

final response = await http.get(
  Uri.parse('https://api.example.com/profile'),
  headers: {'Authorization': 'Bearer $token'},
);

这里有个坑:token 过期怎么办?我们后端统一返回 401,前端收到后跳转登录页。简单粗暴但有效。


权限控制:基于角色的访问

产品经理后来又加需求:“管理员能看到用户列表,普通用户只能看自己。” 得,上 @PreAuthorize。

先在启动类加注解:

@EnableMethodSecurity(prePostEnabled = true)
@SpringBootApplication
public class Application { ... }

然后在 Controller 方法上:

@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
    return userService.findAll();
}

@GetMapping("/profile")
@PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
public User getProfile(@AuthenticationPrincipal UserDetails userDetails) {
    return userService.findByUsername(userDetails.getUsername());
}

注意:Spring Security 默认给角色加前缀 ROLE_,所以数据库里存 "ADMIN",实际校验的是 "ROLE_ADMIN"。可以通过配置去掉前缀,但我觉得保留更清晰。


生产环境踩坑实录

  1. CORS 问题:本地开发时前端跑在 localhost:3000,后端在 8080,浏览器报跨域。解决方法是在 SecurityConfig 里加:

    http.cors().and().csrf().disable();
    

    并单独配 CorsConfigurationSource。

  2. Swagger 被拦:开发时想用 Swagger 测试接口,结果所有接口都要登录。临时解决方案:

    .requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()
    
  3. 日志太吵:Spring Security 默认打印大量 DEBUG 日志。在 application.yml 里关掉:

    logging:
      level:
        org.springframework.security: WARN
    

为什么我这个 Flutter 开发者能搞定?

说实话,Spring Security 文档又臭又长,但核心就三点:

  • 认证(Authentication):你是谁?→ 登录、JWT
  • 授权(Authorization):你能干啥?→ 角色、权限
  • 防护(Protection):防攻击 → CSRF、CORS、密码加密

搞懂这三层,剩下的就是查文档填配置。而且现在有 Cursor 这种工具,输入“Spring Security disable csrf”就能生成代码片段,效率翻倍。虽然我还是习惯用 Vim 写逻辑,但配 Java 依赖和 XML 时,AI 辅助真香。


最后一点感悟

从 Android 到 Flutter,再到被迫写后端安全,我越来越觉得:现代开发者不能只守自己的技术栈。前端要懂点网络协议,后端要理解前端交互逻辑,安全更是贯穿全链路。

上周五晚上,我终于把这套认证系统上线了。测试同学跑完用例,发来一句:“登录/权限功能 OK”。那一刻,虽然窗外下着雨,家里猫还在打翻我的咖啡杯,但心里还是有点小得意——毕竟,一个曾经只关心 setState 和 AnimationController 的人,现在也能写出安全可靠的后端逻辑了。

共勉。

评论 0

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