前言
以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
上邊內容需要注意的是:
- 大部分內容對可以照搬的,僅需改一下ip端口和realm
- 各個url裏邊realms後邊的tzx實際上就是在keycloak服務端創建的realm,這裏的tzx就是我自己創建的一個realm。
- client-id、client-secret、client-name這些很多資源有講,有示例的,就不多說。
- 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基本完成,各種細節性的配置和選擇需要在此基礎上進一步優化。