首页 > 数据库 >redis实现短信登录流程

redis实现短信登录流程

时间:2023-10-24 23:02:58浏览次数:28  
标签:短信 String 登录 redis 验证码 phone session user public

(redis实现短信登录)

最近在学习使用redis,实现一个简单的短信登录功能(没使用第三方api发送短信),使用的是黑马点评项目image.png<a name="pf8N1"></a>先用session实现,再用redis代替session

一、基于session实现短信登录的流程

image.png

<a name="N2YCq"></a>

发送短信验证码

根据上边的流程图写

@PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        //发送短信验证码并保存验证码
        return userService.sendCode(phone,session);
    }
/**
     * 发送验证码
     *
     * @param phone   手机号码
     * @param session session
     * @return result
     */
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            Result.fail("手机号格式错误!");
        }
        //生成验证码,使用hutool包的内容
        String code = RandomUtil.randomNumbers(6);
        //保存验证码到session
        session.setAttribute("code", code);
        //发送验证码
        //因为需要使用第三方服务,暂时先不写
        log.debug("验证码:" + code);
        return Result.ok();
    }

<a name="pEkwc"></a>

短信验证码登录,注册

根据上边的流程图写

@PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
        // 实现登录功能
        return userService.login(loginForm,session);
    }
/**
     * 登录
     *
     * @param loginForm 登录表单
     * @param session   session
     * @return result
     */
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        //session中取出的验证码
        Object cacheCode = session.getAttribute("code");
        //用户提交的验证码
        String code = loginForm.getCode();
        if (code != cacheCode){
            Result.fail("验证码错误");
        }
        //3.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //4.不存在,创建新用户
        if (user == null) {
            user = creatUserWithPhone(phone);
        }
        //5. 保存用户到session
        session.setAttribute("user", user);
        return Result.ok();
    }

<a name="xjTDb"></a>

登录校验

因为不同的controller都需要校验登录状态,所以把登录校验直接写进拦截器,再把用户信息放进ThreadLocal中,这样都能拿到用户信息<br />image.png<br />ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。 <a name="IxFjI"></a>

写拦截器

public class LoginInterceptor implements HandlerInterceptor {

    //前置拦截,登录校验
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.从session获取用户
        HttpSession session = request.getSession();
        Object user = session.getAttribute("user");
        //2. 判断用户是否存在
        if (user == null){
            //3. 没有就拦截
            response.setStatus(401);
            return false;
        }

        //4. 如果有,保存用户到ThreadLocal
        UserHolder.saveUser((User) user);
        return true;
    }

    //渲染之后,返回给用户之前,销毁对应的用户信息,避免内存泄漏
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //销毁
        UserHolder.removeUser();
    }
}

<a name="XsxfS"></a>

配置拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    //添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                //这些路径不用拦截
                .excludePathPatterns(
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/user/login",
                        "/user/code"
                );
    }
}

<a name="ySv65"></a>

获取用户信息返回给前端

/**
     * 登录校验
     *
     * @return
     */
    @GetMapping("/me")
    public Result me() {
        //  获取当前登录的用户并返回
        User user = UserHolder.getUser();
        return Result.ok(user);
    }

<a name="cYabB"></a>

隐藏用户敏感信息

这是上边返回的用户信息,有用户信息泄露的风险<br />image.png<br />前边我们在登录的时候直接将完整的用户信息存到了session中,为了减少内存资源的占用以及降低用户信息泄露的风险,应对代码做出如下调整:<br />UserServiceImpl中<br />image.png

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            Result.fail("手机号格式错误!");
        }
        //2.校验验证码
        //session中取出的验证码
        Object cacheCode = session.getAttribute("code");
        //用户提交的验证码
        String code = loginForm.getCode();
        if (code != cacheCode){
            Result.fail("验证码错误");
        }
        //3.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //4.不存在,创建新用户
        if (user == null) {
            user = creatUserWithPhone(phone);
        }
        //5. 保存用户到session
        //使用hutool包中的BeanUtil.copyProperties()方法将用户的属性复制到userdto中
        session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        return Result.ok();
    }

UserHolder中<br />image.png

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

修改后,用户信息相对安全:<br />image.png <a name="esA7V"></a>

