redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)

redis黑马点评项目学习笔记 mac m1pro windows 含项目配置教学

  • mac M1pro环境配置
  • windows11 wsl2 ubuntu 环境配置
  • 一.短信登录
    • 1. 1发送验证码
    • 1.2短信登录+注册
    • 1.3登录校验拦截器
    • 补缺Cookie Session Token
    • 1.4基于redis+token认证实现短信登陆
    • 1.5完善token认证的刷新机制
  • 2.商户查询缓存
    • 2.1添加商户缓存(到redis中)

mac M1pro环境配置

  1. 视频里用的是虚拟机,但我不想配虚拟机,太臃肿了,我选择docker这里我用的是OrbStack,这款软件使用的是rust编写,性能占用低(相比于dockerDesktop)
    brew insall orbstack或者去官网下载

  2. docker拉取redis 和 mysql镜像,注意常规镜像pull下来run后使用的是rosetta转译,性能会下降.
    因此我们拉取arm64v8/mysql和arm64v8/redis这两个镜像
    pull docker pull arm64v8/mysql
    pull docker pull arm64v8/redis
    redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)_第1张图片
    这里种类是Apple说明没有经过转译在这里插入图片描述

  3. 部署mysql和redis

    docker run -p 3306:3306 \
     --name mysql \
     -v mysql_data:/var/lib/mysql \
     -v mysql_conf:/etc/mysql/conf.d \
     --privileged=true \
     -e MYSQL_ROOT_PASSWORD=123456 \
     -d arm64v8/mysql
    
    docker run -p 6379:6379 \
    --name redis \
    -v redis_data:/data \
    -v redis_conf:/etc/redis/redis.conf \
    -d arm64v8/redis \
    redis-server /etc/redis/redis.conf
    

    使用docker ps查看是否成功
    在这里插入图片描述

  4. 进入mysql数据库中导入sql表并运行sql表语句

    # 1.拷贝SQL文件到mysql容器中(在容器外,自己的sql文件所在位置,mysql是容器名)
    docker cp yyy.sql mysql:/hmdp.sql
    
    # 2. 创建数据库(第一个mysql是容器名,第二个mysql是程序名称 也可以/bin/bash或者bash)
    docker exec -it mysql mysql  -uroot -p123456  
    mysql> create database hmdp;# 创建黑马点评数据库
    mysql> use hmdp; # 使用黑马点评数据库
    
    # 3.登陆控制台执行source 命令,执行sql文件
    mysql> source hmdp.sql
    
  5. 进入Datagrip软件查看一下redis 和 mysql(我用的这个可视化软件,它可以连接很多种数据库)
    redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)_第2张图片

  6. 项目后端

    # 1.clone
    git clone https://gitee.com/chiroua/black-horse-review.git
    # 2.切换分支, init分支就是项目最开始的代码
    git checkout init
    # 3.修改application.yaml中mysql和redis的ip,端口,密码等,这里我redis没有设置密码直接注释了.
    
    • 因为使用的是mysql8的版本
      还要将数据配置文件里driver-class-name: com.mysql.jdbc.Driver修改为如下driver-class-name: com.mysql.cj.jdbc.Driver;否则启动项目有报错
    • 修改idea默认的maven, idea自带一个叫已捆绑(Maven3)的maven,默认的所在路径为/.m2,settings和repo都在这个目录下,但是我没试过,我这里用的自己的maven仓库,记得修改一下maven仓库镜像地址,搜索关键词 maven 阿里
      在这里插入图片描述
      redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)_第3张图片
  7. 项目前端

  • brew install nginx
  • /opt/homebrew/Cellar/nginx/1.25.3/下的html夹替换成src/main/resources/nginx-1.18.0/下的html文件夹
  • /opt/homebrew/etc/nginx/nginx.conf替换成src/main/resources/nginx-1.18.0/conf/nginx.conf
    接下来重新加载nginx配置文件sudo nginx -s reload
  • 启动nginxsudo nginx
  • ps -ef | grep nginx查看nginx是否成功启动
  1. nginx成功启动后,我们启动后端,访问本地8080端口即可成功运行

