Redis实战—黑马点评(一) 登录篇

news2024/9/24 4:25:08

Redis实战 — 黑马点评(一) 登录篇

来自黑马的redis课程的笔记

【黑马程序员Redis入门到实战教程,深度透析redis底层原理+redis分布式锁+企业解决方案+黑马点评实战项目】

目录

  • Redis实战 — 黑马点评(一) 登录篇
    • 1. 项目介绍
    • 2. 短信登录
      • 2.1 流程:
      • 2.2 一些小的收获
      • 2.3 系统安全(重点)
      • 2.4 实现流程和关键代码
        • 2.4.1 发送验证码
        • 2.4.2 短信验证码登录/注册
        • 2.4.3 校验登录状态

1. 项目介绍

在这里插入图片描述

在这里插入图片描述

2. 短信登录

2.1 流程:

在这里插入图片描述

2.2 一些小的收获

  • 使用hutool中的RandomUtil.randomNumbers(6)RandomUtil.randomString(10)随机生成验证码和随机字符串(用于默认用户名)

  • 代码中不要出现“魔法值”,要统一定义常量

  • mp中lambdaQuery的使用(相信之后不用再new QueryWrapper了)

    User user = lambdaQuery().eq(User::getPhone, phone).one();
    
  • 使用UUID.randomUUID().toString(true)来生成不带‘-’的uuid

  • // 使用hutool工具中的对象转map,并自定义操作。
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
            CopyOptions.create()
                    .ignoreNullValue()
                    .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
    
  • 封装好的常用的正则表达式和正则工具类

    public abstract class RegexPatterns {
        /**
         * 手机号正则
         * 使用过程中发现有些手机号不支持如191这些新手机号,自行修改即可,例如要支持191,只需将9[89]改为9[189]
         * 括号里面的就是第2,3位手机号
         */
        public static final String PHONE_REGEX = "^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$";
        /**
         * 邮箱正则
         */
        public static final String EMAIL_REGEX = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
        /**
         * 密码正则。4~32位的字母、数字、下划线
         */
        public static final String PASSWORD_REGEX = "^\\w{4,32}$";
        /**
         * 验证码正则, 6位数字或字母
         */
        public static final String VERIFY_CODE_REGEX = "^[a-zA-Z\\d]{6}$";
    
    }
    
    public class RegexUtils {
        /**
         * 是否是无效手机格式
         * @param phone 要校验的手机号
         * @return true:不符合,false:符合
         */
        public static boolean isPhoneInvalid(String phone){
            return mismatch(phone, RegexPatterns.PHONE_REGEX);
        }
        /**
         * 是否是无效邮箱格式
         * @param email 要校验的邮箱
         * @return true:不符合,false:符合
         */
        public static boolean isEmailInvalid(String email){
            return mismatch(email, RegexPatterns.EMAIL_REGEX);
        }
    
        /**
         * 是否是无效验证码格式
         * @param code 要校验的验证码
         * @return true:不符合,false:符合
         */
        public static boolean isCodeInvalid(String code){
            return mismatch(code, RegexPatterns.VERIFY_CODE_REGEX);
        }
    
        // 校验是否不符合正则格式
        private static boolean mismatch(String str, String regex){
            if (StrUtil.isBlank(str)) {
                return true;
            }
            return !str.matches(regex);
        }
    }
    

2.3 系统安全(重点)

用户登录,管理的三种常见方案:

  1. 传统的cookie + session

    流程:

    1、用户向服务器发送用户名和密码。

    2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

    3、服务器向用户返回一个 session_id,写入用户的 Cookie。

    4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

    5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

    优点:

    • 数据存放在服务端,客户端只拿到令牌,服务端对用户状态掌控力强。

    缺点:

    • 浏览器支持cookie,移动端不行。
    • session是存放在服务端程序内存中的,分布式架构下session共享是个问题,不易水平扩展。
  2. JWT

    流程:

    1、用户向服务器发送用户名和密码。

    2、服务器验证通过后,颁发一个jwt字符串,该字符串中可以包含一些不敏感的用户信息

    3、客户端存储jwt。

    4、用户随后的每一次请求,都会带上jwt,服务端验证jwt判断用户是否合法。

    优点:

    • 去中心化,便于分布式系统使用。
    • 基本信息可以直接放在token中。 username,nickname,role等。

    缺点:

    • 一旦颁发jwt,该jwt在有效期内都有效,服务端不能主动让其失效。
  3. token + redis

    该方案就是cookie + session的一个升级版,其思想是一样的。

    也就是让redis代替session,让token代替sessionid。

    流程:

    1、用户向服务器发送用户名和密码。

    2、服务器验证通过后,在redis里面保存相关数据,比如用户角色、登录时间等等。

    3、服务器向用户返回一个 token。

    4、用户随后的每一次请求,都会带上该token。

    5、服务器收到 token,在redis中找到前期保存的数据,由此得知用户的身份。

    优点:

    • 数据存放在服务端,客户端只拿到令牌,服务端对用户状态掌控力强。
    • 用redis代替session后,不再担心分布式架构session共享的问题,性能好。

    缺点:

    • 依赖redis,redis操作增加系统复杂度。

