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

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

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

    这里种类是Apple说明没有经过转译

  3. 部署mysql和redis

    bash 复制代码
    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
    bash 复制代码
    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表语句

    shell 复制代码
    # 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(我用的这个可视化软件,它可以连接很多种数据库)

  6. 项目后端

    shell 复制代码
    # 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 阿里

  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

    shell 复制代码
    # 安装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

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

shell 复制代码
git clone git@gitee.com: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中存储的手机号和验证码是为了登录时的校验功能
java 复制代码
@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<UserMapper, User> implements IUserService
    我们观察发现 UserServiceImpl类继承了 ServiceImpl<T,T>, 同时指定了两个泛型UserMapper,和User,我们去entity下的user类可以发现,其中指明了该实体类对应的表名,这就是二者之间的联系.
  • 代码优化:这里我优化了一下, 应当先检查当前手机号和session中存储的手机号是否一致
  • 注意代码命名规则,:比如session中的code命名为cacheCode,;类名:驼峰命名法+ 首字母大写, 方法名驼峰命名法+首字母小写
  • api设计规则:设计的类名称为IShopTypeService,对应的路径名称为shop-type
  • 写代码流程:先把思路捋清楚,比如1.2.3.4.每一步干什么,然后再写代码
java 复制代码
@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(个人信息界面)的代码发现会发送如下请求
      其中有一个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
java 复制代码
 @Override
    public Result me() {
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
java 复制代码
@Data
public class UserDTO {
    private Long id;
    private String nickName;
    private String icon;
}
java 复制代码
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();
    }
}
java 复制代码
@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帮我们管理?
java 复制代码
 @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);
    }
java 复制代码
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();
    }
}
java 复制代码
@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, 谁先注册谁先执行, 或者我们手动更改拦截器的优先级(优先级越小越先执行)
java 复制代码
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();
    }
}
java 复制代码
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
}
java 复制代码
@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);
    }
}
相关推荐
YOU OU5 小时前
Redis基础常识与命令
数据库·redis·缓存
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
a4493153627 小时前
iPhone Face ID 故障的分层诊断——原深感系统与主板加密配对机制
macos·ios·电脑·cocoa·iphone
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
白玉cfc7 小时前
【iOS】内存五大分区
macos·ios·cocoa
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
吳所畏惧9 小时前
宝塔面板Redis密码修改指南:SSH命令修改 vs 面板UI界面修改,哪个更靠谱?
运维·服务器·数据库·redis·缓存·ssh
赵丙双10 小时前
Redis 概率相关的数据类型
redis
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json