Redis之短信登录

基于 Session 实现


发送验证码

/**
 * 发送手机验证码
 */
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // 发送短信验证码并保存验证码
    return userService.sendCode(phone, session);

@Override
public Result sendCode(String phone, HttpSession session) {
	// 校验手机号
	if(RegexUtils.isPhoneInvalid(phone)){
		// 不符合 返回错误信息
		return Result.fail("手机号格式不正确");
	}
	// 符合 生成验证码
	String code = RandomUtil.randomNumbers(6);
	// 保存验证码到 session
	session.setAttribute("code", code);
	// 发送验证码
	log.debug("发送验证码: {} 到手机: {}", code, phone);
	// 返回成功信息
	return Result.ok();
}
  1. 发送验证码之前校验手机号是否符合规则
  2. 不符合校验规则就返回错误信息
  3. 符合校验规则就生成验证码并保存到session中
  4. RandomUtil.randomNumbers(6)使用hutool工具生成6位随机验证码
  5. 最后发送验证码并返回成功提示

image.png

登录校验验证码

/**
 * 登录功能
 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
 */
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm, session);
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
	// 校验手机号
	if(RegexUtils.isPhoneInvalid(loginForm.getPhone())){
		// 不符合 返回错误信息
		return Result.fail("手机号格式不正确");
	}
	// 校验验证码
	Object cacheCode = session.getAttribute("code");
	if(cacheCode == null || !cacheCode.toString().equals(loginForm.getCode())){
		// 验证码不一致
		return Result.fail("验证码不正确");
	}
	// 验证码一致 根据手机号查询用户信息
	User user = lambdaQuery().eq(User::getPhone, loginForm.getPhone()).one();
	// 判断用户是否存在
	if(user == null){
		// 不存在 创建新用户并保存
		User registerUser = new User();
		registerUser.setPhone(loginForm.getPhone());
		registerUser.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(8));
		save(registerUser);
		session.setAttribute("user", registerUser);
	}
	if(user != null){
		session.setAttribute("user", user);
	}
	return Result.ok();
}
  1. 校验手机号是否符合规则
  2. 用户输入的验证码与session中保存的验证码进行比对
  3. 不一致则返回验证码错误的提示信息
  4. 验证码一致则拿用户输入的手机号进行查库看用户是否存在
  5. 存在则将查到的用户信息存入session中, 以备后续使用
  6. 不存在则用输入的手机号自动注册一个新用户存库并将信息保存session

登录拦截器

public class LoginInterceptor implements HandlerInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 获取session
		HttpSession session = request.getSession();
		// 获取session中的用户
		Object user = session.getAttribute("user");
		// 判断用户是否存在
		if(user == null){
			// 不存在 401拦截
			response.setStatus(401);
			return false;
		}
		// 存在 保存用户到ThreadLocal
		UserHolder.saveUser((UserDTO) user);
		// 放行
		return true;
	}
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
		// 移除用户
		UserHolder.removeUser();
	}
}
  1. 从请求中拿到session, 再从session中拿到user信息
  2. 如果user信息为空则返回401响应码并拦截登录请求
  3. 如果存在则将用户信息存入线程ThreadLocal线程中, 以备后续使用
  4. 最后放行

注册拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 添加拦截器
		registry.addInterceptor(new LoginInterceptor())
				// 放行请求
				.excludePathPatterns(
						"/shop/**",
						"/voucher/**",
						"/shop-type/**",
						"/upload/**",
						"/blog/hot",
						"/user/code",
						"/user/login"
				);
	}
}

基于 Redis 实现


发送验证码

/**
 * 发送手机验证码
 */
@PostMapping("code")
public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
    // 发送短信验证码并保存验证码
    return userService.sendCode(phone, session);

@Override
public Result sendCode(String phone, HttpSession session) {
	// 校验手机号
	if(RegexUtils.isPhoneInvalid(phone)){
		// 不符合 返回错误信息
		return Result.fail("手机号格式不正确");
	}
    
	// 符合 生成验证码
	String code = RandomUtil.randomNumbers(6);
    
	// 保存验证码到redis
	stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
	
    // 发送验证码
	log.debug("发送验证码: {} 到手机: {}", code, phone);
    
	// 返回成功信息
	return Result.ok();
}
  1. 对比上面的代码就是将保存验证码到session变成保存到redis中

登录校验

/**
 * 登录功能
 * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
 */
