当前位置: 首页 > 工具软件 > Redis-Session > 使用案例 >

【Redis实现的Session共享】

东郭翰音
2023-12-01

Redis的Session共享方式实现登录注册操作

1. 手机验证码注册功能

1.1 Controller 层代码

@Autowried
private UserService userService;

@PostMapping("/code")
public Result sendCode(String phone) {
	// 发送短信验证码并保存验证码
	return userService.sendCode(phone);   // 这里的主要业务我们放在实现类去
}

1.2 业务层代码

// 首先我们注入Spring提供的Redis
@Autowired
private StringRedisTemplate stringRedisTemplate;  // 默认的key和value都是String类型

// 发送验证码的操作
@Override
public Result sendCode(String phone) {
	// 首先校验手机号
	if(RegexUtils.isPhoneInvalid(phone)) {  // 这里其实一堆的正则表达式的工具类,来验证号码的合法性
		// 手机号码不合法,返回错误信息
		return Result.fail("手机号格式错误");
	}
	// 符合,生成验证码
	String code = RandomUtil.randomNumbers(6);  // 这里使用的是hutool提供的工具类,它里面封装了很多实用的工具类,比如生成随机数,对象转换,对象转hash等等(墙裂推荐)
	// 将生成的验证码保存在redis里面,并且设置默认到期时间为3分钟 (用电话号码作为键,保证唯一性)
	// 这里的这些键可以封装成常量类,使代码看起来更优雅,但这里为了直观我直接写死了,后面会提供常量类
	stringRedisTemplate.opsForValue().set("login:code:" + phone,code,3,TimeUnit.MINUTES);
	// 这里直接模拟发送短信验证码了,直接控制台看输出,不是这里的重点
	log.debug("发送短信验证码成功:验证码:{}" + code);
	// 最终返回 ok
	return Result.ok();
}

2. 登录功能

2.1 Controller 层代码

// loginForm:登录需要的参数,包含手机号验证码或者是手机号和密码的组合
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm) {
	return userService.login(loginForm);
}

2.2 业务层代码

// 实现登录功能
public Result login(LoginFormDTO loginForm) {
	// 从请求体中获取手机号
	String phone = loginForm.getPhone();
	// 校验手机号
	if(RegexUtils.isPhoneInvalid(phone)) {
		// 不符合,返回错误信息
		return Result.fail("手机号格式错误");
	}
	// 从redis中获取验证码,根据我们前面传入的key
	String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
	// 再获取用户传过来的验证码
	String code = loginForm.getCode();
	if(cacheCode == null || !cacheCode.equals(code)) {
		// 返回错误信息
		return Result.fail("验证码错误");
	}
	// 根据手机号查询用户
	User user = query.eq("phone",phone).one();
	// 判断用户是否存在
	if(user == null) {
		// 不存在,创建新用户并保存,createUserWithPhone方法在后面定义
		user.createUserWithPhone(phone);
	}
	// 随机生成token,作为登录令牌传给前端,让前端每次请求都带着这个token
	// 因为每次都会带着这个token,这里我们不再使用手机号作为key,防止用户敏感信息泄露
	String token = UUID.randomUUID().toString(true);    // 这里我使用uuid生成随机字符串
	// 将user对象转为hashMap存储在redis里面
	UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);  // 这里也是防止敏感信息泄露,将User对象先转换成UserDTO对象,UserDTO对象里面只封装了一些最基本的信息,比如用户昵称,用户头像
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldVaue) -> fieldVaue.toString())); // 将所有值都转换为String类型

	// 存储在redis
	stringRedisTemplate.opsForHash().putAll("login:token:" + token,userMap);
	// 仿照session,设置30分钟有效时间,超过30分钟没有进行任何操作,清除数据(涉及token的续期,我会在后面的拦截器里面实现)
	stringRedisTemplate.expire("lgoin:token:" + token,30,TimeUnit.MINUTES);
	// 返回token
	return Result.ok(token);
}

// 保存用户的方法 => createUserWithPhone
private User createUserWithPhone(String phone) {
	// 创建用户
	User user = new User();
	user.setPhone(phone);
	user.setNickName("user_" + RandomUtil.randomNumbers(5));  // 为用户默认随机生成用户名,我采用的hutool的生成随机数
	// 保存用户
	save(user);   // 这都是mp里面自带的方法,不多说了
	return user;
}

3. 自定义拦截器配置

3.1 刷新token的拦截器

// 刷新token的拦截器:拦截所有的请求,只要用户发了一次请求,就会刷新一次token的过期时间,并对所有请求放行
public class RefreshTokenInterceptor implements HandlerInterceptor {
	// 这里不能使用注解注入,因为这是我们自定义的拦截器,并没有交给Spring管理,这里采用构造方法的方式来进行依赖注入
	private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
       this.stringRedisTemplate = stringRedisTemplate;
  }
  // 进入controller之前拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		 // 获取请求头中的token,封装在authorization的
		 String token = request.getHeader("authorization");
		 if(!StrUtil.isBlank(token)) {
		 	return true;
		 }
		 // 基于token获取redis中的用户
		 Map<Object,Object> userMap = stringRedisTemplate.opsForValue().entries("login:token:" + token); // entries()方法会自动判断是否为null,因此下面就不用判断是否为null了
		 // 判断用户是否存在 
		 if(userMap.isEmpty()) {
		 	return true;
		 }
		 // 将查询到的hash数据转为UserDTO对象,依旧使用的hutool提供的工具类
		 UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),fasle);  // false表示不忽略错误
		 // 存在,保存用户信息到ThreadLocal,ThreadLocal是线程私有的,相对于其他线程隔离
		 UserHolder.saveUser(userDTO);     // UserHolder也是封装的操作ThreadLocal的工具类,后文会提供
		 // 刷新token的有效期 -> 30 min
		 stringRedisTemplate.expire("login:token:" + token,30,TimeUnit.MINUTES);
		 // 放行
		 return true;
	}
	// 之后
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

3.2 登录拦截器

// 登录拦截器:拦截部分请求,并且在token拦截器之后执行,判断ThreadLocal里面是否有值来决定是否放行
public class LoginInterceptor implements HandlerInterceptor {
	// 进入controller之前进行拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    	// 判断是否需要拦截 (ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截
            response.setStatus(401);
            // 拦截
            return false;
        }
        // 有用户,放行
        return true;
    }
}

此时虽然已经定义好了拦截器,但是还不会生效,不要忘了去注册我们定义好的拦截器

3.3 注册拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 添加我们自定义的拦截器 => LoginInterceptor
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        // 刷新token拦截器,拦截所有请求,优先级更高
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0); // order的意义就是设置拦截器执行顺序的优先级,默认都是0,会按照你定义顺序执行,这里只是为了更明确才设置了值,可以省略。
        
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                ).order(1);    // 排除不需要拦截的路径,优先级更低

    }

}

4. 工具类和常量类

这部分代码只是为了让代码更加简洁优雅,根据自己的需求选择叭

4.1 hutool

首先是前面出现次数极多的hutool工具类,直接导入maven坐标就可以使用了

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.17</version>
        </dependency>

4.2 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();
    }
}

4.3 正则工具类

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}$";
}

4.4 验证工具类

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);
    }
}

4.5 Redis常量工具类

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 3L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}

4.6 Redis的配置

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:
    lettuce:        # 默认就是lettuce的连接池
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 1
        time-between-eviction-runs: 10s
 类似资料: