在前面两节分别介绍了 Keycloak的下载与使用和keycloak与springboot的集成。
接下来第三节让我们一步步的去完成一个简单的前后端分离项目,并且可以扩展实现sso。
一、简介
本文将介绍如何使用Spring Boot、Keycloak和Vue构建一个具有前后端分离架构的Web应用程序。通过将前端与后端完全独立开发和部署,我们可以实现更高效的团队协作和灵活的技术选型。Spring Boot提供了一个稳定可靠的后台框架,Keycloak提供了身份验证和授权的解决方案,而Vue作为一种灵活易用的前端框架,使我们能够快速开发出优秀的用户界面。
二、keycloak配置
首先回顾一下上一节提到的访问类型:
public: 适用于客户端应用,如前端web系统,包括采用vue、react实现的前端项目等。不需要秘钥访问。
confidential: 适用于服务端应用,比如需要浏览器登录以及需要通过密钥获取access token的web系统。需要秘钥访问。
bearer-only: 适用于服务端应用,只允许使用bearer token请接口,项目里的权限是针对接口做校验,请求没有带上token就会返回401。需要秘钥访问。
在这里我们新创一个keycloak客户端,因为我们的项目是前后端分离的,所以此客户端的访问类型为 bearer-only。
三、springboot配置
创建好一个新的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的相关接口去创建、更新、删除用户的信息。
废话不多说,直接上代码。
4.1 后端接口的实现
在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);
}
在实现类里面编写对应的方法,示例代码如下:
4.1.1 注册
@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的一些详细介绍:
- 设计目的:Redis被设计为一个高性能的非关系型数据库,主要用于处理大量数据的高速读写操作。它的数据存储在内存中,这使得其访问速度非常快,每秒可以处理超过10万次的读写操作。
- 数据结构:Redis不仅可以存储简单的字符串,还支持多种复杂的数据类型,如列表(list)、集合(set)、有序集合(sorted sets)和哈希(hash)。这些数据结构使得Redis可以满足更多应用场景的需求。
- 应用场景:Redis通常用于缓存系统,以减轻后端数据库的压力。例如,在Web应用中,可以将频繁访问的数据缓存在Redis中,从而提高读取速度和系统的响应能力。此外,Redis也常用于实现分布式锁,以保证在分布式环境中的数据一致性。
- 性能特点:由于Redis的数据存储在内存中,其读写速度远超传统的基于磁盘的数据库。这使得Redis非常适合需要快速响应的应用,如实时分析、消息队列等。
- 持久化机制:虽然Redis是基于内存的,但它提供了持久化机制,可以将内存中的数据定期保存到磁盘中,以防止数据丢失。Redis支持RDB和AOF两种持久化方式。
- 支持事务:Redis支持简单的事务功能,可以确保一系列命令的原子性执行。这对于需要保证操作完整性的应用来说非常重要。
- 集群支持:Redis支持多种集群方案,可以通过分片(sharding)等方式实现数据的分布式存储,提高系统的可扩展性和容错能力。
- 社区和生态:作为一个开源项目,Redis拥有一个活跃的社区,提供了大量的文档和工具,方便开发者使用和维护。同时,许多编程语言都提供了与Redis交互的库,使得集成Redis变得简单便捷。
4.1.2 登录
注册完成之后,需要登录获取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鉴权的接口:
4.2 前端代码
前端部分都是简易的代码,提供一个思路,仅供参考。
4.2.1 注册
。
<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>
4.2.2登录
<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的集成。
长路漫漫,代码作伴!