前言
以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: test-authz
client-secret: 2811de6b-e703-4644-8330-617ac5104ca6
client-name: test-authz
provider: master
authorization-grant-type: authorization_code
client-authentication-method: basic
scope:
- email
- profile
- openid
- openid_test
上边内容需要注意的是:
- 大部分内容对可以照搬的,仅需改一下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").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基本完成,各种细节性的配置和选择需要在此基础上进一步优化。