前言
用户注册成功后,就可以登录了,在本项目中可以使用两种登录方式:账号密码登录和邮件验证码登录。登录之后会生成一个 JWT,也就是用户 Token,后续所有的访问操作都是由 Token 进行控制的。本篇带领大家实现邮件验证码登录的流程,掌握 JWT 的生成与解析方法。
知识准备
JWT
JWT 全称是:Json Web Token,是一种基于 JSON 的开发标准,为了再网络应用环境间相互传递,广泛应用于身份验证。JWT 设计紧凑而安全,特别适合网站的单点登录场景。
JWT 由三部分组成,每部分间通过 .
连接在一起,格式如:
eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI3Iiwic3ViIjoiS2VsbGVyTm90ZXMiLCJpYXQiOjE1ODM0NjgzOTksImV4cCI6MTU4MzQ2OTU5OSwidXNlck5hbWUiOiIyMjIxMTY3ODkwQHFxLmNvbSIsInVzZXJUeXBlIjowfQ.L8Jiq5OkHAcAXc3XJxWoCFsHP6owTt90B5yEAFpvNYI
这三部分从前到后分别是:
- Header:头部信息,声明类型、加密方式
- Payload:存放有效的信息
- Signature:数字签名
在云笔记项目使用 jjwt 来完成 JWT 的生成和解析,通过 Maven 方式引入 jjwt 的依赖包即可:
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
功能分析
流程设计
账号密码登录流程:
邮件验证码登录流程:
总体方案
1. 账号密码登录
- 收到账号密码登录请求(接口1003)根据账号和用户类型,查询用户信息:能查询到,进入下一步;否则,返回账号不存在。
- 校验密码:校验成功,则根据用户信息生成 token 并返回;否则,返回密码不正确。
2. 邮件验证码登录
- 收到发送登录验证码的请求(接口 1004),根据账号和用户类型,查询用户信息:能查询到,发送邮件验证码;否则,返回账号不存在。
- 收到验证码登录的请求(接口 1005),校验验证码:校验成功,进入下一步;否则,返回验证码错误。
- 根据账号和用户类型查询用户信息:能查询到,根据用户信息生成 token 并返回;否则,返回用户不存在。
代码实现
生成 JWT
JWT 生成过程中,比较重要的两个参数是有效期和签名。
有效期:有效期决定了 JWT 生成之后能使用多久,对于不同的使用场景设置不同的时间。在云笔记项目中,可以允许 12 个小时或者更长的有效时间。
签名:签名包含加密方式和密钥。
- 加密方式可选择 MAC、RSA、ECDSA 等不同的方式
- 密钥是自己指定的一个字符串,建议周期性的更换,以加强 JWT 的安全系数
新建一个工具类 JwtUtils.java 专门用于处理 JWT 的生成和解析,代码如下:
/**
* JWT 有效时间 12 小时
*/
public static final long JWT_EXP_TIME = 12 * 60 * 60 * 1000;
/**
* JWT 签名密钥
*/
public static final String JWT_SIGN_KEY = "kellerNotes20241002";
这两行代码指定了 JWT 的有效时间和签名密钥,接下来需要考虑生成的 JWT 中需要携带什么数据。在云笔记项目中,比较重要的用户数据是用户 ID、注册邮箱、以及用户类型(普通用户或者管理员),因此,将这三个数据放在 Payload 中。
//定义用户名称(即:注册邮箱)的键名
private static String userNameKey = "userName";
//定义用户类型的键名
private static String userTypeKey = "userType";
/**
* 生成 JWT
* @return
*/
public static String getJwtString(UserInfo userInfo){
//获取当前的毫秒数
long now=System.currentTimeMillis();
//构建 JWT
JwtBuilder jwtBuilder = Jwts.builder()
//设置 JWT 的 ID,在这里使用用户 ID 作为 JWT 的 ID
.setId(userInfo.getId() + "")
//设置应用名,作为其中一个校验条件
.setSubject(PublicConstant.appName)
//签发时间
.setIssuedAt( new Date() )
//过期时间
.setExpiration( new Date( now + PublicConstant.JWT_EXP_TIME ) )
//自定义内容:注册邮箱
.claim(userNameKey,userInfo.getEmail())
//自定义内容:用户类型
.claim(userTypeKey,userInfo.getType())
//指定使用 HS256 方式进行加密,同时指定加密密钥
.signWith( SignatureAlgorithm.HS256, PublicConstant.JWT_SIGN_KEY);
//生成 JWT
return jwtBuilder.compact();
}
解析 JWT
JWT 在生成以后,将交由客户端保存,客户端在每次请求时携带 JWT 以验证身份。在云笔记项目中,会从 JWT 中解析出用户 ID、注册邮箱、用户类型等有用信息。使用 jjwt 时,通过以下方式即可验证 JWT 并解析出其中携带的信息:
/**
* 解析 JWT
* @return
*/
public static UserInfo getUser(String jwtString){
try {
Claims claims = Jwts.parser()
//指定解密时的密钥
.setSigningKey(PublicConstant.JWT_SIGN_KEY)
//尝试解析 JWT
.parseClaimsJws(jwtString)
//获取到 JWT 中的信息
.getBody();
//解析出 JWT ID,即用户 ID
int id = Integer.parseInt(claims.getId());
//解析出应用名
String subject = claims.getSubject();
//校验应用名
if(!subject.equals(PublicConstant.appName)){
return null;
}
//创建一个 UserInfo 对象,以接收从 JWT 中解析出的信息
UserInfo userInfo = new UserInfo();
//设置用户 ID
userInfo.setId(id);
//设置邮箱
userInfo.setEmail(claims.get(userNameKey,String.class));
//设置用户类型
userInfo.setType(claims.get(userTypeKey,Integer.class));
return userInfo;
}catch (Exception e){
Console.error("checkJwt","JWT 解析失败",jwtString,e.getMessage());
return null;
}
}
解析过程中可能抛出的异常信息:
- io.jsonwebtoken.ExpiredJwtException:JWT 失效,超过了设置的超时时间
- io.jsonwebtoken.MalformedJwtException:无法识别的 JWT,JWT 格式错误,往往是 Header 部分的格式出了问题
- io.jsonwebtoken.SignatureException:数字签名验证失败,通过指定的密钥无法解析出加密后的信息,可能是 Payload 部分也可能是 Signature 不正确
测试 JWT
JwtUtils 工具类写好后,需要验证一下它的可用性。新建测试类 JwtTest.java,代码如下:
@SpringBootTest
public class JwtTest {
@Test
public void getJwt(){
UserInfo userInfo = new UserInfo();
userInfo.setId(1001);
userInfo.setEmail("abc@de.fg");
userInfo.setType(1);
Console.info("getJwt",JwtUtils.getJwtString(userInfo));
}
@Test
public void checkUser(){
String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxMDAxIiwic3ViIjoiS2VsbGVyTm90ZXMiLCJpYXQiOjE1ODM0NjgxOTUsImV4cCI6MTU4MzQ2OTM5NSwidXNlck5hbWUiOiJhYmNAZGUuZmciLCJ1c2VyVHlwZSI6MX0.GUv05KjGRV8gRA0OMsmAIbwzYioHX90LJnWh6av1ao";
Console.info("checkUser",JwtUtils.getUser(jwt));
}
}
先使用 getJwt() 方法生成 JWT,然后将生成的 JWT 字符串在 checkUser() 方法中验证解析出的信息。
账号密码登录
掌握了操作 JWT 的方法后,登录操作就很容易了。账号密码登录的环节没有新的知识点,只需要按照流程图的步骤一步步实现就好了,Server 层代码如下:
public ResultData login(UserInfo userInfo){
//指定查询条件
UserInfo user = new UserInfo();
user.setEmail(userInfo.getEmail());
user.setType(userInfo.getType());
user.setBaseKyleUseAnd(true);
//查询用户
List<UserInfo> list = userMapper.baseSelectByCondition(user);
if(list == null || list.size() < 1){
return ResultData.error("该用户尚未注册");
}
user = list.get(0);
//校验密码
if(user.getPassword().equals(userInfo.getPassword())){
//返回 JWT
return ResultData.success(JwtUtils.getJwtString(user));
}
return ResultData.error("账号密码错误");
}
验证码登录
使用验证码登录,需要先获取登录验证码(接口 1003),可以和接口 1001 的 Server 层代码合并,将 UserServce 类中的 sendRegisterCode(int type,String email)
方法改为下面的方法,就可以支持发送不同类型的验证码了:
public ResultData sendEmailCode(int userType,int sendType,String email){
UserInfo userInfo = getByEmailAndType(userType,email);
//发送注册验证码时,要求邮箱尚未注册
if(sendType == PublicConstant.REGISTER_TYPE){
if(userInfo != null){
return ResultData.error("该邮箱已被注册");
}
//发送登录验证码或重置密码验证码时,要求邮箱已经注册
}else {
if(userInfo == null){
return ResultData.error("账号不存在");
}
}
return emailService.sendCode(sendType,email);
}
验证码登录 Server 层代码如下:
public ResultData loginWithCode(String email,int type,String code){
//构建查询条件
EmailLog emailLog = new EmailLog();
emailLog.setEmail(email);
emailLog.setType(PublicConstant.LOGIN_TYPE);
emailLog.setCode(code);
//校验验证码
if(emailService.checkCode(emailLog)){
//查询用户信息
UserInfo userInfo = getByEmailAndType(type,email);
if(userInfo == null){
return ResultData.error("用户不存在");
}
//生成 token 并返回
return ResultData.success(JwtUtils.getJwtString(userInfo));
}
return ResultData.error("验证码错误或已过期,请重新获取");
}
效果测试
使用 Postman 进行测试,首先测试接口 1003,注意在 Body 中选择使用 JSON(application/json)
格式发送数据:
接口 1004 的测试:
接口 1005 的测试:
源码地址
本篇完整的源码地址如下:
https://github.com/tianlanlandelan/KellerNotes/tree/master/13.登录/server
小结
本篇带领大家分析账号密码登录、验证码登录的具体流程,并对其逐一实现。通过本篇的学习,可以使大家掌握 JWT 的生成与解析方法、不同登录方式的业务逻辑实现,希望大家自己在业务逻辑代码的基础上自己实现接口的 Controller 层代码,然后再参照项目源码进行对比,看哪些是自己不足的地方,哪些是自己实现得更好的地方,如果有更好的实现方式,欢迎沟通交流。