视频里用的是虚拟机,但我不想配虚拟机,太臃肿了,我选择docker这里我用的是OrbStack,这款软件使用的是rust编写,性能占用低(相比于dockerDesktop)brew insall orbstack
或者去官网下载
docker拉取redis 和 mysql镜像,注意常规镜像pull下来run后使用的是rosetta转译,性能会下降.
因此我们拉取arm64v8/mysql和arm64v8/redis这两个镜像
pull docker pull arm64v8/mysql
pull docker pull arm64v8/redis
这里种类是Apple说明没有经过转译
部署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
进入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
项目后端
# 1.clone
git clone https://gitee.com/chiroua/black-horse-review.git
# 2.切换分支, init分支就是项目最开始的代码
git checkout init
# 3.修改application.yaml中mysql和redis的ip,端口,密码等,这里我redis没有设置密码直接注释了.
项目前端
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
sudo nginx -s reload
sudo nginx
ps -ef | grep nginx
查看nginx是否成功启动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
更多操作查看此博客
redis
# 我直接安装的 没设置密码
su root
apt update
sudo apt-get install redis-server
# 执行redis-cli查看redis正常启动即可
datagrip连接redis和mysql, 其中redis指定不需要用户和密码即可
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;否则启动项目有报错
前端也有一处问题 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即可
@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();
}
接口
中的方法, 具体实现在实现类UserServiceImpl中去重写即可;基本的参数校验->从session中取出验证码和手机号->判断手机号前后是否一致,判断验证码是否正确,判断用户是否存在,不存在则注册一个保存到数据库中public class UserServiceImpl extends ServiceImpl implements IUserService
@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;
}
@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"
);
}
}
因为Http是一种无状态的协议,为了记住这种状态, 引入了 session cookie token
json web token
是一种特殊的token,或者说token的一种session和token是两种认证机制
。cookie和localstorage等是存储session id或token的载体token的本质是一种验证机制, 这里token作为当前用户个人信息的key!!
@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"
);
}
}
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);
}
}
从本节开始我会说明这个业务的原因和业务的思路是如何的