keycloak 一个应用实例(javascript Adapter + Spring boot + Spring security)

一、总体架构

 

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协议认证鉴权

 

  1. 一个前端应用(Javascript+html)
  2. 多个后端应用(Spring-boot应用,提供RESTFULL Service)。
  3. 前端应用通过token调用后端应用提供的RESTFULL Service。
  4. 后端应用之间也相互调用彼此的RESTFULL Service,采用client to client模式。
  5. 为每个应用在keycloak中分别创建一个client,所有应用共享一套用户、一套角色(同一个Realm下)。
  6. 前端应用,植入keycloak-javascript adapter;
  7.  后端应用植入spring-boot-adapter和spring-security-adapter
  8. 前端应用凭借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/*

                                      http://10.110.20.19/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/*

http://10.110.20.19/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();

 

 

 

 

 

 

 


 

猜你喜欢

转载自huangqiqing123.iteye.com/blog/2412701