springboot2集成oauth2和keycloak以及admin rest api

前言

以keycloak作爲sso認證中心服務端,springboot2的客戶端集成方式有很多種,例如僅集成keycloak的jar包方式、集成spring security的方式、以及security+oauth2的方式等。
上述三種方式,從實現以及功能上來說均是一個比一個複雜。
另外,springboot作爲普通客戶端的同時,也可以進行更多的集成,進而實現對keycloak服務端的操作,這就涉及到keycloak中admin rest api的調用。
正常而言,rest api符合rest規範,應該是比較簡單的。但是當rest api牽扯到各種權限和角色的時候,會發現很多其他的細節問題會導致這個rest接口無法調通,尤其是這些問題不是代碼本身問題的時候,就會更加讓人摸不着頭腦。
以下是初步集成security+oauth2+admin rest api過程中部分踩坑記錄,其中有很多細節還有待深入理解。

security+oauth2客戶端集成

條件說明

客戶端集成的前提是,有了已經可用的keycloak服務端,並且已經在服務端控制檯創建好了realm、
client、role、scope等,可以參考上一篇:
https://tuzongxun.blog.csdn.net/article/details/96979245

環境說明

spingboot1.x和springboot2.x集成keycloak的方式是有一定差別的,鑑於爲實際項目服務的宗旨,這一次的集成預研,基於springboot2.1.3版本,以下是客戶端集成時的maven依賴配置:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency> 
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-security</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

項目配置說明

oauth2授權驗證時,需要token等令牌,sso單點登錄需要一個統一的登錄入,這些均是keycloak服務端提供,因此就必須在客戶端集成時進行oauth2的配置,各種url指向對應的keycloak服務的url,如下:

server:
  port: 8884 
spring:
  application:
    name: oauthdemo
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: http://127.0.0.1:8080/auth/realms/tzx
          jwk-set-uri: http://127.0.0.1:8080/auth/realms/tzx/protocol/openid-connect/certs
      client:
        provider:
          master:
            issuer-uri: http://127.0.0.1:8080/auth/realms/tzx
            token-uri: http://127.0.0.1:8080/auth/realms/tzx/protocol/openid-connect/token
            authorization-uri: http://127.0.0.1:8080/auth/realms/tzx/protocol/openid-connect/auth
            user-info-uri: http://127.0.0.1:8080/auth/realms/tzx/protocol/openid-connect/userinfo
            jwk-set-uri: http://127.0.0.1:8080/auth/realms/tzx/protocol/openid-connect/certs
            user-info-authentication-method: header
            user-name-attribute: preferred_username
        registration:
          master:
            client-id: faw-api-authz
            client-secret: 2811de6b-e703-4644-8330-617ac5104ca6
            client-name: faw-api-authz
            provider: master
            authorization-grant-type: authorization_code
            client-authentication-method: basic
            scope:
              - email
              - profile
              - openid
              - openid_test_api

上邊內容需要注意的是:

  1. 大部分內容對可以照搬的,僅需改一下ip端口和realm
  2. 各個url裏邊realms後邊的tzx實際上就是在keycloak服務端創建的realm,這裏的tzx就是我自己創建的一個realm。
  3. client-id、client-secret、client-name這些很多資源有講,有示例的,就不多說。
  4. scope裏邊配置的是一個數組,這裏配了四個,實際上前兩個是keycloak我們創建客戶端時就默認會有的,後兩個是需要自己創建的。在本次集成中,實際也只有最後一個起了作用,會看到後邊的代碼中有用到。

WebSecurity配置類

集成了security,並且要啓用的話,就需要根據實際重寫security適配器,簡易代碼如下:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.authorizeRequests().antMatchers("/test/*").hasRole("USER").antMatchers("/token")		.hasAuthority("SCOPE_openid_test_api").anyRequest().authenticated().and().oauth2Login().and()
				.oauth2Client().and().oauth2ResourceServer().jwt();
	}
}

這裏邊需要注意的就是SCOPE_openid_test_api,這個實際上就是上邊說的那個scope,不過呢直到這裏,配置文件中的scope配置其實都還是沒有起作用的,配置文件中的那個配置是在後邊有用。這裏生效的就是代碼裏所寫的,需要保證這個scope在keycloak服務端有創建。

OAuth2Client的配置

我們啓用了oauth2驗證之後,在各個接口就需要token等令牌信息,只有令牌校驗通過,這個接口才應該被正常的訪問。
那麼訪問接口如何攜帶令牌等授權信息,oauth2對restTemplat進行了封住,需要我們使用的時候進行一定的處理,以使它知道如何獲取令牌如何攜帶令牌,初步實現代碼如下:

import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2ClientContext;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails;
import org.springframework.security.oauth2.common.AuthenticationScheme;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client;
@Configuration
@EnableOAuth2Client
public class OAuth2ClientConfiguration {
	@Autowired
	private ClientRegistrationRepository clientRegistrationRepository;
	@Bean
	protected OAuth2ProtectedResourceDetails resource() {
		ClientCredentialsResourceDetails resource = new ClientCredentialsResourceDetails();
		ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("master");
		resource.setAccessTokenUri(clientRegistration.getProviderDetails().getTokenUri());
		resource.setClientId(clientRegistration.getClientId());
		resource.setClientSecret(clientRegistration.getClientSecret());
		resource.setClientAuthenticationScheme(AuthenticationScheme.header);
		resource.setClientAuthenticationScheme(AuthenticationScheme.header);
		List scopes = new ArrayList(clientRegistration.getScopes());
		resource.setScope(scopes);

		return resource;
	}
	@Bean
	public OAuth2RestTemplate restTemplate() {
		AccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
		OAuth2ClientContext oAuth2ClientContext = new DefaultOAuth2ClientContext(accessTokenRequest);
		return new OAuth2RestTemplate(resource(), oAuth2ClientContext);
	}
}

