8. keycloak和自研系统的集成
简介
keycloak是一个非常强大的权限认证系统,我们使用keycloak可以方便的实现SSO的功能。虽然keycloak底层使用的wildfly,并且提供了非常方便的Client Adapters和各种服务器进行对接,比如wildfly,tomcat,Jetty等。
之前我们也介绍了keycloak如何通过OpenID Connect和SAML协议和wildfly应用进行集成。虽然集成起来非常方便,但是一切都是一个黑盒子,我们并不知道具体的工作原理,今天我们来点实际的,看下keycloak如何和各类自建的系统进行集成。
接入前的前置准备
在接入各种应用之前,需要在keycloak中做好相应的配置。一般来说需要使用下面的步骤:
一般来说,为了隔离不同类型的系统,我们建议为不同的client创建不同的realm。当然,如果这些client是相关联的,则可以创建在同一个realm中。
用户是用来登录keycloak用的,如果是不同的realm,则需要分别创建用户。
这一步是非常重要的,我们需要根据应用程序的不同,配置不同的root URL,redirect URI等。
还可以配置mapper和scope信息。
最后,如果是服务器端的配置的话,还需要installation的一些信息。
有了这些基本的配置之后,我们就可以准备接入应用程序了。
注意,一般来说我们创建的client protocol是openid-connect,这是因为SAML只适用于web程序,并且比较复杂。
SPA单体页面接入keycloak
如果我们是SPA的单体页面,该怎么接入keycloak呢?
页面接入keycloak需要使用keycloak-js这个js客户端。我们直接在页面上引入这个js客户端即可使用了。
先看一下keycloak-js是怎么创建的:
复制 import * as Keycloak from 'keycloak-js' ;
let initOptions = {
url : 'http://127.0.0.1:8080/auth' , realm : 'keycloak-demo' , clientId : 'app-vue' , onLoad : 'login-required'
}
let keycloak = Keycloak (initOptions);
Keycloak客户端的初始化可以接受4个参数,url表示的是keycloak的server url,realm是我们在第一阶段创建的realm,clientId也是在第一阶段创建client时候填的id,最后一个onLoad参数表示的是加载时候执行的action。
有了keycloak的这些配置之后,我们就是可以对其进行初始化了:
复制 keycloak .init ({ onLoad : initOptions .onLoad }) .then ((auth) => {
if ( ! auth) {
window . location .reload ();
} else {
Vue . $log .info ( "Authenticated" );
new Vue ({
el : '#app' ,
render : h => h (App , { props : { keycloak : keycloak } })
})
}
上面代码表示keycloak在初始化的时候首先要去执行login-required操作,也就是说要执行身份认证。
如果没有认证的话将会跳转到keycloak的认证页面。认证完成之后,将会返回auth,表示是否授权成功。
登录成功之后,我们可以通过keycloak对象做很多操作。
比如,登出操作:
复制 < button class = "btn" @click="keycloak.logout()">Logout</button>
获取keycloak的各种属性:
复制 < div class = "jwt-token" >
< h3 style = "color: black;" >JWT Token</ h3 >
{{keycloak.idToken}}
clientId: {{keycloak.clientId}}
Auth Server Url: {{keycloak.authServerUrl}}
</ div >
从keycloak中获取用户信息:
复制 User: {{keycloak.idTokenParsed.preferred_username}}
更重要的是,有了idToken,就可以使用这个token去和keycloak进行交互,获取更多的信息。
我们看下怎么使用keycloak来更新token:
复制 //Token Refresh
setInterval (() => {
keycloak .updateToken ( 70 ) .then ((refreshed) => {
if (refreshed) {
Vue . $log .info ( 'Token refreshed' + refreshed);
} else {
Vue . $log .warn ( 'Token not refreshed, valid for '
+ Math .round ( keycloak . tokenParsed .exp + keycloak .timeSkew - new Date () .getTime () / 1000 ) + ' seconds' );
}
}) .catch (() => {
Vue . $log .error ( 'Failed to refresh token' );
});
} , 6000 )
}) .catch (() => {
Vue . $log .error ( "Authenticated Failed" );
});
SpringBoot 接入keycloak
现在java体系中最流行的就是SpringBoot了,如果在一个通用的SpringBoot程序中接入keycloak呢?
keycloak提供了一个Keycloak Spring Boot adapter ,我们可以这样来引用:
复制 < dependency >
< groupId >org.keycloak</ groupId >
< artifactId >keycloak-spring-boot-starter</ artifactId >
</ dependency >
< dependencyManagement >
< dependencies >
< dependency >
< groupId >org.keycloak.bom</ groupId >
< artifactId >keycloak-adapter-bom</ artifactId >
< version >11.0.2</ version >
< type >pom</ type >
< scope >import</ scope >
</ dependency >
</ dependencies >
</ dependencyManagement >
这个starter支持SpringBoot底层的这些服务器:Tomcat,Undertow,Jetty。
如果是上面3个服务器的话,不需要额外的配置。
有了依赖包之后,我们需要在SpringBoot的配置文件中配置keycloak的一些基本信息:
复制 keycloak . realm = demorealm
keycloak . auth - server - url = http : //127.0.0.1:8080/auth
keycloak . ssl - required = external
keycloak . resource = demoapp
keycloak . credentials . secret = 11111111 - 1111 - 1111 - 1111 - 111111111111
先介绍一下上面配置文件中各项的意思。
注意,配置文件中的内容和keycloak中的各项配置是一致的,大家一定要属性keycloak的配置,多修改多运行一下。
配置文件中realm表示的是我们创建的realm名。
auth-server-url是keycloak中认证服务的url。
ssl-required有三个值,分别是none:所有的请求都不使用HTTPS;external:只有外部请求才使用HTTPS,对于本地的访问使用HTTP;all:所有的请求都使用HTTPS。
Resources就是我们在realm中创建的client。
secret是我们创建的client secret。
我们还可以为keycloak配置一些web.xml中出现的权限:
复制 keycloak . securityConstraints [ 0 ] . authRoles [ 0 ] = admin
keycloak . securityConstraints [ 0 ] . authRoles [ 1 ] = user
keycloak . securityConstraints [ 0 ] . securityCollections [ 0 ] . name = insecure stuff
keycloak . securityConstraints [ 0 ] . securityCollections [ 0 ] . patterns [ 0 ] = / insecure
keycloak . securityConstraints [ 1 ] . authRoles [ 0 ] = admin
keycloak . securityConstraints [ 1 ] . securityCollections [ 0 ] . name = admin stuff
keycloak . securityConstraints [ 1 ] . securityCollections [ 0 ] . patterns [ 0 ] = / admin
上面的配置表示的是访问/insecure需要admin或者user的权限,访问/admin需要admin的权限。
SpringBoot的Rest服务
有了这些配置,我们基本上就可以创建一个基于spring boot和keycloak的一个rest服务了。
假如我们为keycloak的client创建了新的用户:alice。
第一步我们需要拿到alice的access token,则可以这样操作:
复制 export access_token = $(\
curl -X POST http://localhost:8180/auth/realms/spring-boot-quickstart/protocol/openid-connect/token \
-H 'Authorization: Basic YXBwLWF1dGh6LXJlc3Qtc3ByaW5nYm9vdDpzZWNyZXQ=' \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
)
这个命令是直接通过用户名密码的方式去keycloak服务器中拿取access_token,除了access_token,这个命令还会返回refresh_token和session state的信息。
因为是直接和keycloak进行交换,所以keycloak的directAccessGrantsEnabled一定要设置为true。
有小伙伴要问了,上面命令中的Authorization是什么值呢?
这个值是为了防止未授权的client对keycloak服务器的非法访问,所以需要请求客户端提供client-id和对应的client-secret并且以下面的方式进行编码得到的:
复制 Authorization: basic BASE64(client-id + ':' + client-secret)
access_token是JWT格式的,我们可以简单解密一下上面命令得出的token:
复制 {
alg : "RS256" ,
typ : "JWT" ,
kid : "FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"
}.
{
exp : 1603614445 ,
iat : 1603614145 ,
jti : "b69c784d-5b2d-46ad-9f8d-46214add7afb" ,
iss : "http://localhost:8180/auth/realms/spring-boot-quickstart" ,
sub : "e6606d93-99f6-4829-ba99-1329be604159" ,
typ : "Bearer" ,
azp : "app-authz-springboot" ,
session_state : "bdc33764-fd1a-400e-9fe0-90a82f4873c1" ,
acr : "1" ,
allowed-origins : [
"http://localhost:8080"
] ,
realm_access : {
roles : [
"user"
]
} ,
scope : "email profile" ,
email_verified : false ,
preferred_username : "alice"
}.
[signature]
有了access_token,我们就可以根据access_token去做很多事情了。
比如:访问受限的资源:
复制 curl http://localhost:8080/api/resourcea \
-H "Authorization: Bearer " $access_token
这里的api/resourcea只是我们本地spring boot应用中一个非常简单的请求资源链接,一切的权限校验工作都会被keycloak拦截,我们看下这个api的实现:
复制 @ RequestMapping (value = "/api/resourcea" , method = RequestMethod . GET )
public String handleResourceA() {
return createResponse() ;
}
private String createResponse() {
return "Access Granted" ;
}
可以看到这个只是一个简单的txt返回,但是因为有keycloak的加持,就变成了一个带权限的资源调用。
上面的access_token解析过后,我们可以看到里面是没有包含权限信息的,我们可以使用access_token来交换一个特殊的RPT的token,这个token里面包含用户的权限信息:
复制 curl -X POST \
http://localhost:8180/auth/realms/spring-boot-quickstart/protocol/openid-connect/token \
-H "Authorization: Bearer " $access_token \
--data "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket" \
--data "audience=app-authz-rest-springboot" \
--data "permission=Default Resource" | jq --raw-output '.access_token'
将得出的结果解密之后,看下里面的内容:
复制 {
alg : "RS256" ,
typ : "JWT" ,
kid : "FJ86GcF3jTbNLOco4NvZkUCIUmfYCqoqtOQeMfbhNlE"
}.
{
exp : 1603614507 ,
iat : 1603614207 ,
jti : "93e42d9b-4bc6-486a-a650-b912185c35db" ,
iss : "http://localhost:8180/auth/realms/spring-boot-quickstart" ,
aud : "app-authz-springboot" ,
sub : "e6606d93-99f6-4829-ba99-1329be604159" ,
typ : "Bearer" ,
azp : "app-authz-springboot" ,
session_state : "bdc33764-fd1a-400e-9fe0-90a82f4873c1" ,
acr : "1" ,
allowed-origins : [
"http://localhost:8080"
] ,
realm_access : {
roles : [
"user"
]
} ,
authorization : {
permissions : [
{
rsid : "e26d5d63-5976-4959-8683-94b7d85318e7" ,
rsname : "Default Resource"
}
]
} ,
scope : "email profile" ,
email_verified : false ,
preferred_username : "alice"
}.
[signature]
可以看到,这个RPT和之前的access_token的区别是这个里面包含了authorization信息。
我们可以将这个RPT的token和之前的access_token一样使用。
使用KeycloakSecurityContext
KeycloakSecurityContext是keycloak的上下文,我们可以从其中获取到AccessToken,IDToken,AuthorizationContext和realm信息。
而在AuthorizationContext中又包含了授权的权限信息。如果能够在程序中获取到KeycloakSecurityContext,则可以进行更精确的程序控制。
那么怎么获取到KeycloakSecurityContext呢?
复制 private KeycloakSecurityContext getKeycloakSecurityContext() {
return (KeycloakSecurityContext) request . getAttribute ( KeycloakSecurityContext . class . getName ());
}
我们可以直接从request中获取到。
我们看下KeycloakSecurityContext的一些关键方法:
复制 public boolean hasResourcePermission( String name) {
return getAuthorizationContext() . hasResourcePermission (name);
}
public String getName() {
return securityContext . getIdToken () . getPreferredUsername ();
}
public List< Permission > getPermissions() {
return getAuthorizationContext() . getPermissions ();
}
private AuthorizationContext getAuthorizationContext() {
return securityContext . getAuthorizationContext ();
}
Authorization Client Java API
上面介绍的Spring Boot中的其实是隐藏的做法,adaptor自动为我们做了和Keycloak认证服务连接的事情,如果我们需要手动去处理,则需要用到Authorization Client Java API。
添加maven依赖:
复制 < dependencies >
< dependency >
< groupId >org.keycloak</ groupId >
< artifactId >keycloak-authz-client</ artifactId >
< version >${KEYCLOAK_VERSION}</ version >
</ dependency >
</ dependencies >
client会去读取meta-info中的keycloak.json信息:
复制 {
"realm" : "hello-world-authz" ,
"auth-server-url" : "http://localhost:8080/auth" ,
"resource" : "hello-world-authz-service" ,
"credentials" : {
"secret" : "secret"
}
}
接下看下怎么使用AuthzClient。
创建一个:AuthzClient
复制 AuthzClient authzClient = AuthzClient . create ()
获取RPT:
复制 AuthzClient authzClient = AuthzClient . create ();
// create an authorization request
AuthorizationRequest request = new AuthorizationRequest() ;
// send the entitlement request to the server in order to
// obtain an RPT with all permissions granted to the user
AuthorizationResponse response = authzClient . authorization ( "alice" , "alice" ) . authorize (request);
String rpt = response . getToken ();
System . out . println ( "You got an RPT: " + rpt);
本文已收录于 www.flydean.com
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!