redis课程当然采用的是第三种方案。

jwt的致命缺点就是无法主动踢出用户,个人感觉用户管理最佳方案就是token + redis的方式,遇到瓶颈只需要横向扩展redis即可。

2.4 实现流程和关键代码

2.4.1 发送验证码

要考虑的问题:

  • 如何生成验证码?

  • 验证码有效期如何实现?

  • 如何将手机号和验证码绑定?

  • 如何发送短信?

解决:

  • 使用RandomUtil.randomNumbers(6);生成随机六位验证码。
  • 使用redis的特性,设置该数据的ttl。
  • 将手机号作为key,验证码作为value。
  • 购买短信服务。
@Autowired
StringRedisTemplate stringRedisTemplate;

@Override
public Result sendCode(String phone, HttpSession session) {
    // 1. 校验手机号
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2. 若不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3. 符合,生成验证码
    String code = RandomUtil.randomNumbers(6);
    // 4. 保存验证码和手机号到redis, 设置过期时间, 此处LOGIN_CODE_TTL为避免产生魔法值而设置的常量 2L
    stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);
    // 5. 发送验证码(此处没钱购买服务,一般可以去各大云购买短信服务调用相应的api)
    log.debug("发送验证码成功,验证码:{}", code);
    return Result.ok();
}

2.4.2 短信验证码登录/注册

跟着流程走即可,前端需要保存此处返回的token:

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    // 1. 校验手机号
    String phone = loginForm.getPhone();
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2. 若不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 2. 校验验证码
    String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
    String code = loginForm.getCode();
    if (StrUtil.isBlank(code) || !code.equals(cacheCode)) {
        // 3. 不一致 报错
        return Result.fail("验证码错误");
    }
    // 4. 一致 根据手机号查询用户
    User user = lambdaQuery().eq(User::getPhone, phone).one();
    // 5. 判断用户是否存在
    if (user == null) {
        // 6. 不存在 创建用户
        user = createUserWithPhone(phone);
    }
    // 7. 存在 保存到session/redis
    // 7.1 随机生成token,作为登录令牌
    String token = UUID.randomUUID().toString(true);
    // 7.2 将user转为hashmap存储
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                                                     CopyOptions.create()
                                                     .ignoreNullValue()
                                                     .setFieldValueEditor((name, value) -> value.toString()));
    // 7.3 存储
    stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);
    stringRedisTemplate.expire(LOGIN_USER_KEY + token, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8. 返回token
    return Result.ok(token);
}

// 创建用户
private User createUserWithPhone(String phone) {
    // 1. 创建用户
    User user = new User();
    user.setPhone(phone);
    user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));
    // 2. 保存用户
    save(user);
    return user;
}

2.4.3 校验登录状态

主要是使用拦截器实现,知识点:

  • 拦截无权限请求

  • token自动续签

  • 使用ThreadLocal

思路:

一个拦截器用于token续签,一个拦截器用于权限验证。

在这里插入图片描述

token续签:

小tips:由于注册拦截器需要手动new,所以我们不使用自动装配的方式来注入StringRedisTemplate,而是使用构造方法在注册拦截器的时候注入。

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");
        if (StrUtil.isBlank(token)) {
            return true;
        }
        // 2. 获取redis中的用户
        String key = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3. 判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
//        UserDTO user = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserDTO user = BeanUtil.toBean(userMap, UserDTO.class);
        // 5. 存在 保存用户信息到ThreadLocal
        UserHolder.saveUser(user);
        // 6. 刷新token有效期
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 7. 放行
        return true;
    }

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

权限验证:

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

拦截器注册:

