如何基于Spring实现自定义OAuth2授权与PRIVATE_KEY_JWT认证
核心思路
PRIVATE_KEY_JWT 认证方式中,客户端使用自身RSA私钥签名JWT作为身份凭证,授权服务器通过客户端公钥验证JWT有效性,完成客户端认证后签发AccessToken。以下是完整实现示例:
1. 生成客户端RSA密钥对
首先为客户端生成RSA密钥对,用于签名和验证JWT:
# 生成PKCS12格式的密钥库 keytool -genkeypair -alias client-key -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore client-keystore.p12 -validity 3650 # 导出客户端公钥(授权服务器需要此公钥验证JWT) keytool -exportcert -alias client-key -keystore client-keystore.p12 -rfc -file client-public-key.cert
2. 授权服务器配置
2.1 配置JWK源(用于验证客户端JWT)
授权服务器需要加载客户端公钥,提供给JWT验证器使用:
import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; 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 java.io.FileInputStream; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.RSAPublicKey; import java.util.UUID; @Configuration public class AuthorizationServerConfig { @Bean public JWKSource<SecurityContext> jwkSource() throws Exception { // 加载客户端公钥证书 CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate( new FileInputStream("client-public-key.cert") ); RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey(); // 将公钥转换为JWK格式 RSAKey rsaKey = new RSAKey.Builder(publicKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } // 其他配置... }
2.2 配置RegisteredClient(替换原client-secret方式)
更新客户端注册信息,使用PRIVATE_KEY_JWT作为认证方法,无需配置clientSecret:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; 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 java.util.UUID; @Configuration public class RegisteredClientConfig { @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("client_id123") // 指定PRIVATE_KEY_JWT认证方式 .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope("https://purl.imsglobal.org/spec/lti-ags/scope/score") // 如果客户端有公开的JWKS端点,可配置此参数替代本地加载公钥 // .clientSettings(ClientSettings.builder().jwkSetUrl("http://client:8080/.well-known/jwks.json").build()) .build(); return new InMemoryRegisteredClientRepository(registeredClient); } }
2.3 保留授权服务器安全FilterChain
原有安全FilterChain配置无需修改,默认配置已支持PRIVATE_KEY_JWT认证:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.config.Customizer; import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; @Configuration public class SecurityConfig { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.formLogin(Customizer.withDefaults()).build(); } }
3. 客户端请求AccessToken示例
客户端使用私钥签名JWT,向授权服务器请求AccessToken:
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.io.FileInputStream; import java.security.KeyStore; import java.security.PrivateKey; import java.util.Date; import java.util.Map; import java.util.UUID; public class ClientTokenRequestExample { public static void main(String[] args) throws Exception { // 从密钥库加载客户端私钥 KeyStore keyStore = KeyStore.getInstance("PKCS12"); keyStore.load(new FileInputStream("client-keystore.p12"), "your-keystore-password".toCharArray()); PrivateKey privateKey = (PrivateKey) keyStore.getKey("client-key", "your-key-password".toCharArray()); // 生成客户端认证JWT String clientJwt = Jwts.builder() .setIssuer("client_id123") // iss必须等于客户端clientId .setSubject("client_id123") // sub必须等于客户端clientId .setAudience("http://auth-server:9000/oauth2/token") // aud为授权服务器token端点URL .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1小时有效期 .setIssuedAt(new Date()) .setId(UUID.randomUUID().toString()) // jti为唯一标识 .signWith(privateKey, SignatureAlgorithm.RS256) .compact(); // 构造token请求参数 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.add("grant_type", "client_credentials"); params.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"); params.add("client_assertion", clientJwt); params.add("scope", "https://purl.imsglobal.org/spec/lti-ags/scope/score"); // 发送请求获取AccessToken RestTemplate restTemplate = new RestTemplate(); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers); ResponseEntity<Map> response = restTemplate.postForEntity( "http://auth-server:9000/oauth2/token", request, Map.class ); // 提取AccessToken String accessToken = (String) response.getBody().get("access_token"); System.out.println("获取到的AccessToken: " + accessToken); } }
关键注意事项
- 客户端JWT的
iss和sub必须严格等于注册的clientId aud字段必须是授权服务器的Token端点URL(如http://auth-server:9000/oauth2/token)- 授权服务器必须能获取到客户端公钥,可选择本地加载或通过客户端公开JWKS端点拉取
- 确保密钥库密码和密钥密码与生成时一致
内容的提问来源于stack exchange,提问作者Digao
相关产品推荐
相关产品推荐

