redis-实战篇-短信登录

发布时间 2023-09-11 09:12:00作者: 梅落南山

黑马点评,前后端分离的架构模式。前端分布在nginx上,后端分布在tomcat。

短信登陆

导入黑马点评项目:

打开项目,重新设置maven仓库位置。更新src/main/resources下的application.yaml中的数据库配置和redis配置。
后端部署:
点击services添加springboot,启动HmDianPingApplication,如下图所示。
Pasted image 20230905102251.png
前端部署:
打开nginx:在nginx目录下打开cmd窗口,输入命令start nginx.exe。然后打开谷歌浏览器,F12打开开发者工具,最后点击左上角打开手机模式。
访问http://127.0.0.1:8080,即可看到页面。

基于Session实现登录

1653066208144.png
验证码发送功能:
Pasted image 20230905105957.png
可以知道发送验证码的请求为POST,接口为user/code,参数为phone。找到UserController中的sendCode函数,返回userService.sendCode(phone,session),在userService中实现验证发发送功能。

@Override  
public Result sendCode(String phone, HttpSession session) {  
	//校验手机号  
	//RegexUtils提供了正则表达校验,直接调用即可。  
	if (RegexUtils.isPhoneInvalid(phone)) {  
		return Result.fail("手机号格式错误!");  
}  
	//随机生成6位验证码  
	String code= RandomUtil.randomNumbers(6);  
	//保存验证码到session  
	session.setAttribute("code",code);  
	//发送验证码,现实中需要调用API接口发送验证码,此处只将验证码写入日志中模拟发送  
	log.debug("发送验证码{}成功",code);  
	//直接调用Result中的ok对象进行成功返回
	return Result.ok();  
}

登录功能:
在UserService的login()中实现。补充:tb_user表中手机号为主键,所以根据手机号进行用户查找。

@Override  
public Result login(LoginFormDTO loginForm, HttpSession session) {  
	//再次校验手机号  
	String phone=loginForm.getPhone();  
	if (RegexUtils.isPhoneInvalid(phone)) {  
		return Result.fail("手机号格式错误!");  
	}  
	//校验验证码  
	Object cacheCode = session.getAttribute("code");  
	String code=loginForm.getCode();  
	if (cacheCode==null||!cacheCode.toString().equals(code)) {  
		return Result.fail("验证码错误");  
	}  
	//根据手机号查询用户,判断用户是否存在。使用MyBatis Plus写SQL语句  
	User user = query().eq("phone", phone).one();  
	//如果不存在,则创建新用户并保存到数据库  
	if (user==null) {  
		user=createUserWithPhone(phone);  
	}  
	//保存用户信息到session中  
	session.setAttribute("user",user);  
	return Result.ok();  
}  
  
private User createUserWithPhone(String phone) {  
	//创建用户对象  
	User user=new User();  
	user.setPhone(phone);  
	//USER_NICK_NAME_PREFIX为提前写好的常量信息,内容为"user_"  
	user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));  
	//写入数据库。在IService中提前写好了save函数。
	save(user);  
	return user;  
}

登录验证功能:
1653068196656.png
Tomcat工作原理:
当用户发起请求时,会访问我们向tomcat注册的端口,任何程序想要运行,都需要有一个线程对当前端口号进行监听,tomcat也不例外,当监听线程知道用户想要和tomcat连接连接时,那会由监听线程创建socket连接,socket都是成对出现的,用户通过socket像互相传递数据,当tomcat端的socket接受到数据后,此时监听线程会从tomcat的线程池中取出一个线程执行用户请求,在我们的服务部署到tomcat后,线程会找到用户想要访问的工程,然后用这个线程转发到工程中的controller,service,dao中,并且访问对应的DB,在用户执行完请求后,再统一返回,再找到tomcat端的socket,再将数据写回到用户端的socket,完成请求和响应
通过以上讲解,我们可以得知 每个用户其实对应都是去找tomcat线程池中的一个线程来完成工作的, 使用完成后再进行回收,既然每个请求都是独立的,所以在每个用户去访问我们的工程时,我们可以使用threadlocal来做到线程隔离,每个线程操作自己的一份数据
threadlocal:
如果小伙伴们看过threadLocal的源码,你会发现在threadLocal中,无论是他的put方法和他的get方法, 都是先从获得当前用户的线程,然后从线程中取出线程的成员变量map,只要线程不一样,map就不一样,所以可以通过这种方式来做到线程隔离
所以在拦截器中实现校验登录状态功能。
敏感信息隐藏问题:
在这个过程中也要注意返回用户信息之前,将用户的敏感信息进行隐藏:书写一个UserDto对象,在返回前将User对象转化成没有敏感信息的UserDto对象。
session共享问题:
多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时会导致数据丢失问题。
为了满足session共享,则需要满足数据共享,内存存储(低延迟),key-value存储,因此使用redis存储session。

基于Redis实现session登录:

redis的key要满足两点:唯一性和方便携带。因此使用一个随机字符串token作为key。如果采用phone,则将敏感信息存储到redis中,会有泄露的风险,不妥。
使用redis时需要设置有效值,否则数据越来越大会占用过多内存。
因此整体思路是:将验证码code存入login:user+phone中。然后在登录时,通过手机号获取验证码,判断验证码是否一致。如果一致,则根据手机号查询用户信息,不存在用户则新建一个用户,最后将用户数据保存在redis,并随机生成字符串token作为此值的key,也就是session的凭证。当我们校验用户是否登录时,会携带token进行访问,从redis中去除token对应的value,判断是否存在这个数据,如果没有则拦截,如果存在则保存到threadLocal中,并刷新token的有效期,放行。如下图所示。
1653320822964.png
拦截器的添加需要两步:书写拦截器,设置拦截器起作用。
创建一个拦截器的文件:utils/loginInterceptor.java,然后创建设置拦截器的文件:config/MvcConfig

//loginInterceptor
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 {  
		// 获取session  
		// HttpSession session = request.getSession();  
		// Object user = session.getAttribute("user");  
		//获取请求头中的token  
		String token = request.getHeader("authorization");  
		if (StrUtil.isBlank(token)) {  
			response.setStatus(401);  
			return false;  
		}  
		//基于token获取redis中的用户  
		String key=RedisConstants.LOGIN_USER_KEY + token;  
		Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);  
		  
		//判断用户是否存在,如果不存在则进行拦截  
		if (userMap.isEmpty()){  
			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;  
	}  
	  
	@Override  
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {  
	HandlerInterceptor.super.afterCompletion(request, response, handler, ex);  
	//移除用户  
	UserHolder.removeUser();  
	}  
}
//MvcConfig
@Configuration  
public class MvcConfig implements WebMvcConfigurer {  
	@Resource  
	private StringRedisTemplate stringRedisTemplate;  
	@Override  
	public void addInterceptors(InterceptorRegistry registry) {  
		//除了下列链接其他都拦截  
		registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(  
		"/shop/**",  
		"/voucher/**",  
		"/shop-type/**",  
		"/upload/**",  
		"/blog/hot",  
		"/user/code",  
		"/user/login"  
		).order(1);  
	}  
}

但是如果用户访问的是不需要登录的界面比如首页,虽然用户非常活跃,但是过了有效期后token依旧失效了,这样用户的体验感并不好,因此需要进行优化。如下图所示,在拦截器外再加一个拦截器,第一个拦截器拦截所有的页面,第二个确保token的刷新。
1653320764547.png