小tips:

  • 在这里自动装配StringRedisTemplate,通过构造函数注入RefreshTokenInterceptor,使代码更优雅。

  • 不是所有请求都需要权限验证,所以根据需求设置白名单。

  • 使用order来设置拦截器的优先级,order越小,优先级越高,order相同,优先级按注册顺序降低。

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        List<String> excludePath = new ArrayList<>();
        excludePath.add("/user/code");
        excludePath.add("/user/login");
        excludePath.add("/blog/hot");
        excludePath.add("/shop/**");
        excludePath.add("/shop-type/**");
        excludePath.add("/upload/**");
        excludePath.add("/voucher/**");
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(excludePath)
                .order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**")
                .order(0);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/340903.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深度学习笔记:使用随机梯度下降法识别mnist数据集

深度学习算法实现流程&#xff1a; 1 从训练数据中随机选出一部分数据&#xff0c;称为mini-batch。我们的目标为减小mini-batch损失函数的值 2 计算损失函数关于权重的梯度。梯度方向即为损失函数值减小最快的方向 3 将权重沿梯度下降方向更新 4 重复以上步骤&#xff0c;在…

【自动驾驶汽车技术 | 车载雷达系统】

本文编辑&#xff1a;调皮哥的小助理 1、摘要 自动驾驶汽车传感器系统一般包括4种雷达&#xff1a;激光雷达(Lidar)、毫米波雷达(mmWave Radar)、超声波雷达(Ultrasonic Radar)和红外雷达(Infrared Radar)。目前激光雷达和毫米波雷达是基本和必要的车载传感器设备&#xff0c;…

I.MX6ULL内核开发8:linux设备驱动模型

目录 一、为什么需要设备驱动模型 二、sysfs概述 驱动模型一 驱动模型二 kobject kset kobj_type 一、为什么需要设备驱动模型 早期内核&#xff08;2.4之前&#xff09;没有统一的设备驱动模型&#xff0c;但是照样可以使用&#xff08;之前的led字符设备驱动&#xff…

2023-2-12刷题情况

字母板上的路径 题目描述 我们从一块字母板上的位置 (0, 0) 出发&#xff0c;该坐标对应的字符为 board[0][0]。 在本题里&#xff0c;字母板为board [“abcde”, “fghij”, “klmno”, “pqrst”, “uvwxy”, “z”]&#xff0c;如下所示。 我们可以按下面的指令规则行动…

合宙Air103|fbd数据库| fskv - 替代fdb库|LuatOS-SOC接口|官方demo|学习(16):类redis的fbd数据库及fskv库

基础资料 基于Air103开发板&#xff1a;&#x1f697; Air103 - LuatOS 文档 上手&#xff1a;开发上手 - LuatOS 文档 探讨重点 对官方社区库接口类redis的fbd数据库及fskv库的调用及示例进行复现及分析&#xff0c;了解两库的基本原理及操作方法。 软件及工具版本 Luat…

肝了几天的Git入门教程,收获满满

1.简介 谈及版本控制系统&#xff0c;或许大多数程序员最开始接触的都是SVN&#xff08;Subversion&#xff09;&#xff0c;它是一个集中式的版本控制系统&#xff0c;使用的时候需要提供一台的服务器来进行部署&#xff0c;所有的更新与同步操作都需要与这台服务器进行交互&…

windows/linux下Qt可执行程序打包,linux桌面双击运行程序sh脚本

1、windows下Qt打包 windows下Qt的可执行文件打包简单的来说就是利用Qt自带依赖的打包工具windeployqt进行打包&#xff0c;该工具存在Qt安装目录下&#xff0c;执行命令为&#xff1a;windeployqt name.exe 打包依赖文件可参考如下链接中1-7步&#xff0c;后面的步骤是打包依…

156、【动态规划】AcWing ——3. 完全背包问题:二维数组+一维滚动数组(C++版本)

题目描述 原题链接&#xff1a;3. 完全背包问题 解题思路 完全背包相对于01背包来说&#xff0c;对同一个物品可以选择多次。而01背包对同一个物品只能选择一次。 递推公式上的区别&#xff1a;01背包是dp[i][j] max(dp[i - 1][j], dp[i - 1][j - v[i]] w[i])&#xff0c;…

14. 最长公共前缀

14. 最长公共前缀 一、题目描述&#xff1a; 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&#xff0c;返回空字符串 “”。 示例 1&#xff1a; 输入&#xff1a;strs [“flower”,“flow”,“flight”] 输出&#xff1a;“fl” 示例 2&#xff1a; …

