系列目录
(一)keycloak 部署运行及源码打包
(二)keycloak 配置运行
(三)keycloak 基于SpringBoot、Servlet的客户端开发
(四)keycloak 自定义用户(SPI)开发
(五)keycloak 自定义主题
(未完成)(六)keycloak 添加登录验证码功能
(七)keycloak 设置客户端访问类型 bearer-only
(八)keycloak 设置客户端访问类型 confidential
(九)keycloak使用nginx来配置https
前言
keycloak是一套完整的开源认证授权管理解决方案,由红帽开发,提供了多种语言库,方便集成。本系列教程以使用为主,介绍keycloak的搭建,源码编译,以及部分功能的二次开发。
keycloak官网提供了详细的教程以及示例,可以参考官网示例进行编写开发。
官网地址 本系列教程基于官网最新版本18.0进行编写。
keycloak提供了一套默认的主题,通常情况下,我们都需要使用自己的登录页面。keycloak的默认主题包整个后台所有的页面,大部分情况下我们的需求只要修改登录页面即可,本章节我们主要讲解下自定义用户登录页面
1.效果图
2.技术点
- Freemarker模板语言
- vue 前端框架
- ElementUI vuejs的ui框架
3.开发
我们以修改登录页面为例,首先我们到源码中复制一份现有的主题文件,路径/keycloak/themes/src/main/resources/theme
其中keycloak文件夹是默认的控制台登录主题,如果您只需要一个最简单原始的主题,可以直接复制base文件夹。
打开keycloak文件夹,里面对应了很多子文件夹,根据文件夹的名字我们就能看出对应功能。比如我们本次教程的修改登录页面。
打开login文件夹,这个文件夹就是登录页面的所有文件
其中,我们主要修改的是login.ftl 文件,所有的css、images、js 放在了resources文件夹下,还有一个需要注意的地方
theme.properties文件 这个文件配置了一些全局的参数,比如我们需要添加全局样式表。
比如上图所示的,我们统一引用了elementui的css和js,这里的路径是相对 resources文件夹来说的,
即:styles=css/index.min.css 的实际路径是 ./login/resources/css/index.min.css
那么在配置完成后,我们就可以开始真正的编写自己的登录页面了,打开login.ftl文件。这个ftl的后缀是freemark模板的后缀,语法比较简单,各位自行搜索语法规范。
打开login.ftl文件后,可以看到里面有很多内容,我个人比较倾向于自己重新编写所有内容,然后在关键位置复制原来的变量和判断代码,这样写起来比较清晰。
比如我实际的页面是这样些的
红框中的内容就是keycloak原来的freemark变量,(freemark语法和变量以$和<#这种写法开头)
是不是很简单。
同样需要注意下,里面有一部分内容在页面上是不显示的,需要在后台开启后才会显示,如果你也需要这些功能,请结合后台一起调试,比如:是否开启忘记密码,是否开启注册,第三方登录等。
附上我的登录页面的全部内容:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="robots" content="noindex, nofollow">
<title>${msg("loginTitle",(realm.displayName!''))}</title>
<link rel="icon" href="${url.resourcesPath}/img/favicon.ico" />
<link rel="stylesheet" href="${url.resourcesPath}/css/login.css" />
<link rel="stylesheet" href="${url.resourcesPath}/css/index.min.css" />
<script src="${url.resourcesPath}/js/vue.min.js" type="text/javascript"></script>
<script src="${url.resourcesPath}/js/index.min.js" type="text/javascript"></script>
<script src="${url.resourcesPath}/js/svg-icon.js" type="text/javascript"></script>
<style>
.form-item {
position: relative;
}
.form-item-input {
border: 1px solid #ddd;
background: #fff;
border-radius: 5px;
color: #454545;
margin: 30px 0px;
}
.form-error {
border-color: red;
transition-property: border-color;
transition-duration: 0.5s
}
.el-input {
display: inline-block;
height: 47px;
width: 85%;
}
.el-input input {
background: transparent;
border: 0px;
-webkit-appearance: none;
border-radius: 0px;
padding: 12px 5px;
color: #333;
font-size: 16px;
height: 47px;
caret-color: #333;
}
.login-button {
width: 100%;
margin: 0px 0 16px 0;
padding: 16px 0px;
height: 48px;
}
.digital-code-input {
margin: 0px 24px 0px 0px;
padding-left: 12px;
flex: 1;
}
</style>
</head>
<#assign displayMessage=!messagesPerField.existsError('username','password')>
<body>
<div id="app" class="root">
<div class="left">
<img class="logo" src="${url.resourcesPath}/img/logo.png" />
<div class="logo-main">
<img src="${url.resourcesPath}/img/2-2.png" />
</div>
</div>
<div class="right">
<div class="login-form">
<#if realm.password>
<form ref="loginForm" action="${url.loginAction}" @submit.prevent="submitLogin" method="post">
<h3 class="login-title">${kcSanitize(msg("loginTitleHtml",(realm.displayNameHtml!'')))?no_esc}</h3>
<!-- 用户名 -->
<#if !usernameHidden??>
<div class="form-item">
<div class="form-item-input" :class="rule.username ? '' : 'form-error'">
<span class="svg-container">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-yonghu"></use>
</svg>
</span>
<el-input tabindex="1" ref="username" v-model="loginForm.username" @change="checkUserName" placeholder="账号" name="username" type="text" clearable auto-complete="on" />
</div>
<div v-if="!rule.username" class="el-form-item__error">请输入账号</div>
</div>
</#if>
<div class="form-item">
<div class="form-item-input" :class="rule.password ? '' : 'form-error'">
<span class="svg-container">
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-mima"></use>
</svg>
</span>
<el-input tabindex="2" ref="password" v-model="loginForm.password" @change="checkPassword" placeholder="密码" name="password" clearable show-password />
</div>
<div v-if="!rule.password" class="el-form-item__error">请输入密码</div>
</div>
<!-- 记住,忘记密码,后台开启 -->
<div id="kc-form-options" class="remember-box">
<#if realm.rememberMe && !usernameHidden??>
<el-tooltip class="item" effect="dark" placement="top-start">
<div slot="content">在会话有效期间,如果您关闭了浏览器,<br/>再次打开系统后,将自动保持关闭前的状态</div>
<div class="checkbox">
<label class="el-checkbox">
<span class="el-checkbox__input">
<#if login.rememberMe??>
<input id="rememberMe" name="rememberMe" type="checkbox" checked>
<#else>
<input id="rememberMe" name="rememberMe" type="checkbox">
</#if>
</span>
<span>${msg("rememberMe")}</span>
</label>
</div>
</el-tooltip>
</#if>
<#if realm.resetPasswordAllowed>
<div class="forgot-password">
<el-link type="primary" href="${url.loginResetCredentialsUrl}">${msg("doForgotPassword")}</el-link>
</div>
</#if>
</div>
<input type="hidden" id="id-hidden-input" name="credentialId" <#if auth.selectedCredential?has_content>value="${auth.selectedCredential}"</#if>/>
<el-button :loading="loading" type="primary" name="login" class="login-button" round native-type="submit">${msg("doLogIn")}</el-button>
</form>
</#if>
<!-- 其他登录方式 -->
<#if auth?has_content && auth.showTryAnotherWayLink()>
<form id="kc-select-try-another-way-form" action="${url.loginAction}" method="post">
<div class="${properties.kcFormGroupClass!}">
<input type="hidden" name="tryAnotherWay" value="on"/>
<a href="#" id="try-another-way"
onclick="document.forms['kc-select-try-another-way-form'].submit();return false;">${msg("doTryAnotherWay")}</a>
</div>
</form>
</#if>
<!-- 第三方登录 -->
<#if realm.password && social.providers??>
<div>
<ul>
<#list social.providers as p>
<a id="social-${p.alias}" type="button" href="${p.loginUrl}">
<#if p.iconClasses?has_content>
<i class="${properties.kcCommonLogoIdP!} ${p.iconClasses!}" aria-hidden="true"></i>
<span>${p.displayName!}</span>
<#else>
<span>${p.displayName!}</span>
</#if>
</a>
</#list>
</ul>
</div>
</#if>
<!-- 允许注册,后台开启, 注册按钮 -->
<#if realm.password && realm.registrationAllowed && !registrationDisabled??>
<div class="register-user">
<el-link type="primary" href="${url.registrationUrl}">${msg("noAccount")} ${msg("doRegister")}</el-link>
</div>
</#if>
</div>
</div>
<div class="copyright">© 2022 ChangSir. All Rights Reserved.</div>
</div>
</body>
<script>
new Vue({
el: '#app',
data: function() {
return {
loginForm: {
username: "${(login.username!'')}",
password: "",
codeToken: "",
},
rule: {
username: true,
password: true,
codeToken: true,
},
loading: false,
}
},
created: function() {
this.$nextTick(() => {
//其他错误
<#if displayMessage && message?has_content && (message.type != 'warning' || !isAppInitiatedAction??)>
this.$message({
dangerouslyUseHTMLString: true,
type: 'error',
message: "${kcSanitize(message.summary)?no_esc}"
})
//用户密码错误
<#elseif messagesPerField.existsError('username','password') || (usernameHidden?? && messagesPerField.existsError('username','password'))>
this.$message({
dangerouslyUseHTMLString: true,
type: 'error',
message: "${kcSanitize(messagesPerField.getFirstError('username','password'))?no_esc}"
})
</#if>
var req = new XMLHttpRequest(); req.open('GET', document.location.href, false); req.send(null); var headers = req.getAllResponseHeaders(); console.log(headers);
});
},
methods: {
//检查用户名
checkUserName(value) {
this.rule.username = value.trim().length > 0
},
//检查密码
checkPassword(value) {
this.rule.password = value.trim().length > 0
},
//检查验证码
checkCode(value) {
this.rule.codeToken = value.trim().length > 0
},
//登录
submitLogin() {
//校验
this.checkUserName(this.loginForm.username)
this.checkPassword(this.loginForm.password)
//全部校验通过
if(this.rule.username && this.rule.password) {
this.$refs.loginForm.submit();
} else {
return
}
},
}
})
</script>
</html>
4.配置
完成上述开发后,就需要进行配置了。首先我们修改下刚才的文件名(原先的keycloak文件夹),这个文件名就是我们下面在后台中看到的主题名称,请使用英文命名,这里我们命名为test。
现在我们打开keycloak的程序,找到themes文件,将test文件夹复制进去
然后启动我们的keycloak,打开后台,我们以master实例为例,直接修改master的登录页面,也就是我们keycloak控制台的页面。
在 领域设置 菜单的 主题 标签下,找到登录主题
点击登录主题,可以看到这里有一个test,就是我们刚才修改的文件夹。
选择test,然后点击下方的保存,并退出重新登录,这时候你会发现你的登录页面已经变成了你修改的内容了。
聪明的你就完成了keycloak自定义主题开发。
下一节我们会讲下 验证码的配置和开发