@PostMapping("/login")
public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
    // 实现登录功能
    return userService.login(loginForm, session);
}

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
	String phone = loginForm.getPhone();
	String code = loginForm.getCode();
	// 校验手机号
	if(RegexUtils.isPhoneInvalid(phone)){
		// 不符合 返回错误信息
		return Result.fail("手机号格式不正确");
	}
	// 从redis获取验证码并校验
	String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
	if(cacheCode == null || !cacheCode.equals(code)){
		// 验证码不一致
		return Result.fail("验证码不正确");
	}
	// 验证码一致 根据手机号查询用户信息
	User user = lambdaQuery().eq(User::getPhone, phone).one();
	// 判断用户是否存在
	if(user == null){
		// 不存在 创建新用户并保存
		user = createUser(phone);
	}
	// 存在 保存用户到redis
	// 随机生成token 作为登录令牌
	String token = UUID.randomUUID().toString(true);
	// 将User对象转为HashMap存储
	UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
	Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
		CopyOptions.create()
				.setIgnoreNullValue(true)
				.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
	// 存储
	String tokenKey = LOGIN_USER_KEY + token;
	stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
	// 设置token有效期
	stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
	return Result.ok();
}

private User createUser(String phone) {
	User registerUser = new User();
	registerUser.setPhone(phone);
	registerUser.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
	save(registerUser);
	return registerUser;
}
  1. 从redis中拿到验证码进行比对
  2. String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone)
  3. UUID生成登录令牌token, String token = UUID.randomUUID().toString(true)
  4. 将User对象转为HashMap存储
Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
		CopyOptions.create()
				.setIgnoreNullValue(true)
				.setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()))
  • 第一个参数是要转换的对象,即 userDTO
  • 第二个参数是目标 Map 对象,这里传入了一个新的空 HashMap
  • CopyOptions.create() 创建了一个复制选项对象,用于配置复制行为。
  • setIgnoreNullValue(true) 表示忽略源对象中值为 null 的属性。
  • setFieldValueEditor() 设置一个字段值编辑器,使用一个简单的编辑器,将字段值转换为字符串。
  1. 在token前加固定标识, 便于理解, String tokenKey = LOGIN_USER_KEY + token
  2. 将用户信息存入redis, stringRedisTemplate.opsForHash().putAll(tokenKey, userMap)
  3. 设置令牌过期时间stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES)

登录拦截器

public class LoginInterceptor implements HandlerInterceptor {
	private StringRedisTemplate stringRedisTemplate;
	public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
		this.stringRedisTemplate = stringRedisTemplate;
	}
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 获取请求头中的token
		String token = request.getHeader("authorization");
		if(StrUtil.isBlank(token)){
			// 不存在 401拦截
			response.setStatus(401);
			return false;
		}
		String key = RedisConstants.LOGIN_USER_KEY + token;
		// 基于token获取redis中的用户
		Map<Object, Object> userMap = stringRedisTemplate.opsForHash()
				.entries(key);
		// 判断用户是否存在
		if(userMap.isEmpty()){
			// 不存在 401拦截
			response.setStatus(401);
			return false;
		}
		// 将查询到的Hash数据转换为UserDTO对象
		UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
		// 存在 保存用户到ThreadLocal
		UserHolder.saveUser(userDTO);
		// 刷新token有效期
		stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
		
		// 放行
		return true;
	}
  1. 在自定义的拦截器里想使用StringRedisTemplate模板类需要先定义然后用构造函数调用
private StringRedisTemplate stringRedisTemplate;
	public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
		this.stringRedisTemplate = stringRedisTemplate;
	}
  1. 从请求头里拿到token
  • String token = request.getHeader("authorization")
  1. 从redis拿到token对应的user用户信息, entries拿到全部属性
  • Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key)
  1. 将拿到的userMap转换成userDTO, 再存到线程ThreadLocal中, fillBeanWithMap
  • UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false)
  1. 刷新token的有效期
  • stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES)
  1. 思路: request -> token -> userMap -> userDTO -> ThreadLocal -> token.expire

登录拦截器优化

image.png

  1. 第一个拦截器拦截所有路径, 从请求中拿到token, 用token拿到redis中的用户信息存入线程中
  2. 第二个拦截器拦截需要登录的路径, 判断线程中是否存在用户信息, 不存在则判定为该用户未登录, 拦截!

原来的登录拦截器改名为token刷新拦截器即可, 添加一个登录拦截器就行哈哈哈~

public class LoginInterceptor implements HandlerInterceptor {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
		// 判断是否需要拦截 ( ThreadLocal中是否存在用户信息 )
		if(UserHolder.getUser() == null){
			// 不存在 401拦截
			response.setStatus(401);
			return false;
		}
		// 存在 放行
		return true;
	}
}

MvcConfig里注册一下这两个拦截器, 给个执行顺序order

@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void addInterceptors(InterceptorRegistry registry) {
	// token 刷新拦截器
	registry.addInterceptor(new RefreshTokenlnterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
	// 登录拦截器
	registry.addInterceptor(new LoginInterceptor())
			// 放行请求
			.excludePathPatterns(
					"/shop/**",
					"/voucher/**",
					"/shop-type/**",
					"/upload/**",
					"/blog/hot",
					"/user/code",
					"/user/login"
			).order(1);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值