我们这里使用Redis来实现JWT单点登录,不熟悉Redis的朋友也可以借此来了解一下Redis的基本使用。
在此之前,先解释一下什么是单点登录。最普通的那种基于Session+Cookie的实现方式,可以实现简单的登录,,它的使用场景就是单机
系统的情况下。假如我们的系统是分布式
系统,那么不同的系统下就得保存一份同样的登录状态数据,虽然也可以实现,但这无疑是浪费了很多空间。
基于Redis的单点登录,我们只需要在Redis中保存一份数据即可,不管是什么样系统,获取登录状态的时候,我们只需要去Redis中获取即可。
这里介绍后端的实现代码
Controller层
/**
* 用户登录
*
* @param userLoginRequest
* @param request
* @return
*/
@ExcludePath
@PostMapping("/login")
public BaseResponse<UserVO> userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request) {
UserVO userVO = userService.userLogin(userLoginRequest, request);
return ResultUtils.success(userVO, "登录成功");
}
Controller上面的
@ExcludePath
注解,是自定义的一个拦截器放行的注解,只需要在拦截器的定义中,判断方法上时候有这个注解,有的话就放行,这省去了我们在配置注册器中添加excludePath
,我觉得这样更简便一些。
Service层
@Override
@Transactional
public UserVO userLogin(UserLoginRequest userLoginRequest, HttpServletRequest request) {
if (userLoginRequest == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "缺少登录参数");
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
// 1. 校验
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "参数为空");
}
if (userAccount.length() < 4) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "账号错误");
}
if (userPassword.length() < 8) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "密码错误");
}
// 2. 加密
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
// 查询用户是否存在
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_account", userAccount);
queryWrapper.eq("user_password", encryptPassword);
User user = userMapper.selectOne(queryWrapper);
// 用户不存在
if (user == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "用户不存在或密码错误");
}
// 3. 记录用户的登录态
Gson gson = new Gson();
String userJson = gson.toJson(user);
// 4. 生产随机Token,作为登录凭证
String token = UUID.randomUUID().toString();
stringRedisTemplate.opsForValue().set(USER_TOKEN + token, userJson);
// 5. 为Token设置过期时间
stringRedisTemplate.expire(USER_TOKEN + token, 30L, TimeUnit.MINUTES);
// 6. 返回User视图数据(安全的数据)
UserVO userVO = new UserVO();
BeanUtils.copyProperties(user, userVO);
userVO.setToken(token);
// 7. 设置登录状态
userVO.setLogin(true);
return userVO;
}
真正的业务逻辑是在Service
层中。
不管做什么,我们都得对传递进来的参数进行校验,防止出现空指针异常,或者出现格式不正确的情况。即使前端有校验,我们后端也得做校验,更加的严谨。
如果我们校验不通过就直接抛出一个异常,这里的异常也是自定义的,我们这里对返回结果都统一了格式。
自定义异常类
/**
* 自定义异常类
*
* @author jl
*/
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}
public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}
public int getCode() {
return code;
}
}
自定义错误枚举类
/**
* 错误码
*
* @author jl
*/
public enum ErrorCode {
SUCCESS(0, "ok"),
PARAMS_ERROR(40000, "请求参数错误"),
NOT_LOGIN_ERROR(40100, "未登录"),
NO_AUTH_ERROR(40101, "无权限"),
NOT_FOUND_ERROR(40400, "请求数据不存在"),
FORBIDDEN_ERROR(40300, "禁止访问"),
SYSTEM_ERROR(50000, "系统内部异常"),
OPERATION_ERROR(50001, "操作失败");
/**
* 状态码
*/
private final int code;
/**
* 信息
*/
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
在抛出异常的时候同时写了相关的message
,这个参数前端可以直接显示出来,如下图:
参数校验完参数之后,下边我们说一下登录的思路:
第一步就是去数据库中查找,登录成功之后才是我们这里要说的重点,如何将这个登录状态保存。
首先随机生成一个UUID,作为登录凭证(可以类比于Seesion识别时候要用的JSESSIONID)。因为用的Redis,所以我们这里为了实现业务之间的区分,给这个Token加上一个业务前缀,过期时间设置为30分钟。
最后将这个用户的信息和这个Token登录信息一同返回,但要注意的是,返回用户信息不能将所有信息都返回(例如:密码),为了信息的安全,所以,这里定义了一个UserVO用户视图对象
,通过BeanUtils.copyProperties()
可以实现属性的拷贝(很好用)。
最后返回这个UserVo
即可。
以上就是Redis实现单点登录的基本思路,后面讲前端如何保存这个登录状态,以及Redis登录续期,登陆状态校验等…
如果文章中有描述不准确或者错误的地方,还望指正。您可以留言📫或者私信我。🙏
最后希望大家多多 关注+点赞+收藏^_^,你们的鼓励是我不断前进的动力!!!
感谢感谢~~~🙏🙏🙏