Spring Boot 3 的前后端分离架构中使用 Spring Security 实现登录鉴权,通常采用 基于 Token(如 JWT) 的无状态认证方式。以下是完整的配置思路和关键步骤:
✅ 一、添加依赖(Maven)
xml
<!-- Spring Security -->
<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.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
✅ 二、自定义 UserDetailsService
用于从数据库加载用户信息:
java
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword()) // 必须是 BCrypt 加密后的
.roles("USER") // 或根据数据库角色动态设置
.build();
}
}
✅ 三、JWT 工具类(示例)
java
@Component
public class JwtUtil {
private String secret = "yourSecretKeyAtLeast32BytesLong"; // 生产环境应配置在 application.yml
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;
}
}
}
✅ 四、自定义登录接口(绕过 Security 拦截)
java
@RestController
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials");
}
UserDetails userDetails = userDetailsService.loadUserByUsername(loginRequest.getUsername());
String token = jwtUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
}
✅ 五、配置 SecurityFilterChain(核心!)
java
@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
.csrf(csrf -> csrf.disable()) // 前后端分离通常禁用 CSRF
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 无状态
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login").permitAll() // 登录接口放行
.requestMatchers("/public/**").permitAll() // 其他公开接口
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}
}
✅ 六、实现 JWT 认证过滤器
java
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final 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) {
// token 无效
}
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.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);
}
}
chain.doFilter(request, response);
}
}
✅ 七、前端如何配合?
-
登录:POST
/login,携带{ username, password } -
成功后返回
{ token: "xxx" } -
后续请求在 Header 中带上:
Authorization: Bearer <token>
✅ 八、其他建议
- 密码必须使用
BCryptPasswordEncoder加密存储。 - JWT Secret 应配置在
application.yml并避免硬编码。 - 可考虑刷新 Token 机制(Refresh Token)。
- 异常处理:可自定义
AccessDeniedHandler和AuthenticationEntryPoint返回 JSON 而非重定向。
📌 总结
Spring Boot 3 + Security + 前后端分离的关键点:
| 步骤 | 说明 |
|---|---|
| 禁用 CSRF | .csrf().disable() |
| 无状态会话 | SessionCreationPolicy.STATELESS |
| 自定义登录接口 | 放行 /login,手动调用 AuthenticationManager |
| JWT 过滤器 | 在 UsernamePasswordAuthenticationFilter 前校验 Token |
| 前端传 Token | Authorization: Bearer xxx |