Spring Security 初探:从 Android 开发者视角搭建认证系统
上个月,我还在用 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,我们自己管 tokenaddFilterBefore:插入自定义的 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。两者逻辑一致:
- 登录接口
/api/auth/login返回{ token: "xxx" } - 后续所有请求在 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"。可以通过配置去掉前缀,但我觉得保留更清晰。
生产环境踩坑实录
CORS 问题:本地开发时前端跑在
localhost:3000,后端在8080,浏览器报跨域。解决方法是在 SecurityConfig 里加:http.cors().and().csrf().disable();并单独配 CorsConfigurationSource。
Swagger 被拦:开发时想用 Swagger 测试接口,结果所有接口都要登录。临时解决方案:
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**").permitAll()日志太吵: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