【easy视频 | day01】项目了解 + 登录注册 + 使用 token 作为客户端请求令牌
文章目录
- 前言
- 完成任务
- 1. 项目了解
- 2. 登录注册
- 2.1 创建数据表
- 2.2 验证码
- 如果使用 Session 存储验证码:
- 不用 Session 存储验证码,用 Redis 会有什么问题?
- 2.3 注册功能
- 2.4 登录功能
- 2.5 自动登录
- 2.6 退出登录
- 总结

前言
本项目非原创,我只是个小小白,跟随 b 站脚步,找到老罗的这个项目,视频来源于:
高仿B站(单服务版) springboot项目实战 easylive
本人不分享项目源码,支持项目付费!!!
完成任务
1. 项目了解
这是一个仿 b 站的项目,页面分为客户端和后台管理端,可发布视频,在线互动(评论、点赞、投币、发弹幕),后台管理对视频进行审核。
项目采用微服务架构构建,系统架构图:
- 正常线上部署会有 nginx 集群,本地上不存在 nginx 集群,直接就是:前端 ——> 网关。
- 有很多个服务,服务跑起来后,每个服务有一个端口,但是这些服务的端口都不对外,对外的端口只有一个,就是 gateway 的端口。
- 服务与服务之间存在相互调用。
- Nacos 实现服务发现注册、作为配置中心。微服务部署加上 nginx 集群的情况下,将每个的配置抽离出来,放在一个配置中心进行配置,修改的时候不用一个个地去改,只需要修改配置中心。
2. 登录注册
2.1 创建数据表
根据该系统的用户特点,创建用户数据表。
为什么用邮箱发送验证码 ,而不使用手机号 ?
手机号发送验证码需要做财务报表,需要付费;邮箱目前接收验证码不需要付费。对于一个小型的项目,使用邮箱验证码就足够了。
注意:创建表字段后,某些字段可设置不为 NULL、主键、默认值;还需要设计必要的索引。
2.2 验证码
如果使用 Session 存储验证码:
后端将生成的验证码 code 保存到 Session 会话中,当前端调用获取验证码的该接口时,响应头 Set-Cookie 中会包含 JSESSIONID :
如果此时,在进行 register 注册时,校验所填的验证码是否与刚刚存入 Session 的验证码相同,发起的请求中,也会携带这个 JSESSIONID:
JSESSIONID 就是一个会话的标记,使用 Session 会返回一个 JSESSIONID 来标记这个会话,下一次请求时,就会携带这个 JSESSIONID,判断这两个请求是否是同一个会话发出来的(会话可认为是一个浏览器,或者说新开的一个浏览器页面)。如果是同一个会话,校验验证码时就会获取存入当前会话 Session 中的验证码与前端填入的验证码进行比较;如果不是同一个会话,那么获取验证码请求和注册请求都不是同一个 JSESSIONID,那么就不可能从 Session 中获取到刚刚存入的验证码。
不用 Session 存储验证码,用 Redis 会有什么问题?
这里请求验证码时,将验证码保存在 Redis 中,设置过期时间为 10 分钟。
注册时,获取 Redis 中的验证码与前端传入的验证码进行比对:
比对成功,返回 true;不成功,返回 false。
但是,
这里有一个问题,当用户 A 获取验证码 a 后,将 a 存入 Redis;此时,另外一个用户 B 也获取验证码,得到验证码 b ,Redis 会将最新获得的验证码 b 存入,遮蔽掉先前存入的验证码 a。用户 A 输入他获得的验证码 a 进行校验,后端就会从 Redis 中取出最新获得的 b 与之校验,用户 A 就发现不匹配,就会觉得疑惑:明明输入是正确的,为什么就是不匹配呢?
原因就是 Redis 中存储验证码的 key 是定死了的,就是 “checkCode”,全站所有人都用的这一个 key,每个人请求一下获取验证码的接口,这个 key 的值就更新了。它不是一个用户的唯一标识。
所以说,直接这样使用 Redis 来存储验证码,是不可行的。
但就是要用 Redis 来存储验证码,该怎么实现呢 ?
让每个用户请求验证码时,存入 Redis 的 key 是唯一的,能唯一标识这个用户。可以随机生成一个随机数作为每个用户验证码的 key,返回客户端数据时,将对应的 key 也和验证码图片一起返回。
Redis 保存、获取和清除验证码的方法:
UUID:是 Java 中一个主要为对象生成一个唯一标识符的类
randomUUTD():生成一个随机的 UUID 对象,由 128 位数字组成,通常由 32 位十六进制表示,中间用破折号连接。
controller 层调用:
2.3 注册功能
先看 contoller 层中的参数:
这里直接使用注解对方法参数进行校验。要使用 @NotEmpty、@Email、@Size 这些注解,需要在 Controller 类上加上 @Validated 注解。
@NotEmpty: 确保参数不为空。
@Email: 确保字符串是一个有效的电子邮件地址。
@Size: 确保字符串的长度不超过指定的最大值。
@Pattern: 确保字符串符合指定的正则表达式。
密码的正则表达式通常是,至少包含一个数字和一个英文字母,密码长度在 8~18 之间,可以包含特殊字符:
public static final String REGEX_PASSWORD = "^(?=.*\\d)(?=.*[a-zA-Z])[\\da-zA-Z~!@#$%^&*_]{8,18}$";
service 层实现:
用户 ID 使用工具类方法中,随机生成字符串来实现:
第一个生成指定长度的字符串,可以包含字母和数字;第二个生成指定长度的字符串,不能包含字母。用户 ID 不需要字母,使用的是第二个。
设置用户密码,使用 MD5 加密:
这里直接使用 Apache 库中的 DigestUtils 类,计算给定字符串的 MD5 值。
2.4 登录功能
controller 层:
需要获取用户的 IP 地址,通过 HttpServletRequest 来获取用户 ip:
protected String getIpAddr() {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String ip = request.getHeader("x-forwarded-for");
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
// 多次反向代理后会有多个ip值,第一个ip才是真实ip
if (ip.indexOf(",") != -1) {
ip = ip.split(",")[0];
}
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_CLIENT_IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
sevice 层:
之前是使用 Session 来存储用户登录后的标识,在这里使用 token 来作为用户标识,以便于用户登录后,无需再重新登录,可以继续访问。
这里返回的 TokenUserInfoDto 中除了用户相关信息,还有 token 及其过期时间:
@JsonIgnoreProperties(ignoreUnknown = true)注解:无论传入的JSON数据中是否包含类中未定义的属性,这些属性都会被忽略,而不会导致反序列化失败。在这里非常有用,如果说这个类中添加了其它属性,这个注解能保证数据的反序列化不会失败。
Redis 中存储 token 和用户信息,设置 token 值及其过期时间:
还有一步,将这个 token 手动地存入到 Cookie 中:
其实,获得 tokenUserInfoDto 后可以直接返回的,返回给前端后,前端去存放这个 token。但是为了保险,我们也可以像 Session 一样,Session 会在 Cookie 中存放一个 JSESSIONID,这其实就是服务端的工作。我们也可以将 token 存放在 Session 中:
成功访问 login 接口后,可看到 Cookie 中会有 token :
这样的话,当前用户再次访问其它接口,就不需要前端参与,Cookie 会自动将 token 携带过来。
如果服务端还要获取 token 的话,直接使用 Response 不太方便,需要循环寻找,因为 Cookie 中有很多东西,获取 Cookie 得到的是一个数组,还需要遍历数组才能拿到 token。 但是,前端会从 Cookie 里把 token 解析出来,然后放到 head 头中带过来,这时候,在后端,就可以通过 @RequestHeader(“token”) 拿到 token。
登录最后,不能忘记还要清理掉 Redis 中的 验证码,token 可清理也可不清理:
2.5 自动登录
用户第一次成功登录后,Cookie 中会存在一个 token,设置的有效期为 7 天,7 天以内打开这个页面,应该是自动登录的:
如果是 7 天以内,再次登录,还需要再此执行 saveTokenInfo,重新生成一个 token,并重新开始计算时间:
并将这个重新生成的 token 存入 Cookie 中。
2.6 退出登录
退出登录,需要做的就是清理掉 Cookie 和 Redis 中的 token:
总结
一般来说,一个项目的登录注册功能,往往是这个项目动手写代码时第一个要做的功能。这部分功能包括验证码获取、注册、登录、自动登录、退出登录等。这段功能理解起来并不复杂,重点就是对 Redis 数据的操作、细节的把控、以及代码书写规范。
在老罗的这个项目里,通过登录注册功能就可以学到很多以前不太了解的知识:尤其是 token 的使用,Session 的理解。