Spring Security 基础:快速搭建安全认证系统(一个考研失败应届生的血泪实践)
去年12月,我查完成绩那一刻就知道自己完了——398分?不,是政治39分。那一刻我坐在图书馆角落里,盯着屏幕发呆了半小时,脑子里只有一个念头:“完了,春招要开始了,我连项目都还没写完。”
但生活不会因为你情绪崩溃就暂停。为了赶上深圳春招黄金期,我咬着牙把毕业设计从“基于深度学习的图像识别”临时改成了“前后端分离的在线考试系统”——毕竟 Python 写 CV 模型跑不动,不如老老实实用 Java 搞个能跑起来的后端项目。而既然是考试系统,用户登录、权限控制这些功能自然逃不掉。于是,我和 Spring Security 的孽缘就此开始。
为什么不用 Python?因为现实很骨感
我知道很多同学(包括我自己)本科阶段主攻 Python,Django + DRF 能快速搭出一套带认证的 API。但来了深圳才发现,这边大厂清一色 Java 技术栈,尤其是腾讯系公司(别问我怎么知道的,面试面多了自然懂)。我们组的老哥直接说:“Python 在我们这主要用来写脚本和自动化测试,核心业务全是 Springboot。”
所以,哪怕我对 Java 又爱又恨(IDEA 启动慢到我想砸电脑),也得硬着头皮上。好在 Springboot 生态成熟,配合 Spring Security,其实比想象中简单——只要你别被它那堆 AuthenticationProvider、UserDetailsService、PasswordEncoder 绕晕就行。
需求场景:一个“看似简单”的登录功能
上周五晚上 9 点,产品经理突然在群里@我:“明天 demo 要给客户看,登录页加个验证码,普通用户只能看自己的试卷,管理员能管理所有用户。”
我:“???”
这不就是典型的 RBAC(Role-Based Access Control)吗?但 deadline 就在眼前,没时间造轮子,必须用现成的安全框架。
于是,我翻出了尘封已久的 Spring Security 文档,准备速通。
第一步:别慌,先加依赖
新建一个 Springboot 项目(我用的 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>spring-boot-starter-web</artifactId>
</dependency>
<!-- 如果你用 JWT,再加这个 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
启动项目,访问任意接口,你会看到一个默认的登录页——用户名 user,密码在控制台打印出来(形如 Using generated security password: abc123...)。
但这玩意儿显然不能上线,连数据库都没连,纯内存用户,属于“玩具级”认证。
第二步:自定义用户认证逻辑(重点!)
我们需要从数据库读取用户信息。假设你的用户表长这样:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 主键 |
| username | VARCHAR(50) | 唯一用户名 |
| password | VARCHAR(100) | 加密后的密码 |
| role | VARCHAR(20) | 角色:USER / ADMIN |
1. 实现 UserDetailsService
这是 Spring Security 获取用户信息的核心接口。我们写一个自己的实现类:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository; // 假设你用 JPA
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 注意:这里返回的是 org.springframework.security.core.userdetails.User
return User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 必须是 BCrypt 加密后的
.roles(user.getRole()) // 自动转为 ROLE_USER 或 ROLE_ADMIN
.build();
}
}
💡 坑点提醒:很多人在这里直接返回自己的
User实体类,结果报错ClassCastException。记住,必须包装成 Spring Security 的UserDetails!
2. 配置密码加密器
绝对不要存明文密码!Spring Security 推荐用 BCryptPasswordEncoder:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
}
注册时记得加密:
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserRegisterDTO dto) {
String encodedPassword = passwordEncoder.encode(dto.getPassword());
// 保存到数据库...
}
第三步:配置权限规则(URL 级别控制)
现在用户能登录了,但怎么控制谁能看到什么?比如 /admin/** 只有管理员能访问。
在 SecurityConfig 里重写 configure(HttpSecurity http) 方法(注意:Springboot 3.x 用的是 authorizeHttpRequests):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // 前后端分离项目通常关掉 CSRF
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态,配合 JWT
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/login", "/api/auth/register").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/exam/**").hasRole("USER")
.anyRequest().authenticated()
)
.httpBasic(); // 先用 Basic Auth 测试,后面换成 JWT
return http.build();
}
🤯 真实踩坑:一开始我没关 CSRF,前端 POST 登录一直 403。运维小哥还嘲讽我:“你是不是没加
_csrftoken?” 我:“……我们是 Vue + Axios,哪来的 form 表单?”
第四步:集成 JWT(无状态认证)
因为是前后端分离,Session 不太合适(尤其以后可能上微服务)。所以换成 JWT。
生成 Token 的逻辑
@Component
public class JwtUtil {
private String secret = "mySecretKey"; // 生产环境请用更长的随机字符串
private int jwtExpirationMs = 86400000; // 24小时
public String generateToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
自定义 Filter 拦截 Token
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (header != null && header.startsWith("Bearer ")) {
jwt = header.substring(7);
try {
username = jwtUtil.getUsernameFromToken(jwt);
} catch (Exception e) {
logger.error("JWT 解析失败", e);
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtUtil.validateToken(jwt)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
最后在 SecurityConfig 里注册这个 Filter:
@Autowired
private JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); // 插入到认证过滤器前
return http.build();
}
前端每次请求带上:
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
生产环境注意事项(来自血泪教训)
- 密钥管理:别把
secret写死在代码里!用配置中心或环境变量。 - Token 刷新:24 小时过期太短?可以加 Refresh Token 机制,但别学某大厂搞成“永久有效”(安全审计会骂死你)。
- 日志监控:记录登录失败次数,防暴力破解。我们组上周就被扫了 10 万次
/login,还好加了 IP 限流。 - 权限粒度:
hasRole("ADMIN")够用吗?复杂系统建议用@PreAuthorize("hasPermission(#examId, 'READ')")做数据级权限。
性能与架构思考
认证服务独立化:如果未来拆微服务,可以把认证单独做成
auth-service,其他服务通过内网调用验证 Token。缓存用户信息:频繁查 DB?把 UserDetails 缓存到 Redis,key 用 username。
压测结果参考(本地 MacBook Pro M1):
场景 QPS 平均延迟 无 Security 3200 8ms Basic Auth 2800 12ms JWT(无 DB 查询) 2500 15ms JWT + Redis 缓存 2300 18ms 可见 Spring Security 开销可控,别被“性能差”的谣言吓到。
写在最后:从考研失败到能跑通认证
说实话,第一次看到 AccessDeniedException 时我真的想删库跑路。但当你在 Postman 里成功拿到 Token,然后带着它访问 /api/admin/users 返回 200 的那一刻——那种成就感,比考研多考 50 分还爽。
虽然我现在还在投简历(深圳前端岗卷成麻花),但至少我的 GitHub 项目不再是 “Hello World”。Spring Security 看似复杂,其实核心就三点:
- 你是谁(UserDetails)
- 你有没有权限(GrantedAuthority)
- 你怎么证明你是你(Token / Session)
如果你也是应届生,别怕技术栈转换。Python 很香,但 Java 在企业级开发的地位短期内不会动摇。掌握 Springboot + Security,至少能让你在面试时说出:“我做过完整的权限系统,不是只会写 CRUD。”
对了,上周 demo 客户很满意,产品经理请我喝了杯瑞幸。他说:“小伙子,下次需求文档写清楚点。”
我:“……好的,下次我直接写进代码注释里。”
附:完整代码结构建议
src/main/java
├── config
│ └── SecurityConfig.java
├── security
│ ├── CustomUserDetailsService.java
│ ├── JwtUtil.java
│ └── JwtAuthenticationFilter.java
├── controller
│ └── AuthController.java
└── repository
└── UserRepository.java
希望这篇带点情绪、有点干货的文章能帮到和我一样的“落榜打工人”。技术路上,谁不是一边崩溃一边前进呢?

评论 0