> 技术文档 > 微服务19-Java服务安全:使用OAuth2与JWT构建身份认证与授权体系

微服务19-Java服务安全:使用OAuth2与JWT构建身份认证与授权体系


Java服务安全:使用OAuth2与JWT构建身份认证与授权体系

在分布式系统和微服务架构中,保障服务安全至关重要。用户身份验证、资源访问控制、防止未授权访问等需求,都需要一套完善的安全机制来实现。OAuth2与JWT(JSON Web Token)是当前最流行的解决方案组合,被广泛应用于Java服务的安全保护。

本文将详细介绍如何在Java服务中集成OAuth2和JWT,构建可靠的身份认证与授权系统。
在这里插入图片描述

一、核心概念解析

1.1 身份认证与授权基础

  • 身份认证(Authentication):验证用户身份的过程(例如用户名密码登录)
  • 授权(Authorization):确定已认证用户能访问哪些资源的过程
  • 令牌(Token):用户身份的凭证,用于避免频繁验证用户名密码
  • OAuth2:一种授权框架,定义了获取访问令牌的标准流程
  • JWT:一种紧凑的、URL安全的令牌格式,用于在各方之间传递声明信息

1.2 OAuth2核心角色

  • 资源所有者:通常是用户,拥有被访问的资源
  • 客户端:请求访问资源的应用程序
  • 授权服务器:验证用户身份并颁发令牌的服务
  • 资源服务器:存储受保护资源的服务,负责验证令牌有效性

1.3 OAuth2授权流程

OAuth2定义了多种授权流程,适用于不同场景:

  • 授权码流程:最安全,适用于有后端的应用
  • 简化流程:适用于纯前端应用
  • 密码流程:适用于高度信任的应用
  • 客户端凭证流程:适用于服务间通信

二、JWT令牌详解

JWT是一种轻量级的令牌格式,它将用户声明信息编码到令牌中,使得资源服务器可以独立验证令牌,无需与授权服务器通信。

2.1 JWT结构

