Spring Security + OAuth2 + JWT:三剑客合璧,打造“无懈可击”的微服务安全防线 🛡️⚔️
各位Java安全“守护神”,是不是被这三个名词搞得头大:Spring Security负责认证授权、OAuth2定义协议标准、JWT是令牌格式... 它们仨到底怎么联手工作?今天咱们就来个大揭秘,看看这“安全三剑客”如何为你的微服务筑起铜墙铁壁!🔐
第一章:先理清关系——谁是谁,干什么的?🎭
1. Spring Security - 保安队长 🥇
- 职责:管“谁能进”(认证)和“能去哪”(授权)
- 特点:功能强大但配置复杂,像瑞士军刀
2. OAuth2 - 安全协议 📜
- 职责:定义“令牌怎么发、怎么用”的标准流程
- 四种模式:授权码、密码、客户端、简化(最常见的是密码模式和授权码模式)
3. JWT - 令牌格式 🎫
- 职责:令牌长什么样、包含什么信息
- 结构:
Header.Payload.Signature三段式,可自包含用户信息
一句话比喻:
- Spring Security是保安系统
- OAuth2是访客登记流程规定
- JWT是通行证样式
第二章:JWT的“解剖课”——这令牌里都有啥?🔬
一个JWT令牌长这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. # Header(头部)
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ. # Payload(载荷)
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c # Signature(签名)
解码后看看:
// 头部(Header):说明类型和算法
{
"alg": "HS256", // 签名算法:HMAC SHA256
"typ": "JWT" // 类型:JWT
}
// 载荷(Payload):实际数据
{
"sub": "1234567890", // 主题(用户ID)
"name": "John Doe", // 自定义声明
"iat": 1516239022, // 签发时间
"exp": 1516242622, // 过期时间
"roles": ["USER", "ADMIN"] // 用户角色
}
// 签名(Signature):防篡改验证
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret密钥
)
JWT的优缺点:
优点:✅
1. 自包含:不用查数据库就知道用户信息
2. 无状态:服务端不用存session
3. 跨语言:JSON格式,通用
4. 可验证:签名防篡改
缺点:❌
1. 无法作废:发出去就收不回,除非等过期
2. 载荷公开:Base64解码就能看(但不加密)
3. 令牌较长:比普通随机字符串长
第三章:5分钟快速集成——让项目“安全上岗” ⏱️
第1步:加依赖(pom.xml)
<!-- Spring Security核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- OAuth2资源服务器(新版推荐) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<!-- JWT处理库 -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.37.3</version>
</dependency>
第2步:核心配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 1. 密码编码器(必须!不然会报错)
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // 推荐BCrypt
}
// 2. 配置Spring Security
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
// 禁用CSRF(API服务通常不需要)
.csrf(csrf -> csrf.disable())
// 配置URL权限
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll() // 认证接口公开
.requestMatchers("/api/public/**").permitAll() // 公开接口
.requestMatchers("/api/admin/**").hasRole("ADMIN") // 需要ADMIN角色
.anyRequest().authenticated() // 其他都需要认证
)
// 配置OAuth2资源服务器(JWT验证)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
)
// 使用基于token,不需要session
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.build();
}
// 3. 自定义JWT转换器(从JWT提取权限)
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
// 设置JWT中权限声明的前缀(默认是SCOPE_)
converter.setAuthorityPrefix("");
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(converter);
return jwtConverter;
}
}
第3步:JWT工具类
@Component
public class JwtTokenUtil {
// 密钥(生产环境要从配置中心读取!)
private final String secret = "mySuperSecretKeyThatIsAtLeast256BitsLongForHS256Algorithm";
// 有效期(毫秒)
private final long expiration = 86400000; // 24小时
// 1. 生成token
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("username", userDetails.getUsername());
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// 2. 从token中获取用户名
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 3. 验证token是否有效
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 4. 检查token是否过期
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}
第四章:登录接口实现——发“通行证” 🎫
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserDetailsService userDetailsService;
// 登录接口
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
try {
// 1. 认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
// 2. 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 3. 生成JWT
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
// 4. 返回token
return ResponseEntity.ok(new JwtResponse(
token,
"Bearer",
jwtUtil.getExpirationFromToken(token),
userDetails.getUsername(),
userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList())
));
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new ErrorResponse("用户名或密码错误"));
}
}
// 注册接口
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
// 检查用户是否存在
if (userRepository.existsByUsername(request.getUsername())) {
return ResponseEntity.badRequest()
.body(new ErrorResponse("用户名已存在"));
}
// 创建用户
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setRoles(Collections.singletonList("ROLE_USER")); // 默认角色
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("注册成功"));
}
}
第五章:资源服务器配置——验“通行证” 🔍
@Configuration
public class ResourceServerConfig {
// JWT解码器(验证签名)
@Bean
public JwtDecoder jwtDecoder() {
// 使用相同的密钥验证
SecretKeySpec secretKey = new SecretKeySpec(
"mySuperSecretKeyThatIsAtLeast256BitsLongForHS256Algorithm".getBytes(),
"HmacSHA256"
);
return NimbusJwtDecoder.withSecretKey(secretKey).build();
}
}
// 受保护的资源接口
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/profile")
public ResponseEntity<?> getProfile() {
// 从SecurityContext获取当前用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
// 获取用户权限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return ResponseEntity.ok(Map.of(
"username", username,
"authorities", authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()),
"message", "这是你的个人信息"
));
}
// 需要ADMIN角色的接口
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/admin-only")
public ResponseEntity<?> adminOnly() {
return ResponseEntity.ok("只有管理员能看到这个信息");
}
}
第六章:OAuth2密码模式完整流程 🔄
1. 客户端(前端/APP)发送用户名密码到 /api/auth/login
2. 服务端验证用户名密码
3. 验证通过,生成JWT返回
4. 客户端收到JWT,存起来(localStorage/cookie)
5. 后续请求在Header带上:Authorization: Bearer <jwt-token>
6. 服务端验证JWT签名和有效期
7. 验证通过,从JWT中提取用户信息,设置到SecurityContext
8. 执行业务逻辑,返回结果
前端调用示例:
// 1. 登录获取token
const login = async () => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'user', password: 'pass' })
});
const { token } = await response.json();
localStorage.setItem('token', token);
};
// 2. 携带token调用受保护接口
const getProfile = async () => {
const token = localStorage.getItem('token');
const response = await fetch('/api/user/profile', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.json();
};
第七章:高级玩法——刷新令牌 ♻️
JWT过期怎么办?总不能让用户重新登录吧?用刷新令牌!
@Component
public class JwtTokenUtil {
// ... 原有代码 ...
// 生成刷新token(有效期更长)
public String generateRefreshToken(UserDetails userDetails) {
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 7)) // 7天
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
// 刷新token接口
public Map<String, String> refreshToken(String refreshToken) {
try {
// 验证refresh token
Claims claims = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(refreshToken)
.getBody();
String username = claims.getSubject();
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
// 生成新的access token
String newAccessToken = generateToken(userDetails);
String newRefreshToken = generateRefreshToken(userDetails);
return Map.of(
"access_token", newAccessToken,
"refresh_token", newRefreshToken,
"token_type", "Bearer"
);
} catch (Exception e) {
throw new RuntimeException("刷新令牌失败", e);
}
}
}
第八章:安全最佳实践 🏆
-
密钥管理:
# 不要硬编码!用配置中心或环境变量 jwt: secret: ${JWT_SECRET:defaultSecretKeyShouldBeLongAndRandom} expiration: ${JWT_EXPIRATION:86400000} -
HTTPS必须的:JWT在HTTP下是裸奔的
-
合理设置有效期:
- Access Token: 15分钟~2小时
- Refresh Token: 7天~30天
-
敏感操作二次验证:改密码、支付等需要额外验证
-
令牌黑名单:虽然JWT无法作废,但可以建黑名单处理退出登录
-
防CSRF:如果前端是浏览器,还是要防CSRF
第九章:Spring Security 6.x新写法 📝
Spring Security 6.x推荐Lambda风格配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(Customizer.withDefaults())
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
)
.build();
}
}
总结:安全三剑客,各司其职 🤺
- Spring Security:管认证授权流程
- OAuth2:定义令牌标准协议
- JWT:令牌的具体实现格式
最佳拍档:
- 前端传用户名密码 → Spring Security认证
- 认证通过 → 按OAuth2标准生成JWT令牌
- 前端带JWT访问接口 → Spring Security验证JWT
- 验证通过 → 从JWT提取用户信息,执行业务
记住:没有绝对的安全,只有相对的安全。安全是一个持续的过程,不是一劳永逸的功能。根据业务需求选择合适的安全级别,定期更新依赖,做好监控和日志。
现在,你的微服务有了“三剑客”护体,可以安心上线了!如果遇到安全问题,记得回来复习这篇“安全秘籍”哦! 😉