短信登录
导入黑马点评项目
导入资料中提供的SQL文件
其中的核心表有:
tb_user :用户表
tb_user_info :用户详情表
tb_shop:用户信息表
tb_shop_type:商户类型表
tb_blog:用户日记表(达人探店日记)
tb_follow:用户关注表
tb_voucher: 优惠劵表
tb_voucher:优惠劵的订单表
tb_voucher_order
注意事项:MySQL版本最好采用5.7及以上版本
该项目为单体项目,采用前后端分离的模式,将后端部署在Tomcat上,前端会将其放置在Nginx服务器上。
在PC端或者移动端在请求页面时,其实是想Nginx发起请求得到的静态资源,页面在通过Ajax像我们的服务端发起请求去查询数据,这些数据可能来自于Redis集群,或者来自MySQL集群。然后再将查询到的数据返回给前端,前端完成渲染即可。是一种前后端分离的架构。
还会考虑项目的并发能力,项目还具备水平拓展的能力,即项目部署在Tomcat上后,如果单台Tomcat上支撑不住,还可以做一个水平的扩展形成一个负载均衡的集群,在多台Tomcat上部署代码,一旦步入集群之后,将来就会存在一些集群间的数据共享的问题。
!
导入后端模板
从资料中拿到项目源码,将其复制到IDEA中,然后利用IDEA打开,注意springboot与MyBatis-plus的版本冲突。
启动项目后,在浏览器访问:http://localhost:8081/shop-type/list,如果看到数据则证明运行没有问题
注意事项:不要忘记修改application.yml的MySQL,redis地址信息。
导入前端项目:
该项目将整个前端代码部署到Nginx里,
在资料中提供了Nginx文件夹,将其复制到任意目录,确保该目录不包含中文、特殊字符和空格
在该文件夹下打开命令行界面,输入 start nginx.exe
然后在浏览器中点击F12,打开手机模式,并访问 http://localhost:8080
基于session实现登录
短信登录包括短信的发送,然后是基于短信验证码的登录,最后是对登录状态的一个校验
发送短信验证码
用户会提交自己的手机号码,服务端接收到手机号后,我们要去验证用户的手机号是否合法。
校验失败则重新提交,校验成功则生成验证码,还要去保存验证码(我们发送验证码的目的是让用户去做登录,需要校验验证码,需要现将其保存到session中)。
最后发送验证码。
流程图如下
实现步骤:
首先查看请求,请求方式为POST,请求路径为/user/code,请求参数为phone,电话号码,无返回值
先写发送手机验证码的代码,业务代码需要在服务层编译,所以先在Controller层写出方法名以及要调用的参数,phone,以及session(将验证码保存在session中)
代码展示:
public Result sendCode(String phone, HttpSession session) {
//1.校验手机号
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//3.如果符合,生成验证码
String code = RandomUtil.randomNumbers(6);
//4.保存验证码到session
session.setAttribute("code",code);
//5.发送验证码(模拟发送验证码,该业务并未实现)
log.debug("发送短信验证码成功,验证码:{}",code);
// 6.返回结果
return Result.ok();
}
效果展示:
验证码:
短信验证码登录、注册
用户收到验证码则用来做验证或者登录。
首先需要提交手机号和验证码到后台,后台则去验证提交的两个数据,先需要校验验证码(将验证码与上一步保存在session中的验证码做一个比较)。
不一致则会回退上一步,重新提交。如果一样则根据手机号到数据库中去查询该用户是否存在。
如果不存在,则说明从未访问过服务器,手机号是全新的,这是需要去将其注册成一个新用户,填充一些基本信息,将新用户写入到数据库中。
如果存在,则可以登录,服务器需要去保存用户信息到session中。
登录与注册是在一个工程中完成的,
流程图如下
实现步骤:先输入验证码,发送请求并查看,发现为post请求,且负载信息为json字符串,并且传入的是验证码以及电话号码
思路及代码展示:
@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
//1.校验手机号
String phone = loginForm.getPhone();
if (RegexUtils.isPhoneInvalid(phone)) {
//2.如果不符合,返回错误信息
return Result.fail("手机号格式错误");
}
//2.校验验证码
Object cacheCode = session.getAttribute("code");
String code = loginForm.getCode();
// 一般校验时,从反向校验,这种校验不需要if嵌套,否则会嵌套if,避免if嵌套过深
if (cacheCode == null || !cacheCode.toString().equals(code)){
//3.不一致,返回错误信息
return Result.fail("验证码错误");
}
//4.一致,根据手机号查询用户 select * from user where phone = ?
User user = query().eq("phone", phone).one();
//5.判断用户是否存在
if (user == null){
//6.不存在,创建新用户并保存
// 方法定义在函数中 创建用户
//在创建完用户到数据库后还需要保存在session中,所以直接赋值给user
user = createUserWithPhone(phone);
}
//7.保存用户信息到session并返回结果
session.setAttribute("user",user);
//这里不需要返回一个登录凭证,因为用户信息已经保存在session中,所以不需要返回一个登录凭证
//session的原理是在服务器端保存用户信息,并在客户端保存一个标识符。
// 这个标识符就是sessionId,这个标识符在客户端的cookie中保存,这样客户端就可以通过这个标识符来获取服务器端保存的用户信息。
return Result.ok();
}
效果展示:
查询数据库得用户插入成功
完成。
校验登录状态
用户登录成功后,访问一些关键的数据时需要去校验登录状态。
服务器将用户信息保存在session中,那将来用户访问时,需要基于session进行校验。
session是基于cookie的,每一个session都会有一个对应的sessionID保存在浏览器的cookie中,所以当用户访问服务时一定会携带cookie(包含sessionID)。
这时服务器就可以基于cookie中的sessionID拿到session,在从session中拿到用户,服务器只需要验证session中是否有用户信息即可。
如果经过判断没有用户,直接拦截请求。
如果判断存在,就将用户信息缓存起来到ThreadLocal
(线程域对象,核心作用是让每一个线程都有自己的数据副本,避免共享资源引发的竞态条件。 在实际开发业务中,每一个请求到达微服务时,都是一个独立的线程,如果没有使用ThreadLocal,而是将用户信息存放在本地变量中,可能出现多线程并发修改的安全问题,ThreadLocal会将数据保存到每一个线程的内部,在县城内部创建一个ThreadLocalMap去保存,每一个线程都有自己独立的存储空间,相互之间没有干扰)
中(在之后的业务中一定会用到当前登录的用户信息,将其缓存方便后续的业务使用),然后服务器放行。
流程图如下:
实现步骤:
在前面已经完成了发送短信验证码以及短信验证码登录注册的功能,但登录校验的功能并没有完成。
登录校验的URL为http://localhost:8080/user/me,用来查询当前登录的用户信息,然后服务端返回正确的用户信息,则登录校验成功,并将其跳转到首页。
再流程图中用户的请求会带上cookie,而cookie中含有登录凭证SessionID,而服务端只需要根据sessionID得到session,在从session中获取用户,判断用户是否存在。
但是其中有些问题:
我们的登录校验是在UserController中编译的,前端会向userController发起请求,而服务端在UserController的是在对应业务中编译业务逻辑代码,而在后期,越来越多的业务需要去校验用户的登录,我们不能在每一个Controller中都去编译一段登录校验的业务代码,
而在springmvc中的拦截器是在所有的controller执行之前去执行,有了拦截器之后,用户的请求就不能直接去访问服务端的controller,所有请求必须要先经过拦截器,再由拦截器判断该不该放行,那就可以将登录校验的业务代码中放到拦截器中去执行。
而拦截器确实可以实现对用户登录的校验,校验之后,业务可能需要该用户信息。
但用户信息却在拦截器中,Controller并不能拿到。因此需要将拦截器中的用户信息传递到controller中。
需要考虑线程安全问题,把用户信息存储在session中,则需要在跨各种处理组件或页面时都要从session中去取得用户信息,这增加了访问session的开销。
所以我们这里用ThreadLocal。
ThreadLocal就可以解决该问题,ThreadLocal是一个线程域对象,每一个进入Tomcat的请求都是一个独立的线程,而ThreadLocal的核心作用就是让每一个线程都有自己的数据副本,避免共享资源引发的竞态条件,这样就可以让每一个线程都有对应的独立内存空间取保存对应用户,线程之间互不干扰,因此无论几条请求访问哪些Controller都可以做到独立线程,都有自己的独立用户信息,当需要用户信息时,则Controller从ThreadLocal中取出用户即可。
因此我们要在拦截器中实现校验登录,以及将用户信息存入到ThreadLocal。
隐藏用户敏感信息
服务端在登录校验成功后返回的信息较多,包括用户密码,时间,电话等敏感信息,存在泄漏风险,在UserController中,登录校验的业务代码拿到的是用户的全部信息存入session,session是Tomcat的内存空间,这里面的信息越多,对于服务来讲,压力越大。因此不需要存储全部信息 。而我们在登录业务中直接将user的全部信息放入session中,因此需要在DTO包下新建UserDTO,里面只有一些不重要的信息属性字段,因此只需要在登录业务中在存入session调用hutool中的工具类将User属性拷贝给UserDTO。
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
然后将拦截器中取出的User对象改为USerDTO对象即可。也需要将UserHolder(用于新建ThreadLocal并调用的工具类)中的User对象改为UserDTO
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();
}
}
代码展示:
在utils包下新建Logininterceptor类(拦截器)
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.web.servlet.HandlerInterceptor;
public class LoginInterceptor implements HandlerInterceptor {
// 前置拦截器
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取请求头中的session
HttpSession session = request.getSession();
// 2.获取session的用户
Object user = session.getAttribute("user");
// 3.判断用户是否存在
if (user == null) {
// 4.不存在 拦截器拦截 返回401状态码 未授权
response.setStatus(401);
return false;
}
// 5.存在,保存用户信息到ThreadLocal 并放行
// 在工具类中定义了一个UserHolder 是一个线程安全的ThreadLocal变量,用于保存当前线程的用户信息。
// 其中有三个方法:saveUser( 保存),getUser(拿到),removeUser(移除)。
UserHolder.saveUser((UserDTO) user);
return true;
}
// 拦截器 后处理
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 移除用户 避免用户泄漏
UserHolder.removeUser();
}
}
在配置包下新建MVCConfig类
代码如下:
package com.hmdp.config;
import com.hmdp.utils.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
// 除了这些路径,其他路径都进行拦截
.excludePathPatterns(
"/user/code",
"/user/login",
"/blog/hot",
"/voucher/**",
"/shop/**",
"/shop-type/**",
"/upload/**",
"/blog/query/hot",
"/druid/**"
);
}
}
至此,校验登录状态业务完成。
测试:
登录校验成功,并且数据也未泄露。
希望对大家有所帮助!!