集群的session共享问题

session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不tomcat服务时导致数据丢失的问题,所以接下来使用redis <a name="wCnra"></a>

二、Redis代替session实现短信登录的流程

image.png<br />image.png <a name="LqyMd"></a>

发送短信验证码

@Override
    public Result sendCode(String phone, HttpSession session) {
        //校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            Result.fail("手机号格式错误!");
        }
        //生成验证码,使用hutool包的内容
        String code = RandomUtil.randomNumbers(6);
        //保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
        //发送验证码
        //因为需要使用第三方服务,暂时先不写
        log.debug("验证码:" + code);
        return Result.ok();
    }

<a name="kjDLB"></a>

短信验证码登录,注册

@Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            Result.fail("手机号格式错误!");
        }

        //2.校验验证码
        //redis中取出的验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        //用户提交的验证码
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.toString().equals(code)) {
            Result.fail("验证码错误");
        }

        //3.根据手机号查询用户
        User user = query().eq("phone", phone).one();
        //4.不存在,创建新用户
        if (user == null) {
            user = creatUserWithPhone(phone);
        }


        //5. 保存用户到redis
        //5.1使用hutool包中的BeanUtil.copyProperties()方法将用户的属性复制到userdto中
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //5.2将userDTO对象转为map
        //把userDTO对象中的所有属性转为string再储存
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).
                        setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString()));
        //5.3生成随机token
        String token = UUID.randomUUID().toString(true);
        String tokenKey=LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, map);
        //5.4设置token有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok(token);
    }

<a name="wy3yG"></a>

登录校验

private StringRedisTemplate stringRedisTemplate;

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

//在MvcConfig中加入注释的内容
// @Autowired
//     private StringRedisTemplate stringRedisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    //1.获取请求头中的token
    String token = request.getHeader("authorization");
    //2. 判断token是否存在
    if (StrUtil.isBlank(token)) {
        //3. 没有就拦截
        response.setStatus(401);
        return false;
    }

    //4.用token在redis里查询用户信息
    String tokenKey= RedisConstants.LOGIN_USER_KEY + token;
    Map<Object, Object> entries =
    stringRedisTemplate.opsForHash().entries(tokenKey);
    //查询用户是否存在
    if (entries.isEmpty()) {
        //3. 没有就拦截
        response.setStatus(401);
        return false;
    }
    //5.map转为userDto
    UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
    //4. 保存用户到ThreadLocal
    UserHolder.saveUser(userDTO);

    stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL, TimeUnit.MINUTES);
    return true;
}

<a name="eUkWL"></a>

登录拦截器的优化

需要加一个对一切路径都拦截的拦截器<br />image.png<br />新增一个拦截器:

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.从session获取用户
//        HttpSession session = request.getSession();
//        Object user = session.getAttribute("user");
//        //2. 判断用户是否存在
//        if (user == null){
//            //3. 没有就拦截
//            response.setStatus(401);
//            return false;
//        }
//
//        //4. 如果有,保存用户到ThreadLocal
//        UserHolder.saveUser((UserDTO) user);
//        return true;
//    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取请求头中的token
        String token = request.getHeader("authorization");
        //2. 判断token是否存在
        if (StrUtil.isBlank(token)) {
            return false;
        }

        //4.用token在redis里查询用户信息
        String tokenKey= RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> entries =
                stringRedisTemplate.opsForHash().entries(tokenKey);
        //查询用户是否存在
        if (entries.isEmpty()) {
            return false;
        }
        //5.map转为userDto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(entries, new UserDTO(), false);
        //4. 保存用户到ThreadLocal
        UserHolder.saveUser(userDTO);

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

将原来的LoginInterceptor改为:

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

