一、总体架构
1.1、统一代理示意图
注:如果不使用统一代理,javascript adapter会存在跨域问题。
Nginx代理配置
location ^~ /auth/{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://10.110.20.18:8888/auth/;
proxy_redirect off;
}
location ^~ /iot/{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://10.200.84.52:8080/iot-web/;
proxy_redirect off;
}
location ^~ /app1/{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://10.200.84.52:8081/app1/;
proxy_redirect off;
}
location ^~ /app2/{
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header REMOTE-HOST $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://10.200.84.52:8082/app2/;
proxy_redirect off;
}
2.2、服务调用关系示意图
注:图中,蓝线表示RESTFULL Service 调用,红线表示OpenIdConnect协议认证鉴权
- 一个前端应用(Javascript+html)
- 多个后端应用(Spring-boot应用,提供RESTFULL Service)。
- 前端应用通过token调用后端应用提供的RESTFULL Service。
- 后端应用之间也相互调用彼此的RESTFULL Service,采用client to client模式。
- 为每个应用在keycloak中分别创建一个client,所有应用共享一套用户、一套角色(同一个Realm下)。
- 前端应用,植入keycloak-javascript adapter;
- 后端应用植入spring-boot-adapter和spring-security-adapter
- 前端应用凭借bearer + token 或 sessionId 调用RESTFUL服务时,目标服务直接验证token的有效性和权限,不需要与keycloak交互的,即使交互,也是一次性交互(获取keycloak的publickey,用于验证token的签名)
二、安装部署keycloak
2.1、下载地址:
http://www.keycloak.org/downloads.html
2.2、解压安装
如 linux:
unzip keycloak-demo-3.4.3.Final.zip
2.3、启动
Linux/Unix
$ .../bin/standalone.sh
Windows
> ...\bin\standalone.bat
2.4、配置
2.4.1、数据源配置
\keycloak-demo-3.4.3.Final\keycloak\standalone\configuration\standalone.xml
Keycloak 内置h2 数据库,并默认使用h2数据库
\keycloak-demo-3.4.3.Final\keycloak\modules\system\layers\base\com\h2database\h2\main\h2-1.4.193.jar
2.4.2、服务端口配置
\keycloak-demo-3.4.3.Final\keycloak\standalone\configuration\standalone.xml
2.4.3、开启外部访问
默认情况下,keycloak安装完成后,只允许127.0.0.1和localhost访问,要开启外部访问:
需要将文件
\keycloak-demo-3.4.3.Final\keycloak\standalone\configuration\standalone.xml
中的所有127.0.0.1替换为 0.0.0.0
http://10.110.20.18:8888/auth/
2.4.4、初始化管理员账号
可以通过浏览器访问http://localhost:8888/auth进行管理员账号的初始化,
如果不方便通过浏览器访问http://localhost:8888/auth进行管理员账号的初始化,也可以通过命令行的方式进行初始化。
Linux/Unix
$ .../bin/add-user-keycloak.sh -r master -u <username> -p <password>
Windows
> ...\bin\add-user-keycloak.bat -r master -u <username> -p <password>
例如:初始化管理员账号名称:admin,密码:123456a?
[root@WgE0eUFn-tomcat-mlR3qSL1 bin]# ./add-user-keycloak.sh -r master -u admin -p 123456a?
Added 'admin' to '/home/keycloak-demo-3.4.3.Final/keycloak/standalone/configuration/keycloak-add-user.json', restart server to load user
2.5、登录管理控制台
http://10.110.20.18:8888/auth/
点击 Administration Console ,进入管理控制台登录页面。
输入配置步骤【4.4、初始化管理员账号】中初始化的管理员账号和密码
登录成功后,进入管理控制台页面。
三.初始化业务数据
3.1、新建Realm
首先登录管理控制台,展开页面左上角下拉框,点击【Add realm】,展开Add realm页面后,填入realm名称(如:iot),点击【Create】,完成realm的创建。
通过 Configure—Realm Settings进入Realm设置页面,选择Login选项卡,将Require SSL修改为none。(临时关闭,方便测试,生产环境建议开启)
3.2、新建Role
通过页面左上角的select realm下拉框,选择新建的realm iot,进入realm iot的配置管理视图。
通过路径Configure—Roles—Realm Roles,进入Realm Role的管理页面
点击【Add Role】,进入Add Role 页面。
填写 Role Name后,点击【Save】,完成Role的新建。
3.3、新建User
通过页面左上角的select realm下拉框,选择新建的realm iot,进入realm iot的配置管理视图。
通过路径Manage—Users,进入Users的管理页面
点击【Add User】进入新建User页面。
填写 Username(如:zhangsan)后,点击【Save】,完成User的创建。
3.4、为User设置密码
选择新建的User zhangsan,进入Edit页面。
选择Credentials选项卡,填写新密码(如:123456)后,点击【Reset Password】,完成密码的重置。
3.5、为User配置Role
选择新建的User zhangsan,进入Edit页面。
选择Role Mapping选项卡,在Realm Roles区域,选择Roles中的角色(如:user),点击【Add selected】,完成用户的角色配置。
四、前端应用集成
4.1、新建client
登录keycloak管理控制台,选择iot Realm,通过 Configure—clients 进入clients管理页面。
点击页面右上角【Create】按钮,进入Add Client页面。
填写ClientId,如:iot-web
选择协议,如:openid-connect
然后点击【save】,完成client的创建。
创建完成后,会自动进入该client的Edit页面,选择Settings选项卡,
AccessType 选择 public,
Valid Redirect URIs 填写 iot-web应用的访问地址(支持通配符*),
keycloak登录/注销成功后,往client回跳时,
会检查回跳地址是否与该Redirect URIs地址匹配,
如:http://10.110.20.19/iot/*。
Base URL 当前client的默认地址,当keycloak需要往该client跳转或链接时,会使用该地址,如:http://10.110.20.19/iot/
Standard Flow Enabled、Implicit Flow Enabled、Direct Access Grants Enabled、Authorization Enabled对应OAuth2的四种授权许可。
填写好上述信息后,点击【save】完成client iot-web的创建和配置。
4.2、前端应用集成改造
前端应用使用keycloak javascript adapter
4.2.1、准备keycloak.json
登录keycloak管理控制台,
选择iot Realm,
进入Clients管理页面,
选择 iot-web client,进入Edit页面。
选择Installation选项卡。
选择 Format Option:Keycloak OIDC JSON。
点击【Download】,可直接下载。
将下载的keycloak.json放到前端应用iot-web中(与入口文件index.html相同路径即可)。
4.2.2、入口文件修改
4.2.2.1、引入keycloak.js
假设入口文件是index.html,在文件的<head>标签中引入keycloak.js,如:
<head>
<script src="http://10.110.20.19/auth/js/keycloak.js"></script>
</head>
4.2.2.2、执行初始化
//keycloak 初始化
var keycloak = Keycloak();
//注册监听一些事件的回调
//登录成功回调
keycloak.onAuthSuccess = function () {
alert('Auth Success');
};
//登录失败回调
keycloak.onAuthError = function (errorData) {
alert("Auth Error: " + JSON.stringify(errorData) );
};
//token 刷新成功回调
keycloak.onAuthRefreshSuccess = function () {
alert('Auth Refresh Success');
};
//token 刷新失败回调
keycloak.onAuthRefreshError = function () {
alert('Auth Refresh Error');
};
//注销成功回调
keycloak.onAuthLogout = function () {
alert('Auth Logout');
};
//token过期时回调
keycloak.onTokenExpired = function () {
alert('Access token expired.');
};
//初始化参数
var initOptions = {
responseMode: 'fragment', //可选值:fragment、query
flow: 'standard',//可选值:standard、implicit、hybrid
onLoad: 'check-sso' //可选值:check-sso、login-required、或不配置
};
keycloak.init(initOptions).success(function(authenticated) {
alert('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
}).error(function() {
alert('Init Error');
});
4.2.2.3、参数说明
关于token过期
token有效期默认5分钟,refresh-token有效期默认30分钟。
token过期时,可以凭借refresh-token刷新token。
刷新token的时候,refresh-token也会同时刷新。
关于responseMode参数
responseMode参数,可选值fragment和query。
值为fragment时,认证通过后,keycloak往client redirect时,token等参数会放到#之后。
值为query时,认证通过后,keycloak往client redirect时,token等参数会放到?之后。
#之后的参数,是不往后端发送的,只有浏览器端可以获取到window.location.hash。
关于flow参数
可选值:standard(标准授权码模式)、implicit(隐式授权模式)、hybrid(混合模式)
需要与该client在keycloak中的设置信息相匹配,如:
Standard: 标准Oauth2 授权码模式。
Implicit: 标准Oauth2隐式授权模式,不产生refresh token。
Hybrid, 混合模式,
需要client在keycloak中同时开启Standard Flow和Implicit Flow,
认证通过后,keycloak会将授权码和token一起发往client,
client取得accesstoken后可直接使用,
另外,还可以凭借授权码进一步取得refresh token。
关于onLoad参数
可选值:check-sso、login-required、或不配置
1、 不配置
需要显示地调用 login函数,否则不与keycloak交互。
2、 login-required
进入javascript应用时,会检查keycloak是否已登录
如果keycloak未登录,会强制进入keycloak登录页面。
如果keycloak已登录,直接显示已登录状态。
3、 check-sso
进入javascript应用时,会检查keycloak是否已登录,
如果keycloak已登录,javascript应用显示已登录状态。
如果keycloak未登录,javascript显示未登录状态。
4.2.3、调用业务接口
调用业务接口时,在请求header中设置如下参数:
参数名称:Authorization
参数值: "bearer "+keycloak.token
参考示例:
4.2.4、常用函数
/**
/auth/realms/iot/account
返回当前登录账号信息,如:
{
"username": "zhangsan",
"emailVerified": false,
"attributes": {}
}
*/
function loadProfile() {
keycloak.loadUserProfile().success(function(profile) {
output(profile);
}).error(function() {
output('Failed to load profile');
});
}
/**
/auth/realms/iot/protocol/openid-connect/userinfo
返回当前用户信息,如:
{
"sub": "7d59c57b-aba2-4096-9d55-dc7cce45e466",
"preferred_username": "zhangsan"
}
*/
function loadUserInfo() {
keycloak.loadUserInfo().success(function(userInfo) {
output(userInfo);
}).error(function() {
output('Failed to load user info');
});
}
//刷新令牌
function refreshToken(minValidity) {
keycloak.updateToken(minValidity).success(function(refreshed) {
if (refreshed) {
output(keycloak.tokenParsed);
} else {
output('Token not refreshed, valid for ' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');
}
}).error(function() {
output('Failed to refresh token');
});
}
//取得token过期时间
function showExpires() {
if (!keycloak.tokenParsed) {
output("Not authenticated");
return;
}
var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
if (keycloak.refreshTokenParsed) {
o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
}
output(o);
}
//触发登录操作
keycloak.login()
//注销
keycloak.logout()
//取得login url
keycloak.createLoginUrl()
//取得注销url
keycloak.createLogoutUrl()
更多操作可参见keycloak.js
五、后端应用集成
后端应用有多个时,每个都需要在keycloak中创建单独的client。
5.1、新建client
登录keycloak管理控制台,选择iot Realm,通过 Configure—clients 进入clients管理页面。
点击页面右上角【Create】按钮,进入Add Client页面。
填写ClientId,如:app1
选择协议,如:penid-connect
然后点击【save】,完成client的创建。
创建完成后,会自动进入该client的Edit页面,选择Settings选项卡,
AccessType 选择 confidential,
Valid Redirect URIs 填写 app1应用的访问地址(支持通配符*),
keycloak登录/注销成功后,往client回跳时,
会检查回跳地址是否与该Redirect URIs地址匹配,
Redirect URI地址可配置多个。
如:将app1代理前的地址,和代理后的地址同时配置上。
http://10.200.84.52:8081/app1/*
Base URL 当前client的默认地址,
当keycloak需要往该client跳转或链接时,会使用该地址,
如:http://10.200.84.52:8081/
填写好上述信息后,点击【save】完成app1的创建和配置。
选择Crendentials选项卡,记录下Secret。(在后端应用app1中,需要配置app1在keycloak中的clientId和Secret)
同创建app1的方式,以同样的方法,再创建app2 client。如:
http://10.200.84.52:8082/app2/*
5.2、后端应用集成改造
5.2.1、添加依赖 build.gradle
ext {
keycloakVersion = '3.4.3.Final'
}
dependencies {
compile('org.keycloak:keycloak-spring-boot-starter')
compile('org.springframework.boot:spring-boot-starter-security')
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-freemarker')
runtime('org.springframework.boot:spring-boot-devtools')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile('org.springframework.security:spring-security-test')
}
dependencyManagement {
imports {
mavenBom "org.keycloak.bom:keycloak-adapter-bom:${keycloakVersion}"
}
}
5.2.2、application.properties配置
keycloak.auth-server-url=http://10.110.20.19/auth
keycloak.realm=iot
keycloak.public-client=false
keycloak.credentials.secret=8e92f33d-d2b8-4bbb-871f-985e0d852031
keycloak.resource=app1
keycloak.ssl-required = none
#OpenID Connect ID Token attribute to populate the UserPrincipal name with.
#If token attribute is null, defaults to sub.
#Possible values are sub, preferred_username, email, name, nickname, given_name, family_name.
keycloak.principal-attribute=sub
server.port=8081
server.context-path=/app1
译注: to populate XX with YY 用YY填充XX
Yaml格式配置
keycloak:
auth-server-url: http://10.110.20.19/auth
realm: iottest
public-client: false
credentials:
secret: 560ebb2e-5037-4c2a-b4b6-a0570ac4c529
resource: app1
ssl-required: none
principal-attribute: sub
配置说明:参考该client编辑页面,Installation选项卡中信息填写。
5.2.3、新建SecurityConfig
注意放置位置,与入口类同包或入口类子包下。
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.client.KeycloakClientRequestFactory;
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.keycloak.adapters.springsecurity.filter.KeycloakAuthenticationProcessingFilter;
import org.keycloak.adapters.springsecurity.filter.KeycloakPreAuthActionsFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@KeycloakConfiguration
public class SecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Autowired
public KeycloakClientRequestFactory keycloakClientRequestFactory;
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public KeycloakRestTemplate keycloakRestTemplate() {
return new KeycloakRestTemplate(keycloakClientRequestFactory);
}
/**
* Registers the KeycloakAuthenticationProvider with the authentication manager.
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
KeycloakAuthenticationProvider keycloakAuthenticationProvider = keycloakAuthenticationProvider();
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setConvertToUpperCase(true);
keycloakAuthenticationProvider.setGrantedAuthoritiesMapper(mapper);
auth.authenticationProvider(keycloakAuthenticationProvider);
}
/**
* Defines the session authentication strategy.
*/
@Bean
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
@Bean
public KeycloakConfigResolver KeycloakConfigResolver() {
return new KeycloakSpringBootConfigResolver();
}
@Bean
public FilterRegistrationBean keycloakAuthenticationProcessingFilterRegistrationBean(
KeycloakAuthenticationProcessingFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Bean
public FilterRegistrationBean keycloakPreAuthActionsFilterRegistrationBean(
KeycloakPreAuthActionsFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
super.configure(http);
http
.authorizeRequests()
//不需要加 context-path
//.antMatchers("/**").hasRole("USER") //开启后,只要拥有USER角色,就可以访问所有URL
.antMatchers("/products*").hasRole("USER")
.antMatchers("/admin*").hasRole("ADMIN")
.anyRequest().permitAll();
//如果不禁用csrf,调用POST服务时会出错:
//Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
http.csrf().disable();
}
}
说明:类的最下方蓝色字体部分,关于url、角色的配置,需要根据实际业务进行调整。
5.2.4、ClientToClient示例
5.2.4.1、使用KeycloakRestTemplate(推荐)
import java.util.List;
import org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
@Component
class ProductService {
@Autowired
private KeycloakRestTemplate template;
private String endpoint = "http://10.110.20.19/app1/products-list";
public List<String> getProducts() {
ResponseEntity<List> response = template.getForEntity(endpoint, List.class);
return response.getBody();
}
}
更多调用方法参加:
org.keycloak.adapters.springsecurity.client.KeycloakRestTemplate extends RestTemplate implements RestOperations
5.2.4.2、不使用KeycloakRestTemplate
如果是不使用KeycloakRestTemplate,使用其他方式(如:HttpClient)也是可以的,只是需要自己设置header参数:
参数名:Authorization
参数值: "bearer "+ token
Token可以从安全上下文中获取,如:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
KeycloakAuthenticationToken tokenObj = (KeycloakAuthenticationToken) authentication;
String token = tokenObj .getAccount().getKeycloakSecurityContext().getTokenString();