认证
Laravel 用 Breeze/Jetstream 或
Auth::attempt()+ Session/JWT;Spring Boot 的标配是 Spring Security。这篇用「从零写一个 JWT 登录」走通认证主流程,帮 PHP 开发者理解 Spring Security 的核心思想:SecurityFilterChain 过滤器链 + AuthenticationManager。
先理解 Spring Security 的「过滤链」思想
Laravel 的认证是「中间件 + Auth 门面」;Spring Security 是一整条过滤器链,每个请求依次穿过:
请求 → UsernamePasswordAuthenticationFilter(找登录凭证)
→ 校验 → SecurityContext(存当前用户)
→ 过滤器链放行 → Controller核心概念对照:
| Laravel | Spring Security |
|---|---|
Auth::attempt() | AuthenticationManager.authenticate(...) |
auth()->user() | SecurityContextHolder.getContext().getAuthentication() |
auth()->check() | SecurityContextHolder.getContext().getAuthentication() != null |
中间件 auth | 过滤器链的鉴权逻辑 |
Auth::logout() | 清理 SecurityContext |
完整实现 JWT 登录
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
<scope>runtime</scope>
</dependency>1. 密码加密器
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}2. 登录接口(手动认证)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authManager;
private final JwtService jwtService;
private final UserRepository userRepository;
private final PasswordEncoder encoder;
public AuthController(AuthenticationManager authManager, JwtService jwtService,
UserRepository userRepository, PasswordEncoder encoder) {
this.authManager = authManager;
this.jwtService = jwtService;
this.userRepository = userRepository;
this.encoder = encoder;
}
@PostMapping("/login")
public LoginResponse login(@RequestBody LoginRequest req) {
// 1. 交给 Spring Security 认证
Authentication auth = authManager.authenticate(
new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword()));
// 2. 认证通过,签发 JWT
String token = jwtService.generate(auth.getName());
return new LoginResponse(token);
}
@PostMapping("/register")
public User register(@RequestBody RegisterRequest req) {
if (userRepository.findByUsername(req.getUsername()).isPresent()) {
throw BusinessException.badRequest("用户名已存在");
}
User user = new User();
user.setUsername(req.getUsername());
user.setPassword(encoder.encode(req.getPassword())); // 存哈希,绝不存明文
return userRepository.save(user);
}
}TIP
authManager.authenticate(...) ≈ Auth::attempt(['username' => ..., 'password' => ...])。区别:Laravel 的 attempt 会自己查用户、比对哈希;Spring 把「查用户」和「比密码」交给两个可替换组件(UserDetailsService + PasswordEncoder),更模块化。
3. UserDetailsService:查用户(≈ Laravel 的 Auth::user() 数据来源)
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}对照
UserDetailsService.loadUserByUsername ≈ Laravel 认证时框架调 retrieveByCredentials 的地方。Spring Security 规定:你要告诉它「怎么按用户名找到用户」,它就负责密码比对和会话建立。
4. JwtService:签发与校验
@Service
public class JwtService {
private final SecretKey key = Keys.hmacShaKeyFor(
"super-secret-key-at-least-32-characters-long!".getBytes(StandardCharsets.UTF_8));
public String generate(String username) {
return Jwts.builder()
.subject(username)
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 3600_000)) // 1 小时
.signWith(key)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser().verifyWith(key).build()
.parseSignedClaims(token).getPayload().getSubject();
}
public boolean isValid(String token, UserDetails user) {
return extractUsername(token).equals(user.getUsername());
}
}TIP
JWT 是无状态的:服务端不存 Session,Token 自己携带用户信息。对应 Laravel 的 Tymon/JWT 或 Sanctum 的 token 模式。前后端分离 API 首选。
5. JWT 认证过滤器:每个请求自动带上用户
@Component
public class JwtAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthFilter(JwtService jwtService, UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
String username = jwtService.extractUsername(token);
UserDetails user = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth); // 放进安全上下文
} catch (Exception e) {
// token 无效,不设置认证
}
}
chain.doFilter(request, response);
}
}6. SecurityFilterChain 配置(核心)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http,
JwtAuthFilter jwtAuthFilter) throws Exception {
http
.csrf(csrf -> csrf.disable()) // API 无状态,关掉 CSRF
.sessionManagement(sm -> sm.sessionCreationPolicy(
SessionCreationPolicy.STATELESS)) // 无 Session,全走 JWT
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 登录注册放行
.anyRequest().authenticated()) // 其余全部要登录
.addFilterBefore(jwtAuthFilter,
UsernamePasswordAuthenticationFilter.class); // JWT 过滤器插在认证之前
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
throws Exception {
return config.getAuthenticationManager();
}
}这段配置就是 Spring Security 的「主心骨」,解释每行:
| 代码 | 类比 Laravel |
|---|---|
.csrf(csrf -> csrf.disable()) | API 场景关 CSRF(Session 场景要开) |
STATELESS | 不用 Session 存登录态 |
.requestMatchers("/api/auth/**").permitAll() | ->withoutMiddleware('auth') 或放行登录路由 |
.anyRequest().authenticated() | 其余路由都要登录 |
控制器里拿当前用户
@GetMapping("/me")
public User me(Authentication authentication) {
// authentication.getName() = 当前登录用户名
UserDetails user = (UserDetails) authentication.getPrincipal();
return userRepository.findByUsername(user.getUsername()).orElseThrow();
}对应 Laravel 的 auth()->user()。
也可以直接从 SecurityContext 取
SecurityContextHolder.getContext().getAuthentication() 在任何地方都能拿到当前用户(包括 Service 层),类似 Laravel 的 Auth::user() 全局访问。
完整流程串联
POST /api/auth/login
→ UsernamePasswordAuthenticationFilter(被我们关闭)
→ AuthController.login()
→ authManager.authenticate() → UserDetailsService 查用户 + PasswordEncoder 比对
→ 通过 → 签发 JWT 返回前端
GET /api/posts(带 Authorization: Bearer xxx)
→ JwtAuthFilter 解析 token → 设置 SecurityContext
→ 过滤器链 → authorizeHttpRequests 校验已认证 → Controller对照速查
| 需求 | Laravel | Spring Security |
|---|---|---|
| 注册 | User::create + Hash::make | encoder.encode(...) 存库 |
| 登录 | Auth::attempt() | authManager.authenticate(...) |
| 当前用户 | auth()->user() | Authentication 参数 / SecurityContext |
| 是否登录 | auth()->check() | authentication != null && authenticated |
| 登出 | Auth::logout() | 清理 SecurityContext + 前端丢 token |
| 路由保护 | ->middleware('auth') | .anyRequest().authenticated() |
更细的授权控制(角色、权限注解)见 授权,Spring Security 更完整的过滤器链机制见 Spring Security 深入。