配置拦截器:

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                //这些路径不用拦截
                .excludePathPatterns(
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/user/login",
                        "/user/code"
                ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/voucher/**",
                        "/upload/**",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/user/login",
                        "/user/code"
                ).order(0);

    }
}

注:使用的一些常量

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 30L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";
    public static final String CACHE_SHOP_TYPE = "shop:type";
    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}

标签:短信,String,登录,redis,验证码,phone,session,user,public
From: https://blog.51cto.com/u_16277539/8011073

相关文章

  • 分布式锁优化(基于redisson实现)
    基于setnx实现的分布式锁存在下面的问题:1.不可重入同一个线程无法多次获取同一把锁2.不可重试获取锁只尝试一次就返回false,没有重试机制3.超时释放锁超时释放虽然可以避免死锁,但如果是业务执行耗时较长,也会导致锁释放,存在安全隐患4.主从一致性(主写从读)如果Redis提供了主从集群,主......
  • Redis 6 学习笔记 4 —— 通过秒杀案例,学习并发相关和apache bench的使用,记录遇到的问
    背景这是某硅谷的redis案例,主要问题是解决计数器和人员记录的事务操作按照某硅谷的视频敲完之后出现这样乱码加报错的问题 乱码的问题要去tomcat根目录的conf文件夹下修改logging.properties,把下面两个encoding参数都改成GBK就行。其实错误也很明显(ClassNotFoundExceptio......
  • SSH两种登录方式(公私钥)解析
    SSH登录方式主要分为两种1.用户名密码验证方式  说明:(1)当客户端发起ssh请求,服务器会把自己的公钥发送给用户;(2)用户会根据服务器发来的公钥对密码进行加密;(3)加密后的信息回传给服务器,服务器用自己的私钥解密,如果密码正确,则用户登录成功。2.基于密钥的登录......
  • python selenium 利用pyautogui+ActionChains 完美解决我的滑块验证登录问题
    在解决滑块验证的时候不知道什么原因明明是滑块已经对上了,代码执行就是会校验不通过,手动时就可以,中间也做利用ActionChains模块减速滑动轨迹的操作,但仍然不行,后面在执行代码中添加了pyautogui模块使鼠标悬停在屏幕中的某个点而不改变ActionChains鼠标的定位后终于每次都能通过了fro......
  • ASP.NET 安全认证(三)——用Form 表单认证实现单点登录(Single Sign On) 作者:寒羽枫
    【原创】ASP.NET安全认证(三)——用Form表单认证实现单点登录(SingleSignOn)作者:寒羽枫(cityhunter172)第三部分实现单点登录(SingleSignOn) “等了好久终于等到今天,写了好久终于就快完结,但是网友的反应却让我有一些的伤心。盼了好久终于盼到今天,忍了好久终于把此文撰写,那些受冷......
  • docker安装redis
    docker安装Redis拉取镜像dockerpullredis创建目录mkdir/tool/redis镜像里不包含配置文件,需要拉取redis最新的配置文件,查看下载完成直接通过ftp传到/tool/reids目录下就行因为是官方配置,需要我们手动改下配置:#常用配置bind127.0.0.1 #注释掉这部分,使redis可以......
  • 为什么单线程Redis能那么快
    单线程澄清Redis的单线程,指的是Redis的键值对读写由一个线程来完成。Redis的多线程:持久化异步删除集群数据同步网络IO(Redis6.0引入,5.0及之前都是单线程)......
  • Redis-cluster群集操作步骤(主从切换、新增、删除主从节点)
    1.进入集群客户端任意选一个redis节点,进入redis所在目录cd/redis所在目录/src/./redis-cli-h本地节点的ip-predis的端口号-a密码[root@mysql-db01~]#redis-cli-h10.0.0.51-p637910.0.0.51:6379> 2.查看集群中各个节点状态集群(cluster)clusterinfo......
  • docker-搭建一主两备redis集群
    一目的docker-搭建一主两备redis集群概述:目前要搭建一个“一主两备”redis集群,这个三个容器中redis的端口号为默认的6379,对外暴露的端口为6701,6702,6703,其中6701为master。6702和6703为slave二实现1.准备三份配置文件 1.1配置文件可从网上下载,下载后,可按该贴作修改,http......
  • ClickeOnce 打包 vc_redist
    添加RuntimeLibraries在VisualStudio项目属性中,选择发布,选择系统必备组件,然后选中VisualC++RuntimeLibraties,选中“从与我的应用程序相同的位置下载系统必备组件”下载下载vc_redist放入C:\ProgramFiles(x86)\MicrosoftSDKs\ClickOnceBootstrapper\Packages......