前言
学习要善于做笔记,学完长时间不用,学会的知识又被遗忘了,本文是学习张老师redis课程记录,感兴趣的小伙伴可以去B站看原视频,本章节主要实现redis代替session实现登录功能
主要流程图
登录后,后端返回token给前端,前端拿到token后,将token存储到sessionStorage中,在前端添加拦截器,对所有后端请求添加请求头,将token携带到后端验证登录信息。
代码实现
pom文件
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.17</version>
</dependency>
<!--腾讯云短信服务-->
<dependency>
<groupId>com.github.qcloudsms</groupId>
<artifactId>qcloudsms</artifactId>
<version>1.0.6</version>
</dependency>
</dependencies>
发送短信验证码,并将验证码存储到redis中
public Result sendCode(String phone, HttpSession session) {
//校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
//生成验证码
String code = RandomUtil.randomNumbers(6);
//保存数据到redis
stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,
code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.MINUTES);
//发送验证码,APPID,APPKEY,TEMPLATEID,SMSSING这个四个值都是申请腾讯云控制台上获取,获取路径见下图
try {
String[] params = {code, Long.toString(RedisConstants.LOGIN_CODE_TTL)};
SmsSingleSender sender = new SmsSingleSender(RedisConstants.APPID, RedisConstants.APPKEY);
SmsSingleSenderResult result = sender.sendWithParam("86", phone, RedisConstants.TEMPLATEID, params,
RedisConstants.SMSSING, "", "");
} catch (HTTPException e) {
e.getStackTrace();
} catch (IOException e) {
e.getStackTrace();
}
log.debug("发送验证码成功,验证码:{}", code);
return Result.ok();
}
登录功能
用户信息存储到redis有效期设置的为30分钟。
public Result log(LoginFormDTO loginForm, HttpSession session) {
//获取手机号
String phone = loginForm.getPhone();
//校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
return Result.fail("手机号格式错误!");
}
//根据手机号查询用户信息
User user = query().eq("phone", phone).one();
//判断用户是否存在
if (user == null) {
//不存在,创建用户并保存
createUserWithphone(phone);
}
//生成随机的token,作为登录的令牌,不带‘-’
String token = UUID.randomUUID().toString(true);
//将User转换成hashmap存储,此处使用的是hutool自动转换工具
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
Map<String, Object> userMap1 = BeanUtil.beanToMap(userDTO);
//将User转换成hashmap存储,此处使用的是hutool自动转换换成map,
// setIgnoreNullValue(true)忽略对象中为空的值。
// setFieldValueEditor((a, b) -> b.toString()))将传入的userDTO对象中的值转换成String类型
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
CopyOptions.create().setIgnoreNullValue(true)
.setFieldValueEditor((a, b) -> b.toString()));
String tokenkey = RedisConstants.LOGIN_USER_KEY + token;
//将用户信息以token为键存入redis
stringRedisTemplate.opsForHash().putAll(tokenkey, userMap);
//设置token有效期为30分钟
stringRedisTemplate.expire(tokenkey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//返回token
return Result.ok(token);
}
第一个拦截器,拦截所有请求
package com.hmdp.utils;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @auther Kou
* @date 2022/6/11 23:02
*/
public class RefreshTokenInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
//获取请求头中的token
String token = request.getHeader("auth");
//如果token为空直接放行
if (StrUtil.isBlank(token)) {
return true;
}
String tokenkey = RedisConstants.LOGIN_CODE_KEY + token;
//根据token获取用户信息
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenkey);
//判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
//将map转换成userDto对象
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
//保存用户信息导ThreadLocal
UserHolder.saveUser(userDTO);
//刷新token有效期
stringRedisTemplate.expire(tokenkey, RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
//放行
return true;
}
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception e) {
//移除用户
UserHolder.removeUser();
}
}
第二个拦截器
package com.hmdp.utils;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @auther Kou
* @date 2022/6/11 23:19
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
//判断是否需要拦截(ThreadLocal中是否有用户)
if (UserHolder.getUser()==null){
response.setStatus(401);
//拦截
return false;
}
//放行
return true;
}
}
配置拦截的路径,及拦截器执行顺序
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
/**
* @auther Kou
* @date 2022/6/11 23:25
*/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
public void addInterceptors(InterceptorRegistry registry) {
//第二个登录拦截器
registry.addInterceptor(new LoginInterceptor())
.excludePathPatterns(
"/user/login",
"user/code",
"upload/**"
).order(1); // order 值越大执行的优先级越低
//第一个刷新token的拦截器,/** 拦截所有路径
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
}
}
该实现是通过拦截器来实现登录有效期的刷新,如使用的用户较固定,如公司内部使用系统入OA等,登录信息也可不设置有效期,每天晚上通过定时任务来清理redis中的登录信息,具体清理时间也可根据业务要求来。