You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Spring实现自定义OAuth2授权与PRIVATE_KEY_JWT认证

Spring Security OAuth2 Authorization Server 实现 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 12:15:33