我们有以下场景:
问题是资源服务器仅配置了 1 个密钥(当前) - 因此它只能接受来自 1 个身份验证服务器的令牌。
有没有什么可行的方法来支持我们的资源服务器中的多个密钥来解码来自不同身份验证服务器的 JWT?
我们基本上想做到这一点,但有多个键: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver-jwt-decoder-public-key
Spring Security 5.3 表明这可以通过“多租户”实现 https://docs.spring.io/spring-security/site/docs/current/reference/html5/#webflux-oauth2resourceserver-multitenancy
这是基本配置
@Value("${security.oauth2.resourceserver.jwt.key-value}")
RSAPublicKey key;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// using new Spring Security SpE"{{LOCATOR_BASE_URL}}"L
//https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#webflux-oauth2resourceserver-jwt-authorization
.authorizeRequests(authorizeRequests ->
authorizeRequests
.antMatchers("/shipments/**").hasAuthority("SCOPE_DOMPick")
.anyRequest().authenticated()
)
.csrf().disable()
// ****** this is the new DSL way in Spring Security 5.2 instead of Spring Security Oauth @EnableResourceServer ******
.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer
.jwt(jwt ->
jwt.decoder(jwtDecoder())
)
);
}
// static key
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(this.key).build();
是的,Spring Security 5.3 允许您使用多个 jwk-uri 密钥。请在这里阅读我的回答:
https://stackoverflow.com/a/61615389/12053054
如果您无法使用此版本的 SS,可以手动配置 spring security 以使用多个 jwk-uri 密钥。 (点击我提供的链接查看如何操作)。
Spring Security 文档的这一部分指定了如何使用 Spring Security 5.3 执行此操作: https://docs.spring.io/spring-security/site/docs/current/reference/html5/#oauth2resourceserver-multitenancy
JwtIssuerAuthenticationManagerResolver authenticationManagerResolver = new JwtIssuerAuthenticationManagerResolver
("https://idp.example.org/issuerOne", "https://idp.example.org/issuerTwo");
http
.authorizeRequests(authorize -> authorize
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(authenticationManagerResolver)
);
请注意,颁发者 url 是从传入令牌解析的(JWT oauth2 令牌始终包含颁发者 url,其中可以找到用于验证 JWT 令牌的 jwk 的 uri)。通过手动配置(我已经发布的答案),您可以添加自定义行为,例如:您可以检查标头以获取解析哪个颁发者 URL 的信息,而不是直接从 JWT 查找应该使用哪个 ulr 来验证令牌(您已在 spring 中指定了它们) app) 应与此请求一起使用来验证 JWT 令牌。
我知道有点晚了,但这正是我们公司所需要的。没有身份验证服务器的颁发者 url。
此外,也不需要身份验证服务器,因为客户端请求资源服务器上的受保护资源,只需使用私钥生成签名的 JWT 并将其作为授权承载令牌在 http 标头中发送。在资源服务器上,只有在信任库中导入了公钥(证书)的客户端才被允许访问资源。
因此,感谢@Norbert Dopjera 给出的提示,我实现了一个自定义的 AuthenticationManagerResolver,它将在 JWT 标头中查找传输存储在 truststore.jks 文件中的证书(公钥)的别名(密钥 id)的孩子(密钥 id),并将检索这个公钥并创建一个 JWTDecoder,它将检查作为来自 http 标头的授权承载的传入 JWT 是否使用相应的私钥进行签名。
这是使用 Spring Boot 2.7.1 的完整代码:
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtValidators;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationProvider;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
import org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver;
import org.springframework.util.StringUtils;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jwt.JWTParser;
public class TenantAuthenticationManagerResolver implements AuthenticationManagerResolver<HttpServletRequest> {
private static final Logger log = LoggerFactory.getLogger(TenantAuthenticationManagerResolver.class);
private final Map<String, AuthenticationManager> authenticationManagers = new ConcurrentHashMap<>();
private final BearerTokenResolver resolver = new DefaultBearerTokenResolver();
private String trustetoreFile;
private char[] storePasswd;
public TenantAuthenticationManagerResolver(String truststoreFile, char[] storePasswd) {
super();
this.trustetoreFile = truststoreFile;
this.storePasswd = storePasswd;
}
@Override
public AuthenticationManager resolve(HttpServletRequest request) {
try {
return this.authenticationManagers.computeIfAbsent(toTenant(request), this::fromTenant);
}
catch (Exception e) {
throw new InvalidBearerTokenException(e.getMessage());
}
}
private String toTenant(HttpServletRequest request) throws ParseException {
String jwt = this.resolver.resolve(request);
String keyId = ((JWSHeader) JWTParser.parse(jwt).getHeader()).getKeyID();
if (!StringUtils.hasText(keyId)) {
throw new IllegalArgumentException("KeyID missing");
}
return keyId;
}
private AuthenticationManager fromTenant(String tenant) {
return new JwtAuthenticationProvider(jwtDecoder(tenant))::authenticate;
}
private JwtDecoder jwtDecoder(String kid) {
log.info("Building JwtDecoder for {}", kid);
try {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(getPublicKeyFromTruststore(kid)).signatureAlgorithm(SignatureAlgorithm.from("RS512")).build();
OAuth2TokenValidator<Jwt> withDefault = JwtValidators.createDefault();
OAuth2TokenValidator<Jwt> withDelegating = new DelegatingOAuth2TokenValidator<>(withDefault);
jwtDecoder.setJwtValidator(withDelegating);
return jwtDecoder;
}
catch (Exception e) {
throw new IllegalStateException(e.getMessage());
}
}
private RSAPublicKey getPublicKeyFromTruststore(String certificateAlias) throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException {
try (FileInputStream myKeys = new FileInputStream(trustetoreFile)) {
log.info("Opening truststore");
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, storePasswd);
Certificate certificate = myTrustStore.getCertificate(certificateAlias);
if (certificate == null) {
throw new IllegalArgumentException("No entry found for alias " + certificateAlias);
}
return (RSAPublicKey) certificate.getPublicKey();
}
}
}
现在是安全配置:
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManagerResolver;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import your_package.TenantAuthenticationManagerResolver;
@EnableWebSecurity
public class SecurityConfig {
@Value("${jwt.keystore.location}")
private String keyStore;
@Value("${jwt.keystore.password}")
private char[] storePasswd;
@Value("${jwt.algorithm}")
private String algorithm;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManagerResolver<HttpServletRequest> tenantAuthManagerResolver) throws Exception {
//@formatter:off
http
.authorizeRequests()
.mvcMatchers("/").permitAll()
.mvcMatchers("/protectedservice/**").authenticated()
.and().cors()
.and().oauth2ResourceServer(oauth2 -> oauth2
.authenticationManagerResolver(tenantAuthManagerResolver)
);
//@formatter:on
return http.build();
}
@Bean
public AuthenticationManagerResolver<HttpServletRequest> tenantAuthManagerResolver() {
return new TenantAuthenticationManagerResolver(keyStore, storePasswd);
}
}
依赖关系:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
应用程序中的属性.properties:
jwt.keystore.location=/absolute_path_to/truststore.jks
jwt.keystore.password=your_trustsrore_passwd
jwt.algorithm=RS512
进一步要求:
使用 Java keytool 在密钥库中创建密钥对(自签名证书)(保留在身份验证/授权服务器上):
keytool -genkey -keyalg RSA -alias my_alias -keystore my_keystore_file.jks -storepass my_keystore_pass -validity 360 -keysize 2048 -storetype JKS
从密钥库中提取公钥(证书):
keytool -exportcert -alias my_alias -keystore my_keystore.jks -storepass my_keystore_pass -rfc -file my_cert_file.pem
将此证书导入到持有公钥的新密钥库(信任库)中(保留在资源服务器上):
keytool -importcert -alias my_alias -file my_cert_file.pem -keystore my_truststore_file.jks -storepass my_store_pass
对于多租户,请将更多具有不同别名的密钥对添加到密钥库,然后提取证书(公钥)并将其添加到信任库。 my_truststore_file.jks 将在资源服务器的配置属性 jwt.keystore.location 中使用。
用于使用存储在密钥库中的私钥生成签名 JWT 的代码(这应该在 Security Oauth2 Auth 服务器上实现)。我将此代码放入 JUnit 测试类中:
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import com.nimbusds.jose.JOSEObjectType;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.crypto.RSASSASigner;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
class TestJWTGeneration {
@Test
void testCreateJWTNimusKS() throws Exception {
PrivateKey privateKey = getPrivateKeyFromKeystore("/absolute_path_to/keystore.jks", "my_alias");
// Create RSA-signer with the private key
JWSSigner signer = new RSASSASigner(privateKey);
//@formatter:off
// Prepare JWT with claims set
// 1 day JWT validity
Date expirationDate = Date.from(Instant.now().plus(1L, ChronoField.DAY_OF_MONTH.getBaseUnit()));
JWTClaimsSet claimsSet = new JWTClaimsSet.Builder()
.subject("my_subject")
.issuer("https://my_oauth2_server.com/")
.audience("my_audience")
.issueTime(new Date())
.claim("nonce", Base64.getEncoder().encodeToString(UUID.randomUUID().toString().getBytes()))
.expirationTime(expirationDate)
.build();
//@formatter:on
// put the certificate alias in the JWT header as "kid" field (Key ID)
final JWSHeader header = new JWSHeader.Builder(JWSAlgorithm.RS512).type(JOSEObjectType.JWT).keyID("my_alias").build();
final SignedJWT signedJWT = new SignedJWT(header, claimsSet);
signedJWT.sign(signer);
String jwtSigned = signedJWT.serialize();
assertNotNull(jwtSigned);
System.out.println("##Nimbus JWT=" + jwtSigned);
}
public static PrivateKey getPrivateKeyFromKeystore(String pubKeyFile, String keyAlias) throws FileNotFoundException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
try (FileInputStream myKeys = new FileInputStream(pubKeyFile)) {
KeyStore myTrustStore = KeyStore.getInstance(KeyStore.getDefaultType());
myTrustStore.load(myKeys, "my_keystore_pass".toCharArray());
Key key = myTrustStore.getKey(keyAlias, "my_keystore_pass".toCharArray());
return (PrivateKey) key;
}
}
}
下面的示例有一个 PEM 格式的公钥,并且还获得了 Google 的授权。
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(c -> c.disable())
.cors(c -> c.configurationSource(corsConfigurationSource()))
.authorizeHttpRequests(a -> {
a.requestMatchers("/", "/error", "/login").permitAll();
a.anyRequest().authenticated();
})
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(j -> j.jwt(Customizer.withDefaults()))
.logout(l -> l
.logoutSuccessUrl("/").permitAll())
;
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
return new JwtDecoder() {
@Override
public Jwt decode(String token) throws JwtException {
try {
if(JWTParser.parse(token).getJWTClaimsSet().getIssuer().equals("self")){
System.err.println("Self!");
JwtDecoder jwtDecoder = NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
return jwtDecoder.decode(token);
} else
if(JWTParser.parse(token).getJWTClaimsSet().getIssuer().equals("https://accounts.google.com")){
System.err.println("Google");
JwtDecoder jwtDecoder = NimbusJwtDecoder.withIssuerLocation("https://accounts.google.com").build();
return jwtDecoder.decode(token);
}
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
};