【黑马点评】短信登录

【黑马点评】短信登录

基于Session的实现

Session:

Session 是保存在服务端(Tomcat)的一块内存空间,用来存本次用户会话的数据

HTTP 协议是无状态协议 ,多次请求之间相互独立,服务器默认无法识别多次请求来自同一个用户,不知道用户的登录状态。 如果每次请求都去查询数据库校验登录,会消耗大量性能。 用户登录成功后,服务端生成唯一 sessionId,创建对应的 Session 对象保存在服务端内存;再把 sessionID写入 Cookie 返回给浏览器。 浏览器后续每一次请求,自动携带 Cookie 中的 sessionID。服务器读取 sessionID,找到服务端对应的 Session,就可以识别该用户的登录状态。

实现流程

发送短信验证码:

java 复制代码
//Controller:
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        return userService.sendCode(phone,session);
    }

//service

    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(isPhoneInvalid(phone))
        {
            return Result.fail("手机号错误");
        }
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码到session
        session.setAttribute("code", code);
        //4.发送验证码
        log.info("验证码:{}",code);
        return Result.ok();
    }

session可以存放数据,我们将验证码放在session里面,方便后面验证的时候用

短信验证码登录,注册:

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

//Service

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1.校验手机号
        if(isPhoneInvalid(phone))
        {
            return Result.fail("手机号错误");
        }
        //2.校验验证码
        //cachecode是用户输入的验证码
        String cachecode = loginForm.getCode();
        //code是系统生成的验证码
        Object code = session.getAttribute("code");
        if(cachecode==null || !cachecode.toString().equals(code))
        {
            return Result.fail("验证码错误");
        }
        //3.校验用户是否存在
        User user = query().eq("phone", phone).one();
        //不存在,创建并保存在session中
        if(user==null)
        {
            user=createUserByPhone(phone);
        }
        //直接保存在session中
        session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
        return Result.ok();
    }

    private User createUserByPhone(String phone) {
        User user=new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(7));
        return user;
    }

这里做了数据筛除,为了保护用户信息,用UserDTO封装用户信息并返回,里面只包含用户名称和ID。

校验用户状态:

java 复制代码
//拦截器
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) {
            response.setStatus(401);
            return false;
        }
        //保存
        UserHolder.saveUser((UserDTO) user);
        //放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

//Config
@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"
                );
    }
}

ThreadLocal的工作流程:

UserHolder如下:

java 复制代码
package com.hmdp.utils;

import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;

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();
    }
}

ThreadLocal 是一个可以隔绝其他变量的工具,只处理当前的变量。

当我们请求拦截器拦截的方法时,ThreadLocal就会启动。

以请求首页为例:

请求首页→触发拦截器→拦截器流程结束后→执行Controller→Service→Mapper

拦截器内部:

首先会先获取Session,并获取session里面存放的用户

java 复制代码
HttpSession session = request.getSession();
Object user = session.getAttribute("user");

检查用户是否登录,如果登录则将用户信息存储之后放行,否则拦截

java 复制代码
        if(user == null) {
            response.setStatus(401);
            return false;
        }
        //此方法用到了ThreadLocal的set方法
        UserHolder.saveUser((UserDTO) user);
        //放行
        return true;

在响应返回浏览器之后,会销毁信息,防止内存泄漏

java 复制代码
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }

session的共享问题

由于session是每个tomcat服务器独有的,数据不能共享,就会导致当用户第一次在服务器1请求,第二次去服务器二请求的时候找不到登录信息,无法实现数据共享。

上述数据共享问题,redis可以解决。而且redis的数据存储是存在内存之中的,数据的结构也是key,value结构。

基于redis的短信验证

发送验证码:

存储数据结构:String key为手机号,value为验证码

校验登录状态:

存储数据方式:Hash

key: token value:存储数据

代码实现

发送验证码:

只需要修改存储地方就行

java 复制代码
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
        if(isPhoneInvalid(phone))
        {
            return Result.fail("手机号错误");
        }
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //4.发送验证码
        log.info("验证码:{}",code);
        return Result.ok();
    }

登录校验:

java 复制代码
 @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1.校验手机号
        if(isPhoneInvalid(phone))
        {
            return Result.fail("手机号错误");
        }
        //2.校验验证码,从redis里面提取
        String cachecode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY +phone);
        Object code = loginForm.getCode();
        if(cachecode==null || !cachecode.equals(code))
        {
            return Result.fail("验证码错误");
        }
        //3.校验用户是否存在
        User user = query().eq("phone", phone).one();
        //不存在,创建并保存在session中
        if(user==null)
        {
            user=createUserByPhone(phone);
        }
        //4.存在,保存在redis里
        //4.1.将user转成userDTO
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //4.2.将userDTO里的数据转成Map,方便一次性写入redis
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).
                        setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        //4.3.生成token作为key
        String token = UUID.randomUUID().toString();
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
//        session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));
        return Result.ok(token);
    }

    private User createUserByPhone(String phone) {
        User user=new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomNumbers(7));
        return user;
    }

拦截器:

拦截器采用两个,主要针对用户在长时间浏览不需要登录验证的网页导致token不刷新而过期的现象

LoginInterceptor:拦截需要登录的网页

java 复制代码
package com.hmdp.utils;


import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (UserHolder.getUser()==null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
    
}

RefreshInterceptor:拦截所有网页,用于token的刷新

java 复制代码
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取token
        String token = request.getHeader("authorization");
        //2.查看token是否为空,空的说明没登录,不需要刷新token,放行
        if (StrUtil.isBlank(token)) {
            return true;
        }
        //获取用户信息
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        //如果用户信息为空,说明token已经过期
        if (userMap.isEmpty()) {
            return true;
        }
        //将数据转换为userDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //保存
        UserHolder.saveUser(userDTO);
        //刷新token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

MvcConfig:配置拦截器,并配置先后顺序

java 复制代码
```java
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

@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);

        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
    }

}	

如果喜欢我的内容,觉得我的内容有帮助到你的,可以动动小手点赞收藏,我们下期再见!

相关推荐
2601_962298671 小时前
正则表达式引擎 哪些
java·python·perl·pcre·正则表达式引擎
孙克旭_1 小时前
JVM 入门详解:JVM、JRE、JDK 区别,JVM 基础结构梳理
java·开发语言·jvm
随遇而安zx1 小时前
【地基篇】---JVM 内存结构知识点(Java 8 运行时数据区深度解析)
java·开发语言·jvm
布吉岛的石头1 小时前
Java 程序员第 48 阶段1:Transformer 架构总览与自注意力直觉,从 RNN 痛点理解注意力机制
java·架构·transformer
吴声子夜歌1 小时前
Guava——事件总线
java·网络·guava
en.en..1 小时前
Ubuntu嵌入式开发 export环境变量
java·开发语言·数据库
gf13211111 小时前
【python_回复邮件】
android·java·python
攻城有术2 小时前
专项攻克——重写 Redis 依赖包方法的 6 种实现方式
java·数据库·redis·bootstrap
跨境数据猎手2 小时前
京东开放平台商品详情接口(jd.item_get):签名、缓存与批量采集
java·spring·缓存