JWT由三部分组成,用点(.)分隔:

  1. 头部(Header):指定令牌类型和签名算法
{ \"alg\": \"HS256\", \"typ\": \"JWT\"}
  1. 载荷(Payload):包含声明信息
{ \"iss\": \"https://auth.example.com\", \"exp\": 1689872345, \"sub\": \"1234567890\", \"name\": \"John Doe\", \"roles\": [\"USER\", \"ADMIN\"]}
  1. 签名(Signature):确保令牌未被篡改
HMACSHA256( base64UrlEncode(header) + \".\" + base64UrlEncode(payload), secret)

2.2 JWT的优势与局限

优势

  • 自包含:包含所有必要信息,减少服务间通信
  • 无状态:资源服务器可独立验证,适合分布式系统
  • 紧凑:体积小,适合在URL、Header中传递
  • 跨语言:基于JSON,支持多种编程语言

局限

  • 无法直接撤销:一旦签发,在过期前始终有效
  • 载荷可解码:不应存储敏感信息
  • 签名验证有性能成本

三、Spring Security集成实现

Spring Security提供了完整的OAuth2和JWT支持,下面我们将实现一个完整的安全体系。

3.1 搭建授权服务器

3.1.1 添加依赖
<dependencies>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-authorization-server</artifactId> <version>1.1.2</version> </dependency>  <dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>9.31</version> </dependency>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>
3.1.2 配置授权服务器
import com.nimbusds.jose.jwk.JWKSet;import com.nimbusds.jose.jwk.RSAKey;import com.nimbusds.jose.jwk.source.ImmutableJWKSet;import com.nimbusds.jose.jwk.source.JWKSource;import com.nimbusds.jose.proc.SecurityContext;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.core.annotation.Order;import org.springframework.security.config.Customizer;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.core.userdetails.User;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.oauth2.core.AuthorizationGrantType;import org.springframework.security.oauth2.core.ClientAuthenticationMethod;import org.springframework.security.oauth2.core.oidc.OidcScopes;import org.springframework.security.oauth2.jwt.JwtDecoder;import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;import org.springframework.security.provisioning.InMemoryUserDetailsManager;import org.springframework.security.web.SecurityFilterChain;import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;import java.security.KeyPair;import java.security.KeyPairGenerator;import java.security.interfaces.RSAPrivateKey;import java.security.interfaces.RSAPublicKey;import java.util.UUID;@Configuration@EnableWebSecuritypublic class AuthorizationServerConfig { // 配置授权服务器安全过滤器链 @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // 启用OpenID Connect 1.0 http // 认证端点需要认证时重定向到登录页面 .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint(\"/login\")) ) // 接受access token用于用户信息端点 .oauth2ResourceServer((resourceServer) -> resourceServer .jwt(Customizer.withDefaults())); return http.build(); } // 配置默认安全过滤器链(用于表单登录) @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) // 表单登录用于认证用户 .formLogin(Customizer.withDefaults()); return http.build(); } // 配置用户信息服务 @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.builder() .username(\"user\") .password(passwordEncoder().encode(\"password\")) .roles(\"USER\") .build(); UserDetails adminUser = User.builder() .username(\"admin\") .password(passwordEncoder().encode(\"admin\")) .roles(\"ADMIN\", \"USER\") .build(); return new InMemoryUserDetailsManager(userDetails, adminUser); } // 密码编码器 @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // 配置客户端信息 @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(\"order-client\") .clientSecret(passwordEncoder().encode(\"client-secret\")) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri(\"http://localhost:8081/login/oauth2/code/order-client\") .postLogoutRedirectUri(\"http://localhost:8081/\") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope(\"order.read\") .scope(\"order.write\") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(client); } // 生成RSA密钥对 @Bean public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } // 生成RSA密钥对 private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(\"RSA\"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } // JWT解码器 @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } // 授权服务器设置 @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); }}
3.1.3 配置应用属性
server: port: 9000 # 授权服务器端口spring: application: name: auth-server

3.2 搭建资源服务器

资源服务器是提供受保护资源的服务,需要验证请求中的JWT令牌。

3.2.1 添加依赖
<dependencies>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency>  <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency></dependencies>
3.2.2 配置资源服务器
import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;import org.springframework.security.web.SecurityFilterChain;@Configuration@EnableWebSecurity@EnableMethodSecurity // 启用方法级别的安全注解public class ResourceServerConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers(\"/public/**\").permitAll() // 公开接口 .requestMatchers(\"/orders/**\").hasAnyRole(\"USER\", \"ADMIN\") // 需要USER或ADMIN角色 .requestMatchers(\"/admin/**\").hasRole(\"ADMIN\") // 需要ADMIN角色 .anyRequest().authenticated() // 其他请求需要认证 ) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwt -> jwt  .jwtAuthenticationConverter(jwtAuthenticationConverter()) // 配置JWT转换器 ) ); return http.build(); } /** * 配置JWT转换器,将JWT中的声明转换为Spring Security权限 */ @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); // 设置权限前缀(默认是SCOPE_) grantedAuthoritiesConverter.setAuthorityPrefix(\"ROLE_\"); // 设置包含权限的声明名称(默认是scope或scp) grantedAuthoritiesConverter.setAuthoritiesClaimName(\"roles\"); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; }}
3.2.3 配置应用属性
server: port: 8080 # 资源服务器端口spring: application: name: order-service# JWT配置spring.security.oauth2.resourceserver.jwt: issuer-uri: http://localhost:9000 # 授权服务器地址 jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/oauth2/jwks # JWK集合地址

3.3 创建受保护的资源接口

import org.springframework.security.access.prepost.PreAuthorize;import org.springframework.security.core.Authentication;import org.springframework.web.bind.annotation.*;import java.util.ArrayList;import java.util.List;@RestControllerpublic class OrderController { // 公开接口,无需认证 @GetMapping(\"/public/health\") public String healthCheck() { return \"Order service is running\"; } // 需要USER或ADMIN角色 @GetMapping(\"/orders\") public List<Order> getOrders(Authentication authentication) { // 获取当前登录用户信息 String username = authentication.getName(); System.out.println(\"User \" + username + \" is accessing orders\"); // 模拟返回订单列表 List<Order> orders = new ArrayList<>(); orders.add(new Order(1L, \"Order 1\", \"PAID\")); orders.add(new Order(2L, \"Order 2\", \"PENDING\")); return orders; } // 需要ADMIN角色 @DeleteMapping(\"/orders/{id}\") @PreAuthorize(\"hasRole(\'ADMIN\')\") // 方法级别的权限控制 public String deleteOrder(@PathVariable Long id) { // 模拟删除订单 return \"Order \" + id + \" deleted successfully\"; } // 需要特定权限 @PostMapping(\"/orders\") @PreAuthorize(\"hasAuthority(\'SCOPE_order.write\')\") public Order createOrder(@RequestBody Order order) { // 模拟创建订单 order.setId(3L); return order; } // 管理员接口,需要ADMIN角色 @GetMapping(\"/admin/statistics\") public String getStatistics() { return \"Order statistics: 100 orders today\"; } // 内部类:订单模型 public static class Order { private Long id; private String name; private String status; public Order() {} public Order(Long id, String name, String status) { this.id = id; this.name = name; this.status = status; } // Getters and setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } }}

3.4 构建客户端应用

客户端应用需要获取令牌并使用令牌访问资源服务器。

import org.springframework.security.core.annotation.AuthenticationPrincipal;import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;import org.springframework.security.oauth2.client.annotation.RegisteredOAuth2AuthorizedClient;import org.springframework.security.oauth2.core.user.OAuth2User;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.reactive.function.client.WebClient;@RestControllerpublic class ClientController { private final WebClient webClient; // 注入WebClient,用于访问资源服务器 public ClientController(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder.baseUrl(\"http://localhost:8080\").build(); } // 客户端首页,显示用户信息 @GetMapping(\"/\") public String index( @RegisteredOAuth2AuthorizedClient(\"order-client\") OAuth2AuthorizedClient authorizedClient, @AuthenticationPrincipal OAuth2User oauth2User) { return \"Username: \" + oauth2User.getName() + \"
AccessToken: \"
+ authorizedClient.getAccessToken().getTokenValue(); } // 访问资源服务器的订单接口 @GetMapping(\"/orders\") public String getOrders( @RegisteredOAuth2AuthorizedClient(\"order-client\") OAuth2AuthorizedClient authorizedClient) { // 使用访问令牌调用资源服务器 return webClient.get() .uri(\"/orders\") .header(\"Authorization\", \"Bearer \" + authorizedClient.getAccessToken().getTokenValue()) .retrieve() .bodyToMono(String.class) .block(); }}

四、JWT令牌增强与自定义声明

在实际应用中,我们通常需要在JWT中添加自定义声明,以传递额外的用户信息或权限。

4.1 自定义JWT令牌增强器

import org.springframework.security.core.Authentication;import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;import java.util.HashMap;import java.util.Map;public class CustomJwtTokenEnhancer implements OAuth2TokenCustomizer<JwtEncodingContext> { @Override public void customize(JwtEncodingContext context) { // 对于授权码流程,添加自定义声明 if (context.getAuthorizationGrantType().getValue().equals(\"authorization_code\")) { Authentication authentication = context.getPrincipal(); // 添加用户信息 Map<String, Object> customClaims = new HashMap<>(); customClaims.put(\"department\", \"IT\"); // 部门信息 customClaims.put(\"preferences\", Map.of(\"theme\", \"dark\", \"notifications\", true)); // 用户偏好 // 如果是admin用户,添加额外声明 if (authentication.getAuthorities().stream()  .anyMatch(auth -> auth.getAuthority().equals(\"ROLE_ADMIN\"))) { customClaims.put(\"isAdmin\", true); } // 将自定义声明添加到JWT context.getClaims().claims(claims -> claims.putAll(customClaims)); } // 对于客户端凭证流程,添加客户端相关信息 if (context.getAuthorizationGrantType().getValue().equals(\"client_credentials\")) { context.getClaims().claim(\"client_type\", \"backend_service\"); } }}

4.2 在授权服务器中配置令牌增强器

@Beanpublic JwtGenerator jwtGenerator(JWKSource<SecurityContext> jwkSource) { JwtGenerator jwtGenerator = new JwtGenerator(jwkSource); jwtGenerator.setJwtCustomizer(new CustomJwtTokenEnhancer()); return jwtGenerator;}

五、令牌管理与安全最佳实践

5.1 令牌生命周期管理

合理设置令牌有效期:

@Beanpublic RegisteredClientRepository registeredClientRepository() { RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString()) // ... 其他配置 .tokenSettings(TokenSettings.builder() .accessTokenTimeToLive(Duration.ofMinutes(30)) // 访问令牌有效期 .refreshTokenTimeToLive(Duration.ofDays(7)) // 刷新令牌有效期 .reuseRefreshTokens(false)// 是否允许重用刷新令牌 .build()) .build(); return new InMemoryRegisteredClientRepository(client);}

5.2 令牌撤销与黑名单

JWT本身无法被撤销,因此需要实现令牌黑名单机制:

import org.springframework.data.redis.core.RedisTemplate;import org.springframework.security.oauth2.jwt.Jwt;import org.springframework.stereotype.Component;import java.time.Duration;import java.time.Instant;@Componentpublic class JwtTokenBlacklist { private final RedisTemplate<String, String> redisTemplate; private static final String BLACKLIST_PREFIX = \"jwt:blacklist:\"; public JwtTokenBlacklist(RedisTemplate<String, String> redisTemplate) { this.redisTemplate = redisTemplate; } /** * 将令牌加入黑名单 */ public void blacklistToken(Jwt jwt) { String tokenId = jwt.getId(); Instant expiration = jwt.getExpiresAt(); if (expiration != null) { // 计算令牌剩余有效期 Duration ttl = Duration.between(Instant.now(), expiration); // 将令牌ID存入Redis,并设置与令牌相同的过期时间 redisTemplate.opsForValue().set( BLACKLIST_PREFIX + tokenId,  \"revoked\",  ttl ); } } /** * 检查令牌是否在黑名单中 */ public boolean isTokenBlacklisted(Jwt jwt) { String tokenId = jwt.getId(); return redisTemplate.hasKey(BLACKLIST_PREFIX + tokenId); }}

5.3 安全最佳实践

  1. 使用HTTPS:所有OAuth2交互和JWT传输必须使用HTTPS
  2. 最小权限原则:只授予必要的权限范围(scope)
  3. 定期轮换密钥:定期更换用于签名JWT的密钥
  4. 避免敏感信息:不要在JWT中存储密码等敏感信息
  5. 短有效期:访问令牌应设置较短的有效期
  6. 安全存储令牌:客户端应安全存储令牌,避免XSS攻击
  7. 实现令牌撤销:提供用户登出和令牌撤销功能
  8. 使用非对称加密:生产环境应使用RSA等非对称加密算法签名JWT

六、服务间认证与授权

在微服务架构中,服务之间的通信也需要认证和授权,通常使用客户端凭证流程。

import org.springframework.http.HttpHeaders;import org.springframework.security.oauth2.client.OAuth2AuthorizedClient;import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;import org.springframework.security.oauth2.client.registration.ClientRegistration;import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;import org.springframework.stereotype.Component;import org.springframework.web.reactive.function.client.WebClient;@Componentpublic class PaymentServiceClient { private final WebClient webClient; private final OAuth2AuthorizedClientManager authorizedClientManager; private final String clientRegistrationId = \"order-service-client\"; public PaymentServiceClient( WebClient.Builder webClientBuilder, ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService clientService) { // 配置OAuth2授权客户端管理器 this.authorizedClientManager = OAuth2AuthorizedClientManager.create( clientRegistrationRepository, clientService);  // 配置WebClient this.webClient = webClientBuilder .baseUrl(\"http://localhost:8080\") .build(); } // 调用订单服务的内部接口 public String getOrderDetails(Long orderId) { // 获取客户端凭证令牌 ClientRegistration clientRegistration = authorizedClientManager .getClientRegistrationRepository() .findByRegistrationId(clientRegistrationId);  OAuth2AuthorizedClient authorizedClient = authorizedClientManager .authorize(OAuth2AuthorizeRequest.withClientRegistrationId(clientRegistrationId) .principal(clientRegistration.getClientId()) .build()); // 使用令牌调用服务 return webClient.get() .uri(\"/internal/orders/\" + orderId) .header(HttpHeaders.AUTHORIZATION,  \"Bearer \" + authorizedClient.getAccessToken().getTokenValue()) .retrieve() .bodyToMono(String.class) .block(); }}

七、总结

通过OAuth2和JWT,我们可以为Java服务构建一个灵活而强大的安全体系:

  • OAuth2提供了标准化的授权流程,支持多种场景下的令牌获取
  • JWT作为一种自包含的令牌格式,非常适合分布式系统
  • Spring Security提供了完整的OAuth2和JWT支持,简化了安全配置
  • 合理的令牌生命周期管理和安全实践是保障系统安全的关键

在实际应用中,还需要根据业务需求不断调整和增强安全策略,以应对不断变化的安全威胁,保护用户数据和服务安全。