课程笔记:注册邮箱验证
一、概述
从本小节开始,将学习如何进行注册邮箱验证。主要任务是给项目配置一个公共邮箱(可自己注册或由公司提供),用于向用户发送验证码,帮助用户完成注册流程。课程中以QQ邮箱为例,介绍在Spring Boot中发送邮件(包括正文、标题等)的方法。用户收到验证码后填写回来,与系统发送的进行比对,一致则验证成功,反之则失败。
二、注册邮箱验证的两种主流方式
验证码验证(国内常用,课程采用方式)
- 流程:用户填写邮箱后点击发送验证码按钮,系统向该邮箱发送随机生成的验证码。用户收到后填写回来,系统进行比对校验。
- 原理:验证码生成、校验及重复校验。若后续需进行短信验证,只需将发邮件改为发短信,其他校验原理一致。
唯一链接验证(国外常用)
- 原理:给用户邮箱发送一个带有长链接的邮件,链接访问某接口并带参数(含个人信息)。每个邮箱的链接唯一,能访问该链接说明用户是邮箱主人。
- 流程:用户点击唯一链接后,系统验证通过,确认是真实用户。
三、课程采用的验证码验证流程
- 用户在网页点击发送验证码按钮,系统检查该邮箱是否已注册。
- 未注册:将用户信息放入缓存,向邮箱发送验证码。
- 已注册:拒绝注册(不允许两个用户使用同一邮箱)。
- 用户在邮箱中收到验证码,复制到网站继续注册,提交验证码。
- 系统在缓存中校验验证码是否正确、是否过期。
- 不正确或过期:拒绝注册。
- 正确:用户注册成功。
课程笔记:邮件发送功能的开发与完善
一、邮箱配置(以 QQ 邮箱为例)
- 进入邮箱设置:点击邮箱界面的“设置”选项。
- 找到第三方服务:在设置页面的“常规”选项下,找到“第三方服务”相关设置。
- 开启服务并获取授权码:
- 如果服务未开启,点击“开启服务”。
- 开启后,系统会生成一个授权码,该授权码专门用于程序验证,不同于普通登录密码。
- 授权码相当于独立密码,用于提高账号安全性,需妥善保存,后续在程序配置中会用到。
- 其他邮箱配置类似:如 163 邮箱等,配置过程大致相同,找到类似“第三方服务”或“授权码”相关设置,进行相应配置。
二、给 User 类添加字段
- 进入 mybatis 配置类:找到对应的配置类,注释掉其他表,仅保留 User 表。
- 更新数据库表:在数据库中找到对应的 User 表,添加 email 字段,类型为 varchar(100),注释为“邮箱地址”。
- 重新生成旧类:运行 mybatis generator,生成新的 User 类。
- 备份自定义代码:提前复制保存 mapper 中自动生成代码后添加的自定义方法和 SQL 语句,避免重新生成后被覆盖。
- 恢复自定义代码:生成新类后,将备份的代码粘贴回来,并引入相关包。
三、引入邮件发送依赖
在项目中添加 spring-boot-starter-mail 依赖,指定版本为 2.3.4.RELEASE。
四、开发发送邮件接口
在 UserController 中添加接口:
- 方法头:发送邮件,路径为 sendEmail,参数为 emailaddress(邮件地址)。
- 返回格式匹配。
实现方法:
- 校验邮件地址是否有效:
- 使用 utu 包中的 InternetAddress 类的 validate 方法。
- 创建 Email 工具类,新增 isValidEmailAddress 方法,返回 boolean 值。
- 在该方法中,使用 try-catch 包裹 InternetAddress(email).validate(),若无异常返回 true,否则返回 false。
- 使用该方法检查传入的 emailaddress,若无效,返回错误响应(枚举值为 INVALID_EMAIL,中文为“非法的邮件地址”)。
- 检查是否已注册:若邮件地址有效,进一步检查是否已注册。
- 发送邮件:若以上校验均通过,执行邮件发送逻辑。
- 校验邮件地址是否有效:
五、检查邮件地址是否已注册
在 User Service 中新增方法:
- 方法名:
public boolean checkEmailRegistered(String emailaddress)
- 使用 UserMapper 的
selectOneByEmailaddress
方法,根据传入的邮件地址查询用户。 - 若返回的 User 对象不为空,说明邮件地址已被注册,返回 false;否则返回 true。
- 方法名:
在 UserMapper 中补充 SQL 语句:
- 方法名:
selectOneByEmailaddress(String emailaddress)
- SQL 语句:
SELECT * FROM imcomo_user WHERE emailaddress = #{emailaddress} LIMIT 1
- 方法名:
六、邮件发送逻辑实现
在 UserController 中调用检查方法:
- 在通过邮件地址合法性验证后,调用
userService.checkEmailRegistered(emailaddress)
。 - 若返回值为 false(邮件地址已被注册),返回错误响应(枚举值为 EMAIL_ALREADY_BEEN_REGISTERED,中文为“email 地址已被注册”)。
- 在通过邮件地址合法性验证后,调用
创建 EmailService 接口及实现类:
- 接口方法:
void sendSimpleMessage(String to, String subject, String text)
- 实现类:
EmailServiceImpl
,使用 JavaMailSender 发送邮件。
- 接口方法:
配置邮件相关属性:
- 在配置文件中设置邮件主机、端口号、用户名、授权码、编码及验证相关属性。
完善邮件发送方法:
- 在
sendSimpleMessage
方法中,创建 SimpleMailMessage 对象,设置发件人(从常量中获取)、收件人、主题和正文。 - 使用 JavaMailSender 的 send 方法发送邮件。
- 在
在 UserController 中调用邮件发送服务:
- 引入 EmailService。
- 调用
emailService.sendSimpleMessage
,传入邮件地址、主题(从常量中获取)和验证码相关正文。
七、生成随机验证码
在 Email 工具类中新增方法:
- 方法名:
public static String generateVerificationCode()
- 创建包含数字、大写字母、小写字母的字符列表。
- 使用
Collections.shuffle
打乱列表顺序。 - 取列表前六位字符作为验证码。
- 返回生成的验证码字符串。
- 方法名:
测试验证码生成方法:编写测试方法,打印生成的验证码,验证其随机性和正确性。
八、限制重复发送邮件
引入 Redis 依赖:添加
redisson
依赖,用于操作 Redis。在 EmailService 中新增方法:
- 方法名:
public boolean saveEmailToRedis(String emailaddress, String verificationCode)
- 获取 Redis 客户端,连接到本地 Redis。
- 使用
getBucket
方法传入邮箱地址作为 key,获取对应的 bucket。 - 检查 bucket 中是否存在值:
- 不存在:使用
set
方法存入验证码,设置过期时间为 60 秒,单位为秒,返回 true。 - 存在:返回 false,表示 60 秒内已发送过邮件。
- 不存在:使用
- 方法名:
在 UserController 中调用并处理:
- 调用
emailService.saveEmailToRedis
,传入邮箱地址和验证码。 - 根据返回值判断:
- 返回 true:发送邮件,返回成功响应。
- 返回 false:返回错误响应,提示邮件已发送,请稍后再试。
- 调用
九、完善邮件发送内容
将生成的验证码添加到邮件正文内容中。
十、测试验证
- 测试非法邮件地址:发送格式错误的邮件地址,验证是否被拦截。
- 测试已注册邮箱:将邮箱地址设置为已注册的用户,验证是否被拦截。
- 测试重复发送:短时间内反复发送邮件,验证是否被拦截。
十一、总结
通过校验邮件地址合法性、检查是否已注册、限制重复发送以及使用 Redis 缓存验证码,完善了邮件发送接口的功能,提高了系统的安全性和稳定性。
课程笔记:注册接口升级与邮箱验证总结
一、注册接口升级
调整入参:
- 增加
emailaddress
(邮箱地址)和verificationcode
(验证码)两个参数。
- 增加
增加校验:
- 非空校验:对邮箱和验证码进行非空校验,新建异常类
26email不能为空
和27验证码不能为空
。 - 邮箱是否已注册校验:调用
checkEmailRegister
方法,判断邮箱是否已被注册。 - 邮箱和验证码匹配校验:在
emailService
中新增checkEmailAndCode
方法,通过 Redis 获取存储的验证码并与用户传入的验证码进行比对。
- 非空校验:对邮箱和验证码进行非空校验,新建异常类
数据存储:
- 注册成功时,将邮箱地址存入数据库用户表中。
二、校验流程
- 非空校验:确保用户名、密码、邮箱和验证码均不为空。
- 邮箱注册状态校验:防止重复注册。
- 验证码匹配校验:确保用户提供的验证码与 Redis 中存储的验证码一致。
三、验证码存储选择
- 为什么选择 Redis 而不是数据库:
- 临时性:验证码仅一次有效,注册成功后即无作用,无需长期存储。
- 过期机制:Redis 提供方便的过期时间设置,自动处理验证码过期,而数据库难以实现自动过期。
四、总结
通过升级注册接口,增加邮箱和验证码参数及相关校验,确保了用户注册流程的安全性和完整性。使用 Redis 存储验证码,利用其过期机制,有效防止了恶意重复注册和验证码滥用。整个流程包括发送验证码、校验邮箱是否已注册、验证码匹配校验以及最终的用户注册,各步骤紧密衔接,确保了用户注册信息的准确性和系统安全性。
课程笔记:登录状态的保存和验证
一、学习背景
在企业级权限认证中,登录是第一步,不仅需要验证用户身份,还要保存登录状态,以便用户后续操作时系统能够识别其身份。
二、HTTP 无状态特性
HTTP 协议是无状态的,意味着每个请求都是独立的,请求之间不携带状态信息。即使前一个请求通过验证,下一个请求仍需重新验证。这要求我们采取措施保存凭证,以解决无状态带来的问题。
三、凭证保存机制
1. 第一次登录验证
用户第一次登录时,需要提供用户名和密码等信息进行严格的身份验证。
2. Session 的创建
验证通过后,服务器为用户创建一个 Session,Session 是保存在服务器端的数据结构,用于跟踪用户状态。
3. Cookie 的作用
服务器返回给客户端一个 Cookie,其中包含 Session ID。客户端(如浏览器)在后续请求中携带此 Cookie,服务器通过 Session ID 找到对应的 Session,从而识别用户身份。
四、Session 和 Cookie 的工作流程
- 用户发送登录请求:包含用户名和密码。
- 服务器验证并创建 Session:验证通过后,生成 Session 并返回包含 Session ID 的 Cookie。
- 客户端保存 Cookie:浏览器保存 Cookie,在后续请求中自动携带。
- 服务器识别用户:通过 Cookie 中的 Session ID 找到对应的 Session,获取用户状态和数据。
五、特殊情况处理
如果客户端禁用 Cookie,可以采用 URL 重写的方式,在每个请求的 URL 后附加必要的身份参数,以便服务器进行校验。
六、总结
本小节介绍了登录状态保存和验证的基本原理和流程,重点讲解了 HTTP 无状态特性下如何通过 Session 和 Cookie 解决用户身份识别问题。下一个小节将深入探讨 Session 的细节和应用。
课程笔记:深入理解Session
一、Session 的安全性
用户空间的独立性:
- 每个用户的Session空间是独立的,即使使用相同的key存储数据,也不会相互影响。
- Tomcat会为每个用户分配独立的Session ID,每个Session ID对应自己的空间。
Session ID 的生成规则:
- 目标:保证唯一性,防止重复。
- 方法:通常结合随机数、当前时间(过滤大部分同时触发的情况)和JVM的ID值(区分不同服务器)。
二、Session 劫持与防护
Session 劫持:
- 概念:攻击者窃取用户的Session ID,冒充用户进行操作。
- 风险:可能导致用户数据泄露、被恶意操作等。
防护措施:
- HttpOnly 标记:服务器告知客户端,Session Cookie不允许通过前端代码读取,仅允许浏览器读取。
- Secure 标记:如果项目支持HTTPS,标记Session Cookie仅在HTTPS协议下传输。
三、Session 的缺点
扩展性差:
- 分布式环境下,多台机器需要同步和复制Session,处理复杂。
服务端存储压力:
- 用户量大时,存储大量Session数据(无论在内存、Redis还是数据库中)都会带来挑战。
四、实际操作演示
Postman 测试流程:
- 默认Cookie中包含Session ID,每个用户的Session ID唯一。
- 删除Cookie后请求,服务端会创建新的Session并要求设置Cookie。
- 设置Cookie后,后续请求携带该Session ID,服务端据此识别用户。
浏览器中的Session Cookie:
- 查看请求中的Cookie,包含JSession Cookie,代表用户唯一标识。
- 注意保护Session ID,防止泄露。
五、总结
Session用于保存用户状态,其安全性至关重要。通过理解Session的工作原理、Session ID的生成规则以及劫持与防护措施,可以更好地保障用户数据安全。Session存在扩展性和存储压力的缺点,后续将学习JWT来克服这些问题。
课程笔记:JWT 介绍与原理
一、JWT 的重要性
- 独特优势:相比 Session 和 Cookie,JWT 有自身的特点和优势。
- 项目应用:后续小节将把项目中原有的 Session 验证方式升级为 JWT。
二、JWT 的基本概念
- 定义:JWT(JSON Web Token)是一种流行的用于网站身份验证的认证方案。
- 官网:jwt.io ,提供调试工具用于编码和解码。
三、JWT 的组成
JWT 由三部分组成,每部分之间用英文半角句号(.)分隔:
Header(头部):
- 包含两个功能:签名算法和令牌类型。
- 示例:
{"alg": "HS256", "typ": "JWT"}
,其中alg
是签名算法,typ
是令牌类型。
Payload(消息体):
- 是 JWT 中最重要的部分,用于放置业务相关数据。
- 示例:
{"sub": "1234567890", "name": "John Doe", "iat": 1516239022}
,其中name
可改为用户名、身份、用户 ID 等。
Signature(签名):
- 用于验证消息是否被更改过,保证数据安全。
- 生成方式:将 Header 和 Payload 编码后,用 Secret 进行签名。
四、JWT 的生成与验证
生成:
- Header 和 Payload 分别经过 Base64 URL 编码。
- 使用 Secret 对编码后的 Header 和 Payload 进行签名,生成 Signature。
- 三部分组合成完整的 JWT。
验证:
- 服务端解码 JWT,验证 Signature 的有效性。
- 如果 Signature 无效,说明 JWT 被篡改。
五、Session 与 JWT 的对比
流程对比
Session 流程:
- 客户端发起 HTTP 请求,服务端创建 Session,生成唯一的 Session ID。
- 服务端要求客户端保存 Session ID 到 Cookie。
- 后续请求客户端携带 Cookie,服务端通过 Session ID 识别用户。
JWT 流程:
- 客户端发起 HTTP 请求,服务端校验用户名和密码。
- 校验通过后,服务端将用户信息转换为 JWT 并发送给客户端。
- 后续请求客户端携带 JWT,服务端解码 JWT 获取用户信息。
优缺点对比
Session:
- 优点:简单方便,适合小规模网站。
- 缺点:
- 扩展性差:用户量大时,分布式架构下存储和同步 Session 成本高。
- 需要存储数据:服务端需为每个用户开辟空间存储 Session。
JWT:
- 优点:
- 减少存储开销:服务端无需存储用户信息,直接将信息编码到 JWT 中。
- 可扩展性强:分布式架构下,各服务器可独立校验 JWT。
- 可用于交换信息:直接携带用户相关业务数据。
- 防止篡改:签名机制保证 JWT 的完整性。
- 缺点:
- 默认不加密:不适合保存敏感信息,如密码。
- 无法临时废止:一旦发出,无法主动使其失效,需设置合理过期时间。
- 有效期评估难:过长或过短的过期时间都会带来问题。
- 网络开销高:相比 Session ID,JWT 字符串较长。
- 优点:
六、总结
JWT 凭借其减少存储开销和良好的扩展性,在互联网公司中应用越来越广泛。尽管存在一些缺点,但通过合理设置和使用,可以充分发挥其优势。后续小节将进入 JWT 的实际开发应用。
课程笔记:项目实战 - 用户校验从Session升级为JWT
一、项目实战目标
将用户校验从传统的Session Cookie升级为JWT。
二、主要修改内容
- 登录接口升级:不再保存Session,改为在登录时生成JWT Token并返回给用户。
- 过滤器修改:包括用户过滤器和管理员过滤器,以适应JWT校验。
- 用户获取方式升级:调整从请求中获取用户信息的方式。
三、实战步骤
1. 引入JWT依赖
在pom.xml
中添加JWT依赖:
<dependency>
<groupId>com.off0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.14.0</version>
</dependency>
手动刷新项目,确保依赖正确下载。
2. 修改登录接口
(1) 引入必要的包和工具
确保项目中已引入JWT相关的包和工具。
(2) 来到UserController
找到login
接口,准备进行改造。
(3) 保留原有登录逻辑
保留用户名和密码的判空逻辑,以及通过用户名和密码获取用户信息的逻辑。
(4) 生成JWT Token
在原有登录逻辑的基础上,添加JWT Token的生成代码:
// 定义算法
Algorithm algorithm = Algorithm.HMAC256(Constant.JWT_KEY);
// 创建JWT
String token = JWT.create()
.withClaim(Constant.USERNAME, user.getUsername())
.withClaim(Constant.USER_ID, user.getId())
.withClaim(Constant.USER_ROW, user.getRow())
.withExpiresAt(new Date(System.currentTimeMillis() + Constant.JWT_EXPIRE_TIME))
.sign(algorithm);
// 返回token
return APIResponse.success(token);
3. 定义常量
在Constant
类中添加以下常量:
public static final String JWT_KEY = "your_secret_key"; // JWT密钥
public static final String USERNAME = "username"; // 用户名常量
public static final String USER_ID = "user_id"; // 用户ID常量
public static final String USER_ROW = "user_row"; // 用户行常量
public static final long JWT_EXPIRE_TIME = 1000L * 60 * 60 * 24 * 1000; // JWT过期时间(1000天)
四、总结
通过上述步骤,完成了将用户校验从Session升级为JWT的主要工作。登录接口已改造为生成并返回JWT Token,后续请求中客户端需将Token放在请求头中,由服务器进行校验。接下来,还需对过滤器和用户获取方式进行相应升级,以全面支持JWT校验。
课程笔记:项目实战 - JWT 校验与过滤器升级
一、项目重启与环境准备
- 重启项目:清理缓存、删除target目录,重新生成项目文件。
- 解决包不存在问题:
- 刷新Maven依赖,手动触发下载。
- 调整IDEA设置(打开override、importing选项,检测JDK版本,配置process resources)。
- 清空IDEA缓存(File -> Invalidate Caches)。
二、获取JWT Token
- 测试新接口:使用用户名和密码调用login for jwt接口。
- 验证Token:将返回的Token复制到jwt.io网站,解析查看内容是否包含用户名、用户ID、用户角色和过期时间等信息。
三、过滤器升级
修改用户过滤器:
- 删除原从Session获取用户信息的代码。
- 从请求头获取JWT Token(约定Header的Key为jwt token)。
- 校验Token:
- 使用Algorithm.HMAC256(Constant.JWT_KEY)生成Algorithm对象。
- 通过JWT.require(algorithm).build()生成Verifier。
- 使用Verifier.verify(token)解码Token,获取用户信息并设置到CurrentUser对象中。
- 处理解码异常:
- Token过期异常(TokenExpiredException)。
- Token解码失败异常(JWTDecodeException)。
完善用户过滤器配置:
- 修改方法名为user filter config。
- 增加对更新用户信息接口的拦截。
四、总结
完成了JWT Token的生成与校验,以及过滤器的相应升级。用户登录后,通过在请求头中携带JWT Token进行身份校验,取代了原有的Session方式。在实际操作中,要注意处理Maven依赖和IDEA缓存等问题,确保项目顺利运行。