windows11 wsl2 ubuntu 环境配置

  1. mysql

    # 安装mysql
    su root
    apt update
    sudo apt-get install mysql-server
    sudo service mysql start#一般就直接启动了不需要这一步
    mysql
    ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';# 修改root用户密码
    exit
    sudo mysql_secure_installation # 使用脚本删除匿名用户和匿名用户访问的数据库
    
    ## 接下来 执行sql文件,构建数据库
    cp /mnt/c/.....hmdp.sql /tmp/ #每个人不一样,可以参考我复制到/tmp目录下
    mysql -u root -p
    CREATE DATABASE hmdp;
    use hmdp;
    source /tmp/hmdp.sql
    

    更多操作查看此博客

  2. redis

    # 我直接安装的 没设置密码
    su root
    apt update
    sudo apt-get install redis-server
    # 执行redis-cli查看redis正常启动即可
    
  3. datagrip连接redis和mysql, 其中redis指定不需要用户和密码即可

  4. git clone [email protected]:chiroua/black-horse-review.git
    # 修改maven仓库的settings 将源设置成aliyun,这里我直接用idea自带的maven仓库了
    # 修改applicati.yml配置文件中mysql 和 redis相关配置
    

    因为使用的是mysql8的版本
    还要将数据配置文件里driver-class-name: com.mysql.jdbc.Driver修改为如下driver-class-name: com.mysql.cj.jdbc.Driver;否则启动项目有报错

  5. 前端也有一处问题 niginx目录下打开终端start nginx.exe后访问8080无效,
    查看nginx目录下的logs/error.log发现2024/01/04 21:58:41 [emerg] 37092#35300: CreateDirectory() "C:\Users\gx\Desktop\hmdp\black-horse-review\src\main\resources\nginx-1.18.0/temp/client_body_temp" failed (3: The system cannot find the path specified)
    我们手动在logs的同级目录下创建一个temp/client_body_temp即可

一.短信登录

1. 1发送验证码

  • 业务思路: 基本的校验->验证码和手机号保存在session中(不明白session token cookie去学),利用RandomUtil工具类生成验证码即可;session中存储的手机号和验证码是为了登录时的校验功能
@Override
    public Result sendCode(String phone, HttpSession session) {
        //获得用户手机号
        //1.校验手机号正确性
        if (RegexUtils.isPhoneInvalid(phone))
            return Result.fail("手机号格式不正确");
        session.setAttribute("phone", phone);
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码
        session.setAttribute("code", code);
        //4.发送验证码
        log.debug("发送短信验证码成功, 验证码:{}", code);
        //5.返回ok
        return Result.ok();
    }

1.2短信登录+注册

  • 业务思路:controller 中 return UserService接口中的方法, 具体实现在实现类UserServiceImpl中去重写即可;基本的参数校验->从session中取出验证码和手机号->判断手机号前后是否一致,判断验证码是否正确,判断用户是否存在,不存在则注册一个保存到数据库中
  • 说在前面: 本项目使用了mybatis-plus, 直接的好处是可以直接用save(), query等api, 原因是因为
    public class UserServiceImpl extends ServiceImpl implements IUserService
    我们观察发现 UserServiceImpl类继承了 ServiceImpl, 同时指定了两个泛型UserMapper,和User,我们去entity下的user类可以发现,其中指明了该实体类对应的表名,这就是二者之间的联系.redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)_第4张图片
  • 代码优化:这里我优化了一下, 应当先检查当前手机号和session中存储的手机号是否一致
  • 注意代码命名规则,:比如session中的code命名为cacheCode,;类名:驼峰命名法+ 首字母大写, 方法名驼峰命名法+首字母小写
  • api设计规则:设计的类名称为IShopTypeService,对应的路径名称为shop-type
  • 写代码流程:先把思路捋清楚,比如1.2.3.4.每一步干什么,然后再写代码
@Override
	    public Result login(LoginFormDTO loginForm, HttpSession session) {
	        //1.校验手机号格式是否正确
	        String phone = loginForm.getPhone();
	        if(RegexUtils.isPhoneInvalid(phone)) return Result.fail("手机号格式不正确!");//这里不知道为啥,不用return true代表格式不正确
	        //2.校验手机号是否和session里面的一致
	        String cachePhone = (String) session.getAttribute("phone");
	        if (!cachePhone.equals(phone)) return Result.fail("前后手机号不匹配!");
	        //3.校验验证码
	        Object cacheCode = session.getAttribute("code");
	        String code = loginForm.getCode();
	        if (cacheCode == null || !cacheCode.toString().equals(code)) return Result.fail("验证码错误!");
	        //4.判断用户是否存在
	        User user = query().eq("phone", phone).one();
	        //5.不存在的话直接创建一个用户
	        if (user == null) user = createUserWithPhone(phone);
	        //6.保存用户信息到session中
	        session.setAttribute("user", user);
	        return Result.ok();
	    }
	//注册
	    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;
	    }

