SpringSecurity OAuth2 Server认证方式之client_secret_jwt采用jwt签名加密的方式,更加安全!

系统要求

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的认证方式。

  • 7
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

觉自性本然

您的鼓励与支持是我最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值