登录 1:JWT 身份验证

前言

用户注册成功后,就可以登录了,在本项目中可以使用两种登录方式:账号密码登录和邮件验证码登录。登录之后会生成一个 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 层代码,然后再参照项目源码进行对比,看哪些是自己不足的地方,哪些是自己实现得更好的地方,如果有更好的实现方式,欢迎沟通交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值