系统要求
JDK17
spring-security-oauth2-authorization-server 1.3.0 以上
spring-boot-starter-oauth2-client 3.3.0 以上
Spring Boot 3.3.0 以上
该文章中的项目代码是基于demo_05的代码进行改进的。如果不了解,可以直接下载demo_05,或看之前的文章。
修改授权服务
开启jwt认证设置
/**
* 应用注册仓库
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwdEncoder) {
// 客户端相关配置
ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用户授权确认
.requireAuthorizationConsent(false)
//如果是客户端认证(client_credentials)且client-authentication-method=client_secret_jwt 会用这个对称算法,当然也可以设置一个非对称算法
.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.HS256)
.requireProofKey(false)//如果authorization_code,且client-authentication-method=none,会自动开启
//如果用非对称算法,就需要客户端也提供一个公钥,客户端对自己的私钥加密自己的认证信息,提交给服务端,
//并提供一个链接让服务端获取客户端公钥。服务端验证通过后会用自己的私钥签名并加密token颁发给客户端
//.jwkSetUrl("http://localhost:8080/jwks")
.build();
TokenSettings tokenSetting=TokenSettings.builder()
.authorizationCodeTimeToLive(Duration.ofSeconds(60))
.accessTokenTimeToLive(Duration.ofHours(2))
.refreshTokenTimeToLive(Duration.ofHours(5))
.reuseRefreshTokens(false)//如果为true表示refreshToken可以重复使用,false则每次返回新的
.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.build();
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("clientid")
.clientSecret(passwdEncoder.encode("client_secret"))如果是CLIENT_SECRET_POST才会用到
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) //不建议使用,安全性低
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST) //不建议使用,安全性低
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8010/login/oauth2/code/authcode")
.postLogoutRedirectUri("http://localhost:8010/index")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(clientSettings)
.tokenSettings(tokenSetting)
.build();
JdbcRegisteredClientRepository jdbcRegister=new JdbcRegisteredClientRepository(jdbcTemplate);
if(null==jdbcRegister.findByClientId(oidcClient.getClientId())) {
jdbcRegister.save(oidcClient);
}
return jdbcRegister;
}
可以看到对于客户端的认证有很多方式,本次主要介绍CLIENT_SECRET_JWT方式,所以只要加一行:
clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
就可以,其它的方式可以不加,都加上也不影响。
还有就是ClientSettings对象,得设置到客户端用的那种加密算法。这里用的是对称加密算法MacAlgorithm.HS256,秘钥是client_secret
修改客户端服务
YML配置
server:
port: 8010
logging:
level:
root: info
spring:
freemarker:
template-loader-path: classpath:/templates/
suffix: .html
charset: UTF-8
cache: false
content-type: text/html
application:
name: oauth_client
security:
oauth2:
client:
provider:
oauth:
issuer-uri: http://www.oauth2server.com:8080
authorization-uri: http://www.oauth2server.com:8080/oauth2/authorize
token-uri: http://www.oauth2server.com:8080/oauth2/token
registration:
clientSecretBasic:
provider: oauth
client-id: clientid
client-secret: client_secret
client-authentication-method: client_secret_basic
authorization-grant-type: client_credentials
scope: openid,profile
clientSecretPost:
provider: oauth
client-id: clientid
client-secret: client_secret
client-authentication-method: client_secret_post
authorization-grant-type: client_credentials
scope: openid,profile
clientSecretJwt:
provider: oauth
client-id: clientid
client-secret: client_secret
client-authentication-method: client_secret_jwt
authorization-grant-type: client_credentials
scope: openid,profile
authcode:
provider: oauth
client-id: clientid
client-authentication-method: none
authorization-grant-type: authorization_code
scope: openid,profile
redirect-uri: '{baseUrl}/login/oauth2/code/authcode'
这个是在demo_05客户端代码的基础上,加了一段:clientSecretJwt的配置
添加Controller接口
/**
* client-authentication-method=client_secret_jwt
* 授权码模式认证示例4
* @param model
* @param authorizedClient
* @param oauth2User
* @return
*/
@GetMapping("clientSecretJwt")
public Object authCodeClientSecretJwt(Model model, @RegisteredOAuth2AuthorizedClient("clientSecretJwt") OAuth2AuthorizedClient authorizedClient) {
System.out.println(authorizedClient.getAccessToken().getTokenValue());
model.addAttribute("token", authorizedClient.getAccessToken().getTokenValue());
return authorizedClient;
}
配置DefaultClientCredentialsTokenResponseClient
/**
* 自定义实现DefaultClientCredentialsTokenResponseClient
* 用于处理CLIENT_SECRET_JWT客户端的认证方式
* @return
*/
@Bean
public DefaultClientCredentialsTokenResponseClient CLIENT_SECRET_JWT_TokenResponseClient() {
System.out.println("=============DefaultClientCredentialsTokenResponseClient======================");
Function<ClientRegistration, JWK> jwkResolver = (clientRegistration) -> {
//因为服务端配置的客户端加密方式是:MacAlgorithm.HS256,所以这里用HmacSHA256
if (clientRegistration.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.CLIENT_SECRET_JWT)) {
SecretKeySpec secretKey = new SecretKeySpec(clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8),"HmacSHA256");
return new OctetSequenceKey.Builder(secretKey)
.keyID(UUID.randomUUID().toString())
.build();
}
return null;
};
NimbusJwtClientAuthenticationParametersConverter<OAuth2ClientCredentialsGrantRequest> paramConverter=new NimbusJwtClientAuthenticationParametersConverter<>(jwkResolver);
paramConverter.setJwtClientAssertionCustomizer(clientAssert->{//这里可以自定义一些claim
//clientAssert.getClaims().claim("iss", "http://localhost:8080/clientid");
});
OAuth2ClientCredentialsGrantRequestEntityConverter requestEntityConverter =
new OAuth2ClientCredentialsGrantRequestEntityConverter(){
@Override
protected MultiValueMap<String, String> createParameters(
OAuth2ClientCredentialsGrantRequest clientCredentialsGrantRequest) {
ClientRegistration clientRegistration = clientCredentialsGrantRequest.getClientRegistration();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
parameters.add(OAuth2ParameterNames.GRANT_TYPE, clientCredentialsGrantRequest.getGrantType().getValue());
if (!CollectionUtils.isEmpty(clientRegistration.getScopes())) {
parameters.add(OAuth2ParameterNames.SCOPE,
StringUtils.collectionToDelimitedString(clientRegistration.getScopes(), " "));
}
if (ClientAuthenticationMethod.CLIENT_SECRET_POST.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
parameters.add(OAuth2ParameterNames.CLIENT_SECRET, clientRegistration.getClientSecret());
}
if (ClientAuthenticationMethod.CLIENT_SECRET_JWT.equals(clientRegistration.getClientAuthenticationMethod())||ClientAuthenticationMethod.PRIVATE_KEY_JWT.equals(clientRegistration.getClientAuthenticationMethod())) {
parameters.add(OAuth2ParameterNames.CLIENT_ID, clientRegistration.getClientId());
}
return parameters;
}
};
requestEntityConverter.addParametersConverter(paramConverter);
DefaultClientCredentialsTokenResponseClient tokenResponseClient =
new DefaultClientCredentialsTokenResponseClient();
tokenResponseClient.setRequestEntityConverter(requestEntityConverter);
return tokenResponseClient;
}
/**
* 构建认证客户端对象管理器
* @param clientRegistrationRepository
* @param authorizedClientRepository
* @param client 重新定义的支持CLIENT_SECRET_JWT,或private_key_jwt
* @return
*/
@Bean
public OAuth2AuthorizedClientManager authorizedClientManager(
ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository,
DefaultClientCredentialsTokenResponseClient client) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()//授权码provider,实现正常授权码模式,
.clientCredentials(clientCre->{//客户端provider+CLIENT_SECRET_JWT实现,因为客户端认证默认只支持:none,client_secret_basic,client_secret_post
clientCre.accessTokenResponseClient(client);//所以要自定义一下,让它支持CLIENT_SECRET_JWT
})
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager =
new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
因为Spring Security OAuth2 Server 自带的客户端provider对客户端认证默认只支持:none,client_secret_basic,client_secret_post所以重写一下DefaultClientCredentialsTokenResponseClient
可以看到一行代码:SecretKeySpec secretKey = new SecretKeySpec(clientRegistration.getClientSecret().getBytes(StandardCharsets.UTF_8),"HmacSHA256"); 中把客户端的secret当做了密钥。
常见错误
com.nimbusds.jose.KeyLengthException: The secret length must be at least 256 bits
at com.nimbusds.jose.crypto.impl.MACProvider.<init>(MACProvider.java:125) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
at com.nimbusds.jose.crypto.MACSigner.<init>(MACSigner.java:133) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
at com.nimbusds.jose.crypto.MACSigner.<init>(MACSigner.java:189) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
at com.nimbusds.jose.crypto.factories.DefaultJWSSignerFactory.createJWSSigner(DefaultJWSSignerFactory.java:90) ~[nimbus-jose-jwt-9.37.3.jar:9.37.3]
at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.createSigner(NimbusJwtEncoder.java:208) ~[spring-security-oauth2-jose-6.3.1.jar:6.3.1]
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[na:na]
如果出现以上错误,表示client_secret的长度太短了,因为对称加密算法会把client_secret当做密钥来使用,所以长度有一定要求。
具体代码请参看:demo_06
下期再讲private_key_jwt的认证方式。