1.3登录校验拦截器

  • 业务思路:
    • 设计拦截器的原因: 点评的不少业务都需要验证当前是否是登录状态,比如我们观察前端info.html(个人信息界面)的代码发现会发送如下请求redis黑马点评项目启动指南(含mac m1pro | windows11 wsl2 ubuntu环境配置 持续更新中~)_第5张图片
      其中有一个Cookie, 如果每个业务都要从http request的session中取的话会很麻烦,因此我们设置一个登录拦截器LoginInterceptor类, 所有的业务都会先经过这个类最后到达controller, 我们在这个类中将当前session中的user属性存储到本地线程中,要用的时候直接从本地线程UserHolder类(封装了TreadLocal类对象)中取就行.
    • 防止用户隐私泄露: 我们session中一开始存的是完整的user对象(包含了user的密码等个人隐私), 我们封装一个UserDto类, 将user中的信息拷贝到userDto中, session中存userDto就行, 后续的UserHolder等cache类从session中拿到的数据也就是userDto了. 但是我们createUser方法往数据库中存储的时候还是存储完整的user对象哦
    • 实现WebMvcConfigurer类中的addInterceptor方法, 并设置哪些请求路径不需要被拦截
  • 除了拦截器外还可以设计过滤器
  • 缺: 什么是TreadLocal内存泄露,为什么afterCompletion后要removeUser(); 什么是session cookie token
  • mac idea 实现方法快捷键 ^ + o
 @Override
    public Result me() {
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}
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.用户是否存在,不存在返回false,并设置状态码
        if (user == null){
            response.setStatus(401);
            return false;
        }
        //3.存在就将当前用户存储在ThreadLocal中
        UserHolder.saveUser((UserDTO) user);
        return true;
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //移除用户,防止内存泄漏
        UserHolder.removeUser();
    }
}
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(// 匿名对象 stream coding
                "/user/code",
                "/voucher/**",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "/upload/**",
                "/user/login"
        );
    }
}

补缺Cookie Session Token

因为Http是一种无状态的协议,为了记住这种状态, 引入了 session cookie token

  • Cookie: 存储在前端, 数据的一种载体, 后端可以将数据包装在Cookie中发送给客户端端, 比如将session放在cookie中送到客户端, cookie跟随HTTP的每个请求发送出去
  • Session: 存储在后端
  • Token: 在很多地方都会用到, 只是一个通用的名词;诞生在服务器这端, 但保存在浏览器这边, 由客户端主导一切, 可以存放在Cookie或者Storage中, jwt即json web token是一种特殊的token,或者说token的一种
  • 总结: cookie是载体,而token和session则是靠它实现的验证机制,本质就是cookie上携带的字符串; session和token是两种认证机制。cookie和localstorage等是存储session id或token的载体

1.4基于redis+token认证实现短信登陆

  • 业务思路: 这里采用token认证机制, 不再使用session了, 因为如果是tomcat服务器集群,session存储在不同的tomcat服务器中, 假如小明的session存储在A服务器中, 第二次小明发送请求的时候经过负载均衡请求到达B服务器, 那么B服务器没有小明的session,小明又要重新登录; 但是如果让多个服务器之间数据同步的话会造成数据冗余,同时同步数据也需要一定的时间,综上我们选择redis来存储数据
    • 用redis存储code, phone作为其key
    • 用redis存储user信息, token作为其keytoken的本质是一种验证机制, 这里token作为当前用户个人信息的key!!
    • springboot内置的tomcat会帮我们维护session,这里我们自己维护redis的时候注意数据的丢失, 因为不可能让数据一直存储在redis中
    • 具体实现业务代码的时候注意自己new的对象spring不会帮我们管理的
  • 缺: 什么样的对象可以让spring帮我们管理?
 @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //获得用户手机号
        //1.校验手机号正确性
        if (RegexUtils.isPhoneInvalid(phone))
            return Result.fail("手机号格式不正确");
        //session.setAttribute("phone", phone); 使用redis后我还没想好它的key是啥
        //2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码到redis中并设置缓存时间
        stringRedisTemplate.opsForValue().set(LOGIN_USER_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);//stream流
        //4.发送验证码
        log.debug("发送短信验证码成功, 验证码:{}", code);
        //5.返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.校验手机号格式是否正确
        String phone = loginForm.getPhone();
        if(RegexUtils.isPhoneInvalid(phone)) return Result.fail("手机号格式不正确!");//这里不知道为啥,不用return true代表格式不正确
