赞
踩
在前面两节分别介绍了 Keycloak的下载与使用和keycloak与springboot的集成。
接下来第三节让我们一步步的去完成一个简单的前后端分离项目,并且可以扩展实现sso。
本文将介绍如何使用Spring Boot、Keycloak和Vue构建一个具有前后端分离架构的Web应用程序。通过将前端与后端完全独立开发和部署,我们可以实现更高效的团队协作和灵活的技术选型。Spring Boot提供了一个稳定可靠的后台框架,Keycloak提供了身份验证和授权的解决方案,而Vue作为一种灵活易用的前端框架,使我们能够快速开发出优秀的用户界面。
首先回顾一下上一节提到的访问类型:
public: 适用于客户端应用,如前端web系统,包括采用vue、react实现的前端项目等。不需要秘钥访问。
confidential: 适用于服务端应用,比如需要浏览器登录以及需要通过密钥获取access token的web系统。需要秘钥访问。
bearer-only: 适用于服务端应用,只允许使用bearer token请接口,项目里的权限是针对接口做校验,请求没有带上token就会返回401。需要秘钥访问。
在这里我们新创一个keycloak客户端,因为我们的项目是前后端分离的,所以此客户端的访问类型为 bearer-only。
创建好一个新的springboot项目之后,配置yml文件,下面是我的配置,仅供大家参考。
keycloak: realm: springboot resource: sso-project-backend auth-server-url: http://localhost:8080/ ssl-required: NONE bearer-only: true credentials: secret: nCTbFSEBZDTME14MMf0LBxyzSmmPzbee cors: true #允许跨域 # use-resource-role-mappings: false #鉴权 security-constraints: #需要用户权限的接口 - auth-roles: - user - admin security-collections: - name: user-role - patterns: - /api/v1/* #放行接口 - auth-roles: security-collections: - name: any - patterns: - /api/v1/user/login - /api/v1/user/register - /api/v1/user/register/register-captcha - /api/v1/user/login/captcha - /api/v1/user/retrieve-pwd/captcha - /api/v1/user/getTokenByRefreshToken
里面的相关数据在keycloak客户端的“安装”中,选择json格式,复制粘贴即可。
秘钥我们前两节也讲过了。
最简单的实现思路:前端通过调用后端的注册、登录接口,在后端使用api请求keycloak的相关接口去创建、更新、删除用户的信息。
废话不多说,直接上代码。
在controller层创建一个名为LoginController的文件,在里面编写相关的接口,包括注册、登录、获取验证码等等,示例如下:
@RestController @RequestMapping("/api/v1/user") public class LoginController { @Autowired private LoginService loginService; @SneakyThrows @PostMapping("/register") public Response userRegister(@RequestBody Register register) { return Response.status(loginService.doRegister(register)); } @SneakyThrows @PostMapping("/login") public Response userLogin(@RequestBody Login login) { return Response.success(loginService.doLogin(login)); } @SneakyThrows @GetMapping("/getTokenByRefreshToken") public Response getTokenByRefreshToken(String refreshToken) { if (refreshToken == null) { throw new AuthException("无权限访问!"); } return Response.success(loginService.getTokenByRefreshToken(refreshToken)); } @SneakyThrows @GetMapping("/login/captcha") public Response captcha() { return Response.success(loginService.captcha()); } @SneakyThrows @GetMapping("/register/register-captcha") public Response registerPhoneCaptcha(String phoneNumber) { return Response.status(loginService.phoneCaptcha(phoneNumber, SmsTypeEnum.USER_REGISTER.getType())); } @SneakyThrows @GetMapping("/retrieve-pwd/captcha") public Response forgotPwdCaptcha(String phoneNumber) { return Response.status(loginService.phoneCaptcha(phoneNumber, SmsTypeEnum.RETRIEVE_PWD.getType())); } @SneakyThrows @GetMapping("/logout") public Response logout(String refreshToken) { return Response.success(loginService.logout(refreshToken)); } }
在service层编写对应的接口,直接上代码了哈~
public interface LoginService { /** * 注册 * @param register * @return */ boolean doRegister(Register register); /** * 登录 * @param login * @return */ KeycloakTokenResponse doLogin(Login login); /** * 根据刷新token获取token * @param refreshToken * @return */ KeycloakTokenResponse getTokenByRefreshToken(String refreshToken) throws AuthException; /** * 退出登录 * @param refreshToken * @return */ boolean logout(String refreshToken); /** * 验证码 * @return */ Map<String,String> captcha(); /** * 发送手机验证码 * @param phoneNumber * @param sendType * @return */ boolean phoneCaptcha(String phoneNumber,String sendType); /** * 重置用户登录密码 * @param resetUserPassword * @return */ boolean resetUserPassword(ResetUserPassword resetUserPassword); }
在实现类里面编写对应的方法,示例代码如下:
@Override public boolean doRegister(Register register) { try { String phoneNumber = register.getPhoneNumber(); if (StrUtil.isBlank(register.getPassword()) || StrUtil.isBlank(register.getConfirmPassword())) { throw new RuntimeException("密码不能为空!"); } if (StrUtil.isBlank(phoneNumber)) { throw new RuntimeException("手机号不能为空!"); } if (StrUtil.isBlank(register.getPhoneCaptcha())) { throw new RuntimeException("手机验证码不能为空!"); } if (!register.getPassword().equals(register.getConfirmPassword())) { throw new RuntimeException("两次密码不一致!"); } String key = SmsTypeEnum.USER_REGISTER.getRedisKey() + phoneNumber; //验证码校验 Object captchaObject = redis.get(key); if (captchaObject == null) { throw new RuntimeException("验证码已失效!"); } String redisCaptcha = String.valueOf(captchaObject); if (!redisCaptcha.equals(register.getPhoneCaptcha())) { throw new RuntimeException("验证码错误!请重新输入!"); } //调取admin注册接口 //注册KC Boolean keycloakUser = keycloakAdminUtil.createKeycloakUser(register, KeycloakRegisterUserTypeEnum.LEARNER.getType()); if (keycloakUser) { //注册成功删除验证码 redis.deleteByKey(key); } return keycloakUser; } catch (RuntimeException e) { e.printStackTrace(); throw new RuntimeException(e.getMessage()); } }
前端把手机号和验证码、密码等信息传进来之后,先去做校验,然后再调取keycloak createUser方法去创建keycloak用户。
在这里用到了Redis,Redis是一个开源的键值对(Key-Value)存储系统,它支持网络、可基于内存、分布式、可选持久性的数据库,并提供多种语言的API。以下是关于Redis的一些详细介绍:
注册完成之后,需要登录获取token,然后前端拿到这个token来请求后端接口。登录的示例代码如下:
@Override public KeycloakTokenResponse doLogin(Login login) { Object redisCaptchaObj = redis.get(login.getCaptchaKey()); if (ObjectUtil.isEmpty(redisCaptchaObj)) { throw new RuntimeException("验证码已过期!"); } String redisCaptcha = String.valueOf(redisCaptchaObj); if (!redisCaptcha.equalsIgnoreCase(login.getCaptcha())) { throw new RuntimeException("验证码错误,请重试!"); } KeycloakTokenResponse response = token.getTokenByPassword(login.getUsername(), login.getPassword()); if (StrUtil.isNotEmpty(response.getAccessToken())) { userService.saveLoginLog(login.getUsername()); } return response; }
这里获取了登录信息,根据用户输入的密码调取keycloak api接口,得到token response信息,返回到前端之后,就可以通过accessToken访问接口了。
前面在yml文件配置了接口鉴权,/api/v1/* 表示/api/v1下的所有接口都需要鉴权,如下图所示:
下面是不需要token鉴权的接口:
前端部分都是简易的代码,提供一个思路,仅供参考。
。
<template> <div> <h2>注册</h2> <form @submit.prevent="register"> <div> <label for="phone">手机号:</label> <input type="text" id="phone" v-model="phone" /> </div> <div> <label for="code">验证码:</label> <input type="text" id="code" v-model="code" /> <button type="button" @click="sendCode">发送验证码</button> </div> <div> <label for="password">密码:</label> <input type="password" id="password" v-model="password" /> </div> <button type="submit">注册</button> </form> </div> </template> <script> export default { data() { return { phone: '', code: '', password: '', }; }, methods: { async register() { try { const response = await fetch('你的后端接口地址', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ phone: this.phone, code: this.code, password: this.password }), }); const data = await response.json(); if (data.success) { alert('注册成功'); } else { alert('注册失败:' + data.message); } } catch (error) { console.error('注册失败:', error); } }, async sendCode() { try { const response = await fetch('你的发送验证码接口地址', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ phone: this.phone }), }); const data = await response.json(); if (data.success) { alert('验证码已发送'); } else { alert('发送验证码失败:' + data.message); } } catch (error) { console.error('发送验证码失败:', error); } }, }, }; </script>
<template> <div> <h2>登录</h2> <form @submit.prevent="login"> <div> <label for="phone">手机号:</label> <input type="text" id="phone" v-model="phone" /> </div> <div> <label for="password">密码:</label> <input type="password" id="password" v-model="password" /> </div> <button type="submit">登录</button> </form> </div> </template> <script> export default { data() { return { phone: '', password: '', token: '', }; }, methods: { async login() { try { const response = await fetch('你的后端接口地址', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ phone: this.phone, password: this.password }), }); const data = await response.json(); if (data.success) { this.token = data.token; console.log('Token:', this.token); alert('登录成功'); } else { alert('登录失败:' + data.message); } } catch (error) { console.error('登录失败:', error); } }, }, }; </script>
登录完成之后,可以把token存储到local或者cookie里面,随便怎么玩,在请求接口的时候带上token即可!
这样就实现了一个通过keycloak鉴权的简单前后端分离项目,当springboot版本太高(3.0)的话,也可以集成spring-security来进行操作,不过得手动添加一些config才能正常使用,这个会在后面再出一篇博客来演示springbootV3+keycloak的集成。
长路漫漫,代码作伴!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。