在线支付系列【23】支付宝支付接入指南

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言接入指南1. 创建应用2. 绑定应用3. 配置密钥4. 上线应用5. 开通产品沙箱环境开发前准备&#xff08;沙箱环境&#xff09;1. 获取参数、秘钥、证书2. 下载支付宝客户端3. 案例演示前言 在之…

一个自动配置 opengrok 多项目的脚本

前段时间在服务器上配置 opengrok 阅读代码&#xff0c;项目有很多个&#xff0c;一个一个手动配置比较繁琐。 我从搭建 tomcat 和 opengrok&#xff0c;到配置和索引完 5 个 Android 项目&#xff0c;用了差不多一整天。 要是再让我手动配置几个项目&#xff0c;估计真要崩溃…

学习Request和Response这一篇就够啦~

Request(请求) &#xff1a; Request&#xff1a;获取请求数据 Response:设置响应数据 Request继承体系&#xff1a; 使用request对象&#xff0c;查阅JavaEE API文档的HttpServeltRequest接口 Tomcat需要解析请求数据&#xff0c;封装为request对象&#xff0c;并且创建requ…

知识图谱嵌入技术研究综述

作者 张天成 1 , * 田 雪 1 , * 孙相会 1 , * 于明鹤 2 , * 孙艳红 1 , * 于 戈 摘要 知识图谱 是一种用图模型来描述知识和建模事物之间的关联关系的技术。 知识图谱嵌入 作为一种被广泛采用的知识表示方法。 主要思想是将知识图谱中的实体和关系嵌入到连续的向量空间中…

Ansible---playbook剧本

目录 引言&#xff1a;什么是playbook&#xff1f; 一、Playbook 1.1、playbook中的核心元素 1.2、playbook中的基础组件 1.3、playbook格式说明 1.4、实例&#xff1a;httpd服务剧本 二、playbook中的模块 2.1、Templates 模块 2.2、tags 模块 2.3、Roles 模块 引言&…

关于链表中插入结点的操作……

服了&#xff0c;好久没敲链表了&#xff0c;这都忘了 newnode->next cur->next; cur->next newnode; newnode->next cur->next; cur->next newnode; newnode->next cur->next; cur->next newnode; newnode->next cur->next; cur-…

科技常识就像雨衣,要常备哦

科技常识就像雨衣&#xff0c;平常不准备&#xff0c;遇雨成落汤鸡 昨日晨跑遇雨&#xff0c;随身带轻便雨塑料雨衣 趣讲大白话&#xff1a;晴天挖水渠 *********** 信息科技是现代科技的【火车头】 往前看&#xff1a;要关注趋势 往后看&#xff1a;要了解行业历史 在当下&…

数据结构 | 栈与队列

&#x1f525;Go for it!&#x1f525; &#x1f4dd;个人主页&#xff1a;按键难防 &#x1f4eb; 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;和大家一起学习&#xff0c;一起进步&#x1f440; &#x1f4d6;系列专栏&#xff1a;数据结构与算法 &#x1f52…

使用C#编写k8s CRD Controller

本文项目地址&#xff1a;k8s-crd - Repos (azure.com)CRDCRD指的是Custom Resource Definition。开发者更多的关注k8s对于容器的编排与调度&#xff0c;这也是k8s最初惊艳开发者的地方。而k8s最具价值的地方是它提供了一套标准化、跨厂商的 API、结构和语义。k8s将它拥有的一切…

【测试开发】web 自动化测试实战 --- MuiscServerTest

目录界面测试功能测试1. 登录注册模块功能测试2. 音乐列表页自动化测试3. 喜欢音乐列表页自动化测试4. 上传音乐模块自动化测试5. 以上所有测试用例集成测试套件项目测试亮点web 自动化测试实战就通过测试自己的 onlinemusicserver 音乐服务器项目进行测试&#xff0c;通过 sel…

冰冰学习笔记:多线程

欢迎各位大佬光临本文章&#xff01;&#xff01;&#xff01; 还请各位大佬提出宝贵的意见&#xff0c;如发现文章错误请联系冰冰&#xff0c;冰冰一定会虚心接受&#xff0c;及时改正。 本系列文章为冰冰学习编程的学习笔记&#xff0c;如果对您也有帮助&#xff0c;还请各位…