//        //2.校验手机号是否和session里面的一致
//        String cachePhone = (String) session.getAttribute("phone");
//        if (!cachePhone.equals(phone)) return Result.fail("前后手机号不匹配!");
        //3.校验验证码
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_USER_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) return Result.fail("验证码错误!");
        //4.判断用户是否存在
        User user = query().eq("phone", phone).one();
        //5.不存在的话直接创建一个用户
        if (user == null) user = createUserWithPhone(phone);
        //6.保存用户信息到redis中, token作为key并设置缓存时间
        String token = UUID.randomUUID().toString(true);
        String tokenKey = LOGIN_USER_KEY +  token;
        Map<String, Object> userMap = BeanUtil.beanToMap(user, new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        //设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
        //7.返回token
        return Result.ok(token);
    }
public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;//用来接住外部传过来的StringRedisTemplate
    public LoginInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获得token, 判断token是否为空
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) return false;
        //2.通过token获取redis中的用户信息,并判断同户是否存在,不存在返回false,并设置状态码
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (userMap.isEmpty()){//及时为空也会被包装成一个对象,因此这里不能判断null而是isEmpty
            response.setStatus(401);
            return false;
        }
        //3.将map变成userDto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //4.存在就将当前用户存储在ThreadLocal中
        UserHolder.saveUser(userDTO);
        //5.每拦截一次, redis中存储的用户信息刷新一次
        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();
    }
}
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;//该类中有一个@Configuration注解说明该类可以由spring来帮我们管理对象
    @Override
    public void addInterceptors(InterceptorRegistry registry) {//再该类中让spring帮我们管理对象然后传给LoginInterceptor
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)).excludePathPatterns(// 匿名对象 stream coding
                "/user/code",
                "/voucher/**",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "/upload/**",
                "/user/login"
        );
    }
}

1.5完善token认证的刷新机制

  • 业务思路: 因为之前的一层拦截器只能拦截部分请求, 假如用户一直在浏览index页面30min,那么再访问需要user信息的页面的时候就会需要重新登录, 因此这里再多设置一层拦截器拦截所有请求
    • 第一层拦截器: 拦截所有请求 + 将当前用户信息存储到ThreadLocal中;判断是否有用户的任务交给下一层,本层的任务是存储user到当前thread+刷新token(不判断token是否为空, 若无token代表无user信息, 无user信息的情况给第二层拦截器处理)
    • 第二层拦截器: 拦截部分需要user信息的页面的请求, 若从ThreadLocal中取出来的数据为空则直接return false 即可
    • 注意注册拦截器的时候拦截器有优先级顺序, 默认情况下都是0, 谁先注册谁先执行, 或者我们手动更改拦截器的优先级(优先级越小越先执行)
public class RefreshTokenInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;//用来接住外部传过来的StringRedisTemplate
    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获得token, 判断token是否为空
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            return true;//均return true, 判断是否有用户的任务交给下一层,本层的任务是存储user到当前thread+刷新token
        }
        //2.通过token获取redis中的用户信息,并判断同户是否存在,不存在返回false,并设置状态码
        String tokenKey = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(tokenKey);
        if (userMap.isEmpty()){//及时为空也会被包装成一个对象,因此这里不能判断null而是isEmpty
            return true;
        }
        //3.将map变成userDto
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //4.存在就将当前用户存储在ThreadLocal中
        UserHolder.saveUser(userDTO);
        //5.每拦截一次, redis中存储的用户信息刷新一次
        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();
    }
}
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.从UserHolder获得用户
        UserDTO userDto = UserHolder.getUser();
        //2.判断是否有user用户
        if (userDto == null) {
            response.setStatus(401);
            return false;
        }
        return true;
    }
    //不用重写afterCompletion方法, 第一层拦截器负责重写
    拦截器执行顺序:
    	preHandle: 拦截器1 -> 拦截器2
    	afterCompletion: 拦截器2 - > 拦截器1
}
@Configuration
public class MyConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;//该类中有一个@Configuration注解说明该类可以由spring来帮我们管理对象
    @Override
    public void addInterceptors(InterceptorRegistry registry) {//再该类中让spring帮我们管理对象然后传给LoginInterceptor
        registry.addInterceptor(new LoginInterceptor()).excludePathPatterns(// 匿名对象 stream coding
                "/user/code",
                "/voucher/**",
                "/blog/hot",
                "/shop/**",
                "/shop-type/**",
                "/upload/**",
                "/user/login"
        ).order(1);
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);
    }
}

2.商户查询缓存

从本节开始我会说明这个业务的原因和业务的思路是如何的

2.1添加商户缓存(到redis中)

  • 业务原因
  • 业务思路
    查看网络请求时间验证是否快了很多

你可能感兴趣的:(redis,macos,ubuntu,java)