Spring Security + OAuth2 + JWT:三剑客合璧,打造“无懈可击”的微服务安全防线

19 阅读6分钟

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);
        }
    }
}

第八章:安全最佳实践 🏆

  1. 密钥管理

    # 不要硬编码!用配置中心或环境变量
    jwt:
      secret: ${JWT_SECRET:defaultSecretKeyShouldBeLongAndRandom}
      expiration: ${JWT_EXPIRATION:86400000}
    
  2. HTTPS必须的:JWT在HTTP下是裸奔的

  3. 合理设置有效期

    • Access Token: 15分钟~2小时
    • Refresh Token: 7天~30天
  4. 敏感操作二次验证:改密码、支付等需要额外验证

  5. 令牌黑名单:虽然JWT无法作废,但可以建黑名单处理退出登录

  6. 防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:令牌的具体实现格式

最佳拍档

  1. 前端传用户名密码 → Spring Security认证
  2. 认证通过 → 按OAuth2标准生成JWT令牌
  3. 前端带JWT访问接口 → Spring Security验证JWT
  4. 验证通过 → 从JWT提取用户信息,执行业务

记住:没有绝对的安全,只有相对的安全。安全是一个持续的过程,不是一劳永逸的功能。根据业务需求选择合适的安全级别,定期更新依赖,做好监控和日志。

现在,你的微服务有了“三剑客”护体,可以安心上线了!如果遇到安全问题,记得回来复习这篇“安全秘籍”哦! 😉