2021年8月24日 | Leave a comment https://www.cnblogs.com/vigoz/p/12984556.html 登录及身份认证是现代web应用最基本的功能之一,对于企业内部的系统,多个系统往往希望有一套SSO服务对企业用户的登录及身份认证进行统一的管理,提升用户同时使用多个系统的体验,Keycloak正是为此种场景而生。本文将简明的介绍Keycloak的安装、使用,并给出目前较流行的前后端分离应用如何快速接入Keycloak的示例。 Keycloak是什么 Keycloak是一种面向现代应用和服务的开源IAM(身份识别与访问管理)解决方案 Keycloak提供了单点登录(SSO)功能,支持OpenID Connect、OAuth 2.0、SAML 2.0标准协议,拥有简单易用的管理控制台,并提供对LDAP、Active Directory以及Github、Google等社交账号登录的支持,做到了非常简单的开箱即用。 Keycloak常用核心概念介绍 首先通过官方的一张图来了解下整体的核心概念 这里先只介绍4个最常用的核心概念: Users: 用户,使用并需要登录系统的对象 Roles: 角色,用来对用户的权限进行管理 Clients: 客户端,需要接入Keycloak并被Keycloak保护的应用和服务 Realms: 领域,领域管理着一批用户、证书、角色、组等,一个用户只能属于并且能登陆到一个域,域之间是互相独立隔离的, 一个域只能管理它下面所属的用户 Keycloak服务安装及配置 安装Keycloak Keycloak安装有多种方式,这里使用Docker进行快速安装 docker run -d --name keycloak \ -p 8080:8080 \ -e KEYCLOAK_USER=admin \ -e KEYCLOAK_PASSWORD=admin \ jboss/keycloak:10.0.0 123456 docker run -d --name keycloak \ -p 8080:8080 \ -e KEYCLOAK_USER=admin \ -e KEYCLOAK_PASSWORD=admin \ jboss/keycloak:10.0.0 访问http://localhost:8080并点击Administration Console进行登录 创建Realm 创建一个新的realm: demo,后续所有的客户端、用户、角色等都在此realm中创建 创建客户端 创建前端应用客户端 创建一个新的客户端:vue-demo,Access Type选择public 创建后端应用客户端 创建一个新的客户端:spring-boot-demo,Access Type选择bearer-only 保存之后,会出现Credentials的Tab,记录下这里的secret,后面要用到 关于客户端的访问类型(Access Type) 上面创建的2个客户端的访问类型分别是public、bearer-only,那么为什么分别选择这种类型,实际不同的访问类型有什么区别呢? 事实上,Keycloak目前的访问类型共有3种: confidential:适用于服务端应用,且需要浏览器登录以及需要通过密钥获取access token的场景。典型的使用场景就是服务端渲染的web系统。 public:适用于客户端应用,且需要浏览器登录的场景。典型的使用场景就是前端web系统,包括采用vue、react实现的前端项目等。 bearer-only:适用于服务端应用,不需要浏览器登录,只允许使用bearer token请求的场景。典型的使用场景就是restful api。 创建用户和角色 创建角色 创建2个角色:ROLE_ADMIN、ROLE_CUSTOMER 创建用户 创建2个用户:admin、customer 绑定用户和角色 给admin用户分配角色ROLE_ADMIN 给customer用户分配角色ROLE_CUSTOMER Vue应用集成Keycloak简明指南 创建vue项目 vue create vue-demo 12 vue create vue-demo 添加官方Keycloak js适配器 npm i keycloak-js --save npm i axios --save 123 npm i keycloak-js --savenpm i axios --save main.js <span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span> <span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.vue'</span> <span class="hljs-keyword">import</span> Keycloak <span class="hljs-keyword">from</span> <span class="hljs-string">'keycloak-js'</span> Vue.config.productionTip = <span class="hljs-literal">false</span> <span class="hljs-comment">// keycloak init options</span> <span class="hljs-keyword">const</span> initOptions = { <span class="hljs-attr">url</span>: <span class="hljs-string">'http://127.0.0.1:8080/auth'</span>, <span class="hljs-attr">realm</span>: <span class="hljs-string">'demo'</span>, <span class="hljs-attr">clientId</span>: <span class="hljs-string">'vue-demo'</span>, <span class="hljs-attr">onLoad</span>:<span class="hljs-string">'login-required'</span> } <span class="hljs-keyword">const</span> keycloak = Keycloak(initOptions) keycloak.init({ <span class="hljs-attr">onLoad</span>: initOptions.onLoad, <span class="hljs-attr">promiseType</span>: <span class="hljs-string">'native'</span> }).then(<span class="hljs-function">(<span class="hljs-params">authenticated</span>) =></span>{ <span class="hljs-function"><span class="hljs-title">if</span>(<span class="hljs-params">!authenticated</span>)</span> { <span class="hljs-built_in">window</span>.location.reload(); } <span class="hljs-keyword">else</span> { Vue.prototype.$keycloak = keycloak <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Authenticated'</span>) } <span class="hljs-keyword">new</span> Vue({ <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-params">h</span> =></span> h(App), }).$mount(<span class="hljs-string">'#app'</span>) <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =></span>{ keycloak.updateToken(<span class="hljs-number">70</span>).then(<span class="hljs-function">(<span class="hljs-params">refreshed</span>)=></span>{ <span class="hljs-keyword">if</span> (refreshed) { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Token refreshed'</span>); } <span class="hljs-keyword">else</span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Token not refreshed, valid for '</span> + <span class="hljs-built_in">Math</span>.round(keycloak.tokenParsed.exp + keycloak.timeSkew - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime() / <span class="hljs-number">1000</span>) + <span class="hljs-string">' seconds'</span>); } }).catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Failed to refresh token'</span>, error) }) }, <span class="hljs-number">60000</span>) }).catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Authenticated Failed'</span>, error) }) 123456789101112131415161718192021222324252627282930313233343536373839404142434445 <span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span><span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">'./App.vue'</span><span class="hljs-keyword">import</span> Keycloak <span class="hljs-keyword">from</span> <span class="hljs-string">'keycloak-js'</span> Vue.config.productionTip = <span class="hljs-literal">false</span> <span class="hljs-comment">// keycloak init options</span><span class="hljs-keyword">const</span> initOptions = { <span class="hljs-attr">url</span>: <span class="hljs-string">'http://127.0.0.1:8080/auth'</span>, <span class="hljs-attr">realm</span>: <span class="hljs-string">'demo'</span>, <span class="hljs-attr">clientId</span>: <span class="hljs-string">'vue-demo'</span>, <span class="hljs-attr">onLoad</span>:<span class="hljs-string">'login-required'</span>} <span class="hljs-keyword">const</span> keycloak = Keycloak(initOptions) keycloak.init({ <span class="hljs-attr">onLoad</span>: initOptions.onLoad, <span class="hljs-attr">promiseType</span>: <span class="hljs-string">'native'</span> }).then(<span class="hljs-function">(<span class="hljs-params">authenticated</span>) =></span>{ <span class="hljs-function"><span class="hljs-title">if</span>(<span class="hljs-params">!authenticated</span>)</span> { <span class="hljs-built_in">window</span>.location.reload(); } <span class="hljs-keyword">else</span> { Vue.prototype.$keycloak = keycloak <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Authenticated'</span>) } <span class="hljs-keyword">new</span> Vue({ <span class="hljs-attr">render</span>: <span class="hljs-function"><span class="hljs-params">h</span> =></span> h(App), }).$mount(<span class="hljs-string">'#app'</span>) <span class="hljs-built_in">setInterval</span>(<span class="hljs-function">() =></span>{ keycloak.updateToken(<span class="hljs-number">70</span>).then(<span class="hljs-function">(<span class="hljs-params">refreshed</span>)=></span>{ <span class="hljs-keyword">if</span> (refreshed) { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Token refreshed'</span>); } <span class="hljs-keyword">else</span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Token not refreshed, valid for '</span> + <span class="hljs-built_in">Math</span>.round(keycloak.tokenParsed.exp + keycloak.timeSkew - <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().getTime() / <span class="hljs-number">1000</span>) + <span class="hljs-string">' seconds'</span>); } }).catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Failed to refresh token'</span>, error) }) }, <span class="hljs-number">60000</span>) }).catch(<span class="hljs-function"><span class="hljs-params">error</span> =></span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Authenticated Failed'</span>, error)}) HelloWorld.vue <template> <div class="hello"> <h1>{{ msg }}</h1> <div> <p> current user: {{user}} </p> <p> roles: {{roles}} </p> <p> {{adminMsg}} </p> <p> {{customerMsg}} </p> </div> </div> </template> <script> import axios from 'axios' export default { name: 'HelloWorld', props: { msg: String }, data() { return { user: '', roles: [], adminMsg: '', customerMsg: '' } }, created() { this.user = this.$keycloak.idTokenParsed.preferred_username this.roles = this.$keycloak.realmAccess.roles this.getAdmin() .then(response=>{ this.adminMsg = response.data }) .catch(error => { console.log(error) }) this.getCustomer() .then(response => { this.customerMsg = response.data }) .catch(error => { console.log(error) }) }, methods: { getAdmin() { return axios({ method: 'get', url: 'http://127.0.0.1:8082/admin', headers: {'Authorization': 'Bearer ' + this.$keycloak.token} }) }, getCustomer() { return axios({ method: 'get', url: 'http://127.0.0.1:8082/customer', headers: {'Authorization': 'Bearer ' + this.$keycloak.token} }) } } } </script> 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475 <template> <div class="hello"> <h1>{{ msg }}</h1> <div> <p> current user: {{user}} </p> <p> roles: {{roles}} </p> <p> {{adminMsg}} </p> <p> {{customerMsg}} </p> </div> </div></template> <script>import axios from 'axios' export default { name: 'HelloWorld', props: { msg: String }, data() { return { user: '', roles: [], adminMsg: '', customerMsg: '' } }, created() { this.user = this.$keycloak.idTokenParsed.preferred_username this.roles = this.$keycloak.realmAccess.roles this.getAdmin() .then(response=>{ this.adminMsg = response.data }) .catch(error => { console.log(error) }) this.getCustomer() .then(response => { this.customerMsg = response.data }) .catch(error => { console.log(error) }) }, methods: { getAdmin() { return axios({ method: 'get', url: 'http://127.0.0.1:8082/admin', headers: {'Authorization': 'Bearer ' + this.$keycloak.token} }) }, getCustomer() { return axios({ method: 'get', url: 'http://127.0.0.1:8082/customer', headers: {'Authorization': 'Bearer ' + this.$keycloak.token} }) } }}</script> getAdmin()及getCustomer()这2个方法内部分别请求restful api Spring Boot应用集成Keycloak简明指南 添加Keycloak Maven依赖 <span class="hljs-tag"><<span class="hljs-name">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.keycloak<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span> <span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>keycloak-spring-boot-starter<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span> <span class="hljs-tag"><<span class="hljs-name">version</span>></span>10.0.0<span class="hljs-tag"></<span class="hljs-name">version</span>></span> <span class="hljs-tag"></<span class="hljs-name">dependency</span>></span> 123456 <span class="hljs-tag"><<span class="hljs-name">dependency</span>></span> <span class="hljs-tag"><<span class="hljs-name">groupId</span>></span>org.keycloak<span class="hljs-tag"></<span class="hljs-name">groupId</span>></span> <span class="hljs-tag"><<span class="hljs-name">artifactId</span>></span>keycloak-spring-boot-starter<span class="hljs-tag"></<span class="hljs-name">artifactId</span>></span> <span class="hljs-tag"><<span class="hljs-name">version</span>></span>10.0.0<span class="hljs-tag"></<span class="hljs-name">version</span>></span><span class="hljs-tag"></<span class="hljs-name">dependency</span>></span> Spring Boot配置文件 官方文档及网上大部分示例使用的都是properties格式的配置文件,而yaml格式的配置文件相对更简洁清晰些,此示例使用yaml格式的配置文件,内容如下 <span class="hljs-attr">server:</span> <span class="hljs-attr">port:</span> <span class="hljs-number">8082</span> <span class="hljs-attr">keycloak:</span> <span class="hljs-attr">realm:</span> <span class="hljs-string">demo</span> <span class="hljs-attr">auth-server-url:</span> <span class="hljs-string">http://127.0.0.1:8080/auth</span> <span class="hljs-attr">resource:</span> <span class="hljs-string">spring-boot-demo</span> <span class="hljs-attr">ssl-required:</span> <span class="hljs-string">external</span> <span class="hljs-attr">credentials:</span> <span class="hljs-attr">secret:</span> <span class="hljs-string">2d2ab498-7af9-48c0-89a3-5eec929e462b</span> <span class="hljs-attr">bearer-only:</span> <span class="hljs-literal">true</span> <span class="hljs-attr">use-resource-role-mappings:</span> <span class="hljs-literal">false</span> <span class="hljs-attr">cors:</span> <span class="hljs-literal">true</span> <span class="hljs-attr">security-constraints:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">authRoles:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ROLE_CUSTOMER</span> <span class="hljs-attr">securityCollections:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">customer</span> <span class="hljs-attr">patterns:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">/customer</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">authRoles:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ROLE_ADMIN</span> <span class="hljs-attr">securityCollections:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">admin</span> <span class="hljs-attr">patterns:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">/admin</span> 1234567891011121314151617181920212223242526 <span class="hljs-attr">server:</span> <span class="hljs-attr">port:</span> <span class="hljs-number">8082</span><span class="hljs-attr">keycloak:</span> <span class="hljs-attr">realm:</span> <span class="hljs-string">demo</span> <span class="hljs-attr">auth-server-url:</span> <span class="hljs-string">http://127.0.0.1:8080/auth</span> <span class="hljs-attr">resource:</span> <span class="hljs-string">spring-boot-demo</span> <span class="hljs-attr">ssl-required:</span> <span class="hljs-string">external</span> <span class="hljs-attr">credentials:</span> <span class="hljs-attr">secret:</span> <span class="hljs-string">2d2ab498-7af9-48c0-89a3-5eec929e462b</span> <span class="hljs-attr">bearer-only:</span> <span class="hljs-literal">true</span> <span class="hljs-attr">use-resource-role-mappings:</span> <span class="hljs-literal">false</span> <span class="hljs-attr">cors:</span> <span class="hljs-literal">true</span> <span class="hljs-attr">security-constraints:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">authRoles:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ROLE_CUSTOMER</span> <span class="hljs-attr">securityCollections:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">customer</span> <span class="hljs-attr">patterns:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">/customer</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">authRoles:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">ROLE_ADMIN</span> <span class="hljs-attr">securityCollections:</span> <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">admin</span> <span class="hljs-attr">patterns:</span> <span class="hljs-bullet">-</span> <span class="hljs-string">/admin</span> 除了几个必填的配置项外,另外需要注意的几个配置项如下 credentials.secret:上文添加客户端后Credentials Tab内对应的内容 bearer-only:设置为true,表示此应用的Keycloak访问类型是bearer-only cors:设置为true表示允许跨域访问 security-constraints:主要是针对不同的路径定义角色以达到权限管理的目的 /customer:只允许拥有ROLE_CUSTOMER角色的用户才能访问 /admin:只允许拥有ROLE_ADMIN角色的用户才能访问 未配置的路径表示公开访问 Controller内容 @RestController public class HomeController { @RequestMapping(<span class="hljs-string">"/"</span>) public String <span class="hljs-function"><span class="hljs-title">index</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"index"</span>; } @RequestMapping(<span class="hljs-string">"/customer"</span>) public String <span class="hljs-function"><span class="hljs-title">customer</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"only customer can see"</span>; } @RequestMapping(<span class="hljs-string">"/admin"</span>) public String <span class="hljs-function"><span class="hljs-title">admin</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"only admin cas see"</span>; } } 123456789101112131415161718 @RestControllerpublic class HomeController { @RequestMapping(<span class="hljs-string">"/"</span>) public String <span class="hljs-function"><span class="hljs-title">index</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"index"</span>; } @RequestMapping(<span class="hljs-string">"/customer"</span>) public String <span class="hljs-function"><span class="hljs-title">customer</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"only customer can see"</span>; } @RequestMapping(<span class="hljs-string">"/admin"</span>) public String <span class="hljs-function"><span class="hljs-title">admin</span></span>() { <span class="hljs-built_in">return</span> <span class="hljs-string">"only admin cas see"</span>; }} 项目效果演示 分别启动前后端项目后,本地8081端口对应vue前端项目,本地8082端口对应Spring Boot实现的restful api项目 首次访问vue前端项目 第一次访问vue项目会跳转Keycloak登录页 登录admin用户 登录customer用户 总结 Keycloak部署及接入简单,轻量的同时功能又不失强大,非常适合企业内部的SSO方案。 本文示例项目地址:keycloak-demo