登录

341 阅读4分钟

基于Session实现登录

1.发送验证码

image.png

 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都对应着一个sessionid保存在浏览器的cookie中
     session.setAttribute("code", code);
 ​
     //5.发送验证码
     log.debug("发送短信验证码成功,验证码:{}", code);
 ​
     return Result.ok();
 }

2.校验验证码

image.png

 public Result login(LoginFormDTO loginForm, HttpSession session) {
     //1.校验手机号 TODO 没有指定唯一key,有漏洞
     String phone = loginForm.getPhone();
     if (RegexUtils.isPhoneInvalid(phone)){
         return Result.fail("手机号格式错误!"); 
     }
     //2.校验验证码
     Object sessionCode = session.getAttribute("code");
     String submitCode = loginForm.getCode();
 ​
     if (sessionCode == null || !sessionCode.toString().equals(submitCode))  {
         //3.不一致,报错
         return Result.fail("验证码错误");
     }
 ​
     //4.一致,根据手机号查询用户 select * from tb_user where phone = ?
     User user = query().eq("phone", phone).one();
 ​
     //5.判断用户是否存在
     if (user == null) {
         //6.不存在,创建新用户并保存
         user = createUserWithPhone(phone);
     }
 ​
     //7.保存用户信息到session中
     session.setAttribute("user", user); // TODO 没有指定唯一key,有漏洞
     return Result.ok();
 }

3. 登录状态校验

image.png

 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.不存在,拦截
         response.setStatus(401);
         return false;
     }
     //5.存在,保存用户信息到ThreadLocal
     UserHolder.saveUser((User) user);
     return true;
 }
 ​
 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
     //afterCompletion:请求处理完之后被执行
     //移除用户
     UserHolder.removeUser();
 }

基于session的ThreadLocal,用户的每次请求还是要先判断session,再把session中的用户信息保存到ThreadLocal,从而实现用户的每一次请求都有用户信息。

为什么不直接用session进行登录校验呢? 因为如果这样的话每一次请求(Controller)都要携带session,再从session中读取数据,麻烦;而用了ThreadLocal后,可以直接在拦截器中把用户信息放在ThreadLocal中,在同一次请求的别的地方读取数据更为方便)。

缺点

在多台Tomcat中session并不共享,当请求切换到不同tomcat服务时会导致数据丢失的问题。

Redis代替session实现登录

image.png

1.发送验证码

 public Result sendCode(String phone, HttpSession session) {
     //1.校验手机号
     if (RegexUtils.isPhoneInvalid(phone)) {
         //2.不符合,返回错误信息
         return Result.fail("手机号格式错误");
     }
     //3.符合,生成验证码
     String code = RandomUtil.randomNumbers(6);
 ​
     //4.保存验证码到redis  相当于set key value ex 120
     stringRedisTemplate.opsForValue().set("login:code:" + phone, code, 2, TimeUnit.MINUTES);
 ​
     //5.发生验证码
     log.debug("发送短信验证码成功,验证码:{}", code);
 ​
     return Result.ok();

2.校验验证码

 public Result login(LoginFormDTO loginForm, HttpSession session) {
     //1.校验手机号
     String phone = loginForm.getPhone();
     if (RegexUtils.isPhoneInvalid(phone)) {
         return Result.fail("手机号格式错误!");
     }
     // 2.从redis获取验证码并校验
     String cacheCode = stringRedisTemplate.opsForValue().get("login:code:" + phone);
     String submitCode = loginForm.getCode();
 ​
     if (cacheCode == null || !cacheCode.toString().equals(submitCode)) {
         //3.不一致,报错
         return Result.fail("验证码错误");
     }
 ​
     //4.一致,根据手机号查询用户 select * from tb_user where phone = ?
     User user = query().eq("phone", phone).one();
 ​
     //5.判断用户是否存在
     if (user == null) {
         //6.不存在,创建新用户并保存
         user = createUserWithPhone(phone);
     }
 ​
     //7.保存用户信息到redis中
     //7.1. 随机生成token,作为登录令牌
     String token = UUID.randomUUID().toString(true);
     // 7.2. 将User对象转换为Hash存储,User对象的id是long,需要转为String
     UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
     Map<String, Object> userDTOMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
             CopyOptions.create()
                     .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
     // 7.3存储
     stringRedisTemplate.opsForHash().putAll("login:token:" + token, userDTOMap);
     //7.4 设置token有效期
     stringRedisTemplate.expire("login:token:" + token, 30, TimeUnit.MINUTES);
     // 8.返回token
     return Result.ok(token);
 }

3.登录状态校验

 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     //1.获取请求头的token
     String token = request.getHeader("authorization");
     if (StrUtil.isBlank(token)) {
         //不存在,拦截
         response.setStatus(401);
         return false;
     }
     //2.基于token获取redis中的用户
     Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
     //3.判断用户是否存在
     if(userMap.isEmpty()){
         //不存在,拦截
         response.setStatus(401);
         return false;
     }
     //4.存在,将查询到的Hash数据转为UserDTO对象
     UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
 ​
     //5.保存用户信息到ThreadLocal
     UserHolder.saveUser(userDTO);
     
     //6.刷新token有效期
     stringRedisTemplate.expire("login:token:" + token, 999999 , TimeUnit.DAYS);
     return true;
 }
 ​
     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
         //afterCompletion:请求处理完之后被执行
         //移除用户
         UserHolder.removeUser();
     }

服务端返回token给客户端,前端每次请求都会携带token。


为什么不用手机号作为key呢?

因为用手机号作为key的话,要校验的时候每次请求都携带手机号,放在浏览器中不安全,所以把token放在前端。

改进登录状态刷新问题

上面的登录状态校验只有在规定路径才会刷新token有效期,而其他路径并不会刷新有效期,可能导致登录用户状态异常。

解决:新增一个拦截器,在第一个拦截器中拦截所有的路径,同时刷新令牌,第二个拦截器需要判断ThreadLocal中的userDTO对象是否存在。

image.png

第一个拦截器

 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     //1.获取请求头的token
     String token = request.getHeader("authorization");
     if (StrUtil.isBlank(token)) {
         return true;
     }
     //2.基于token获取redis中的用户
     Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("login:token:" + token);
     //3.判断用户是否存在
     if(userMap.isEmpty()){
         return true;
     }
     //4.存在,将查询到的Hash数据转为UserDTO对象
     UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
 ​
     //5.保存用户信息到ThreadLocal
 ​
     UserHolder.saveUser(userDTO);
     //6.刷新token有效期
     stringRedisTemplate.expire("login:token:" + token, 999999 , TimeUnit.DAYS);
     return true;
 }

第二个拦截器

 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     //1.判断是否需要拦截,依据:ThreadLocal中是否有用户
     if (UserHolder.getUser() == null){
         //没有,需要拦截,设置状态码
         response.setStatus(401);
         return false;
     }
     //有用户,放行
     return true;
 }