上邊代碼主要作用就是爲了裝配OAuth2RestTemplate這個bean,以用來在後邊發送oauth2授權的請求,下邊就是一個相應的controller。

測試示例

@Autowired
private OAuth2RestTemplate restTemplate;
@GettMapping( "/test/getToken")
public String client() {
		String result = restTemplate.postForObject("http://127.0.0.1:8884/token", null, String.class);
		return result;
}
@PostMapping("/token")
public String getToken(@AuthenticationPrincipal Jwt jwt) {
		String tokenId = jwt.getId();
		String value = jwt.getTokenValue();
		return "tokenId:" + tokenId + ",token:" + value;
}

這裏爲何要寫兩個接口呢?仔細看便會發現,第一個接口就是一個普通接口,除開收到之前security的權限限制外,就沒有別的條件,因此這個接口是可以不用任何額外操作,可以直接在瀏覽器地址欄請求的。
在這個普通接口裏做了二次請求,目標接口用了@AuthenticationPrincipal註解以及jwt令牌類,裏邊的邏輯就是獲取tokenid和token內容。
可能有人已經明白了,之所以這裏一個註解和一個特定類就能拿到token,就是因爲上邊的restTemplate使用的是我們配置過的OAuth2RestTemplate,他在發請求的時候就會去配置文件中查找資源,請求token,然後再發起實際的目標請求。進而在目標接口收到請求的時候就可以根據註解和特定的類拿到token等信息。
到這裏,springboot2+secutiry+oauth2+jwt+keycloak的一個基本集成就算是完成了,瀏覽器訪問http://localhost:8884/test/getToken就會返回token等信息。

admin rest api調用

在上邊的例子中,我們有配置client-id等信息,這些信息均來自與keycloak服務端。網上的各種示例說明,基本都是說的直接在keycloak服務端控制檯創建,也就是跟我們用任何軟件一樣點點鼠標。
而實際上,keycloak提供了admin rest api,以使我們可以再java代碼中調用,來創建各種原本在keycloak控制檯創建的資源,下邊就以創建一個簡單的client作爲示例進行說明。

依賴集成

java操作keycloak服務端,這個java代碼所在的項目實際上本身就是一個客戶端,因此上邊的那些依賴、配置和代碼其實也都是必要的,同時,除了上邊的依賴外,還需要另外集成兩個依賴:

<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-authz-client</artifactId>
<version>6.0.1</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
<version>6.0.1
</dependency>

代碼示例

需要再次聲明的是,java操作keycloak服務端,這個java代碼所在的項目實際上本身就是一個客戶端,因此上邊的那些依賴、配置和代碼其實也都是必要的。
在上邊的基礎上,如果需要用java代碼創建一個client,示例代碼如下:

@RequestMapping("/test/createClient")
	public void test() {
		try {
			ClientRepresentation client = new ClientRepresentation();
			client.setClientId("client-test000123");
			client.setId("client-test-id00123");
			client.setPublicClient(false);
			client.setSecret("1235879");
			client.setEnabled(true);
			client.setProtocol("openid-connect");
			List origins = new ArrayList();
			origins.add("*");
			client.setWebOrigins(origins);
			List urls = new ArrayList();
			urls.add("*");
			client.setRedirectUris(urls);
			client.setClientAuthenticatorType("client-secret");
			client.setServiceAccountsEnabled(true);
			client.setDirectAccessGrantsEnabled(true);
			HttpHeaders headers = new HttpHeaders();
			headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
			HttpEntity<?> httpEntity = new HttpEntity<>(client, headers);
			String jsonStr;
			restTemplate.postForObject("http://127.0.0.1:8080/auth/admin/realms/tzx/clients", httpEntity,String.class, client);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

上邊的代碼主要是參考keycloak官網的admin rest api操作說明:
https://www.keycloak.org/docs-api/6.0/rest-api/
代碼是沒有問題的,但是如果有人從上往下抄一遍,會發現執行上邊的創建客戶端的請求會報錯:

org.springframework.web.client.HttpClientErrorException$Forbidden: 403 Forbidden
	at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:83)
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:122)
	at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:102)

爲何會這樣呢?這其實就是我說的坑,原因在與當前這個client的role裏沒有創建client的權限。
例如我這裏配置文件裏的client是faw-api-authz,那麼要想成功用java代碼創建別的client
,就需要faw-api-authz擁有創建client的權限,需要在keycloak控制檯設置步驟依次如下:
進入tzx這個realm——》點擊clients——》找到並點擊faw-api-authz——》點擊service Account roles——》找到client roles——》點擊下拉框,找到realm management——》選擇create client和manage clients——》點擊add select。
注:settings那裏的service accounts enable需要打開。
有了上邊的設置之後,重啓springboot服務,再次訪問,會發現我們新的client就創建成功了,在keycloak的 控制檯的clients也會看到多了一個,各種參數就是java代碼裏寫的參數。
至此,springboot2集成security+oauth2+jwt+keycloak+keycloak admin rest api基本完成,各種細節性的配置和選擇需要在此基礎上進一步優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章