redis指令参考:https://blog.csdn.net/weixin_43994244/article/details/127489311
redis安装:https://blog.csdn.net/weixin_43994244/article/details/123111671?spm=1001.2014.3001.5502
阿里云短信认证参考:https://blog.csdn.net/weixin_46931860/article/details/124512435
1.session共享问题:
多台Tomcat并不共享session存储空间,当请求切换到不同Tomcat服务时导致数据丢失的问题
2.session拷贝问题:
1、每台服务器中都有完整的一份session数据,服务器压力过大
2、session拷贝数据时,可能会出现延迟
3.Redis代替session需要考虑的问题:
1、选择合适的数据结构
2、选择合适的key
3、选择合适的存储粒度
4.Redis的key满足条件:
1、key要具有唯一性
2、key要方便携带
1.用户提交手机号,校验手机号是否合法。
2.如果不合法,则要求用户重新输入手机号,如果手机号合法,后台此时生成对应的验证码
3.保存验证码到redis中,手机号作为key(只在缓存中使用,数据结构简单)
4.通过短信的方式将验证码发送给用户
@Resource
private StringRedisTemplate stringRedisTemplate;
//发送短信验证码并保存验证码
@Override
public Result sendCode(String phone) throws ClientException {
//校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//不符合返回错误信息
return Result.fail("手机号格式错误!");
}
//符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//保存验证码到redis中,设置有效时间
stringRedisTemplate.opsForValue().set("login:code:"+phone,code,5,TimeUnit.MINUTES);
//发送短信验证码方法
log.debug("发送验证码成功,验证码: {}",code);
return Result.ok();
}
1.用户输入验证码和手机号,后台从redis中根据key拿到当前验证码
2.和用户输入的验证码进行校验,不一致,则无法通过校验;一致,则后台根据手机号查询用户
3.如果用户不存在,则为用户创建账号信息,保存到数据库
4.将用户信息保存到redis中,随机串token作为key(返回给前端,不能存储敏感信息),方便后续获得当前登录信息
/**
* 登录参数,包含手机号、验证码;或者手机号、密码
* @return
*/
@Override
public Result login(LoginFormDTO loginForm) {
String phone = loginForm.getPhone();
//1.校验手机号
if(RegexUtils.isPhoneInvalid(phone)){
//不符合返回错误信息
return Result.fail("手机号格式错误!");
}
//2.校验验证码
String cacheCode = stringRedisTemplate.opsForValue().get("login:token:" + phone);
String code = loginForm.getCode();
if(cacheCode == null || !cacheCode.equals(code)) {
//3.不一致
return Result.fail("验证码错误");
}
//4.根据手机号查询用户
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if(user == null){
//6.不存在,创建新用户并保存
user = createUserWithPhone(phone);
save(user);
}
//7.保存用户信息到redis中
//7.1 随机生成token,作为登录令牌
String token = UUID.randomUUID().toString(true);
//7.2 将User对象转为UserDto作为Hash存储
UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
//对象转map,将long类型转为String
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
//对key和value自定义-忽略空值-对字段值修改(参数:字段名,字段值)
CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue)-> fieldValue.toString()));
stringRedisTemplate.opsForHash().putAll("login:token:"+token,map);
//设置有效期
stringRedisTemplate.expire("login:token:",30,TimeUnit.MINUTES);
//8 返回到token
return Result.ok(token);
}
相关类数据:
User:
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_user")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 手机号码
*/
private String phone;
/**
* 密码,加密存储
*/
private String password;
/**
* 昵称,默认是随机字符
*/
private String nickName;
/**
* 用户头像
*/
private String icon = "";
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
LoginFormDTO:
@Data
public class LoginFormDTO {
private String phone;
private String code;
private String password;
}
UserDTO:
@Data
@ToString
public class UserDTO {
private Long id;
private String nickName;
private String icon;
}
登录信息校验正则表达式:
public abstract class RegexPatterns {
/**
* 手机号正则
*/
public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
/**
* 邮箱正则
*/
public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
/**
* 密码正则。4~32位的字母、数字、下划线
*/
public static final String PASSWORD_REGEX = "^\\w{4,32}$";
/**
* 验证码正则, 6位数字或字母
*/
public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
}
校验工具类:
public class RegexUtils {
/**
* 是否是无效手机格式
* @param phone 要校验的手机号
* @return true:符合,false:不符合
*/
public static boolean isPhoneInvalid(String phone){
return mismatch(phone, RegexPatterns.PHONE_REGEX);
}
/**
* 是否是无效邮箱格式
* @param email 要校验的邮箱
* @return true:符合,false:不符合
*/
public static boolean isEmailInvalid(String email){
return mismatch(email, RegexPatterns.EMAIL_REGEX);
}
/**
* 是否是无效验证码格式
* @param code 要校验的验证码
* @return true:符合,false:不符合
*/
public static boolean isCodeInvalid(String code){
return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
}
// 校验是否不符合正则格式
private static boolean mismatch(String str, String regex){
if (StrUtil.isBlank(str)) {
return true;
}
return !str.matches(regex);
}
}
用户在请求时候,会从携带token到后台,后台通过token从redis中拿到用户信息,如果没有用户信息,则进行拦截,如果有用户信息,则将用户信息保存到threadLocal中,并且放行。
备注:threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离。
1.编写拦截器
public class LoginInterceptor implements HandlerInterceptor {
private StringRedisTemplate stringRedisTemplate;
public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的token
String token = request.getHeader("authorization");
if (StrUtil.isBlank(token)) {
return true;
}
// 2.基于TOKEN获取redis中的用户
String key = LOGIN_USER_KEY + token;
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
// 3.判断用户是否存在
if (userMap.isEmpty()) {
return true;
}
// 5.将查询到的hash数据转为UserDTO
UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
// 6.存在,保存用户信息到 ThreadLocal
UserHolder.saveUser(userDTO);
// 7.刷新token有效期
stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
// 8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户
UserHolder.removeUser();
}
}
2.配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
//添加登录拦截器,设置拦截路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
//登录拦截器
registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
//配置拦截路径
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login");
}
}
3.ThreadLocal工具类
public class UserHolder {
private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();
public static void saveUser(UserDTO user){
tl.set(user);
}
public static UserDTO getUser(){
return tl.get();
}
public static void removeUser(){
tl.remove();
}
}
问题:上述拦截器只是拦截需要被拦截的路径,假设当前用户访问了一些不需要拦截的路径,此拦截器就不会生效,所以此时令牌刷新的动作实际上就不会执行,token仍会过期
解决方案:添加一个拦截器,在第一个拦截器中拦截所有的路径,把第二个拦截器做的事情放入到第一个拦截器中,同时刷新令牌,因为第一个拦截器有了threadLocal的数据,所以此时第二个拦截器只需要判断拦截器中的user对象是否存在即可,完成整体刷新功能。
RefreshTokenInterceptor
/**
* 拦截一切路径,刷新token时间
*/
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) throws Exception {
String token = request.getHeader("authorization");
//放行
if(StrUtil.isBlank(token)){
return true;
}
//2.基于token获取redis中的用户
Map<Object, Object> map = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
//3.判断用户是否存在
if(map.isEmpty()){
//4.不存在,拦截,返回401状态码
return true;
}
//5.将查询到的数据转为userDto,保存到ThreadLocal中
UserDTO userDTO = BeanUtil.fillBeanWithMap(map, new UserDTO(), false);
UserHolder.saveUser(userDTO);
//7.刷新token有效期
stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,30, TimeUnit.MINUTES);
//8.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
LoginInterceptor
/**
* 拦截需要登录的路径
*/
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用户访问一直不需要登录的页面,拦截器不生效,不刷新token有效期
//则添加一个 RefreshTokenInterceptor拦截一切路径,刷新token时间
if(UserHolder.getUser() == null){
//没有,需要拦截
response.setStatus(401);
return false;
}
//有用户,则放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserHolder.removeUser();
}
}
拦截器配置
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Resource
private StringRedisTemplate stringRedisTemplate;
//添加登录拦截器,设置拦截路径
@Override
public void addInterceptors(InterceptorRegistry registry) {
//默认顺序为添加顺序
//token刷新拦截器,拦截所有请求,先执行
registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
//登录拦截器
registry.addInterceptor(new LoginInterceptor())
//配置拦截路径
.excludePathPatterns(
"/shop/**",
"/voucher/**",
"/shop-type/**",
"/upload/**",
"/blog/hot",
"/user/code",
"/user/login");
}
}