redis-实战篇-短信登录
黑马点评,前后端分离的架构模式。前端分布在nginx上,后端分布在tomcat。
短信登陆
导入黑马点评项目:
打开项目,重新设置maven仓库位置。更新src/main/resources下的application.yaml中的数据库配置和redis配置。
后端部署:
点击services添加springboot,启动HmDianPingApplication,如下图所示。
前端部署:
打开nginx:在nginx目录下打开cmd窗口,输入命令start nginx.exe
。然后打开谷歌浏览器,F12打开开发者工具,最后点击左上角打开手机模式。
访问http://127.0.0.1:8080,即可看到页面。
基于Session实现登录
验证码发送功能:
可以知道发送验证码的请求为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;
}
登录验证功能:
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的有效期,放行。如下图所示。
拦截器的添加需要两步:书写拦截器,设置拦截器起作用。
创建一个拦截器的文件: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的刷新。
本文作者:梅落南山
本文链接:https://www.cnblogs.com/ting65536/p/17692617.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步