前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot项目集成【用户身份认证】实战 【实战核心篇】基于JWT生成和校验Token

SpringBoot项目集成【用户身份认证】实战 【实战核心篇】基于JWT生成和校验Token

作者头像
天罡gg
修改2023-04-02 15:49:18
8490
修改2023-04-02 15:49:18
举报
文章被收录于专栏:天罡gg天罡gg

前言

书接上文 技术选型篇,我们做了【用户身份认证】的技术选型说明,对基于Session、Token、JWT的方案进行了详细的对比分析,详细说明了它们都是什么和各自的优缺点!这些是实战的基础,还没看过的同学,建议先看上文。最终我和狗哥(博客主页) 采用的是目前流行的基于JWT的Token用户身份认证机制!

本文是实战核心篇,重点是把JWT的核心代码实现! 基于上文我们分析的【用户身份认证】的流程(如下图),我们可以确定使用JWT的核心是实现两点:生成Token、校验Token! 接下来我们就来实现它!

JWT认证流程
JWT认证流程

PS,完整的用户身份认证代码早已实现,和狗哥也已联调通过,正在赶工博文,预告一下我将分三篇来写,非常详细,料很足,准备好发车喽,Let’s go!

  • 【技术选型篇】基于Session、Token、JWT怎么选? 【上文-已发布】
  • 【实战核心篇】基于JWT生成和校验Token【本文】
核心JWT生成和校验Token
核心JWT生成和校验Token
  • 【实战全流程篇】基于JWT+双重检查的登录+登出+拦截器 --防XSS+CSRF漏洞【下文】
实战基于JWT+双重检查的登录+登出+拦截器 --防XSS+CSRF漏洞
实战基于JWT+双重检查的登录+登出+拦截器 --防XSS+CSRF漏洞

本文目录


专栏介绍

因为可能还有很多同学还不清楚上下文,所以简单介绍一下这个专栏要做的事:

天罡老哥和狗哥(博客主页)有意从0到1带大家搭建一个SpringBoot+SpringCloud+Vue的前后端分离项目! 打造一个短小精悍、技术主流、架构规范的前后端分离实战项目!我负责后端,狗哥负责前端! 目的就是让大家通过项目实战,学到一些真东西,将所学理论落地,助力有心强大的你更快的成长!开启你的工作之旅,让开发游刃有余!

详细的后端规划后端大纲思维导图在开篇已经给出,你可以到开篇查收:基于SpringBoot+SpringCloud+Vue前后端分离项目实战 --开篇


一、引入依赖

官方推荐Java的JWT开源库中,收藏数最高的是:java-jwtjjwt-root

JWT开源库
JWT开源库

我们选择使用java-jwt库,项目中将认证相关的通用实现会封装到common层!提前展示一下目录结构,方便大家对照实战:

tg-book-common层认证相关目录结构
tg-book-common层认证相关目录结构

pom中引入依赖,版本号依然定义在父pom定义!

代码语言:javascript
复制
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.11.0</version>
</dependency>

二、TokenProvider 接口

令牌的提供者接口。

从用户身份认证对Token的应用场景来看,核心实现就两个方法:创建Token校验Token

所以,这里提取一个接口TokenProvider,虽然目前只有JWT一种实现,但JWT实际上也只是Token中的一种,所以,以后想用其它Token,只要实现TokenProvider接口,就可以平滑的切过去。

符合开闭原则:对扩展开放,对修改关闭!

代码语言:javascript
复制
public interface TokenProvider {

    /**
     * 根据用户授权信息,创建token
     */
    String create(AuthContextInfo authContextInfo);

    /**
     * 校验token,解析出用户授权信息
     */
    AuthContextInfo verify(String token) ;
}

AuthContextInfo里保存的是认证信息,包含两个重要字段(也就是要存入Payload中的信息):

代码语言:javascript
复制
private String userId;
private String userName;

三、JwtTokenProvider 实现类

基于JWT实现的令牌提供者,快速预览说明如下:

JwtTokenProvider
JwtTokenProvider

从上图可以看出,除了两个核心方法,还定义了两个Payload相关的常量,不过这不是重点。

重点是红框处的【将依赖由构造函数传入】,说明一下为什么这么做!

JWT的签名算法(JwtAlgorithm)和 过期时间(expire)都是变化点,根据依赖倒置原则,要依赖抽象接口,不依赖具体实现,所以我们将它交给外部传入!

另外,在common层实现的类,对变化点应不做决定,而是交给上层决定将依赖注入。

1. 创建Token方法

代码语言:javascript
复制
@Override
public String create(AuthContextInfo authContextInfo) {
    Date issuedAt = new Date();

    Calendar expiresAt  = Calendar.getInstance();
    expiresAt.add(Calendar.SECOND, expire);

    return JWT.create()
        // 签发者
        .withIssuer(authContextInfo.getUserId())
        // 主题
        .withSubject(SUBJECT)
        // 签发时间
        .withIssuedAt(issuedAt)
        // 过期时间
        .withExpiresAt(expiresAt.getTime())
        // 在签发时间之前不可用
        .withNotBefore(issuedAt)
        // 自定义 userName
        .withClaim(CLAIM_USERNAME, authContextInfo.getUserName())
        .sign(this.jwtAlgorithm.getAlgorithm());
}
  • with开头的方法都是构建payload字段信息,withClaim是构建自定义字段,可以构建多个自定义字段!
  • sign方法是指定签名算法 这里不依赖具体算法,而是依赖JwtAlgorithm接口!说完校验token再具体说 JwtAlgorithm。

2. 校验Token方法

代码语言:javascript
复制
@Override
public AuthContextInfo verify(String token) {
    DecodedJWT decodedJWT;
    try {
        // 校验token,无效或过期会抛异常
        decodedJWT = this.jwtAlgorithm.getJwtVerifier().verify(token);
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
    // 主题不一致,被修改了
    if (!SUBJECT.equals(decodedJWT.getSubject())) {
        return null;
    }
    // 返回userId和userName
    AuthContextInfo authInfo = new AuthContextInfo();
    authInfo.setUserId(decodedJWT.getIssuer());
    authInfo.setUserName(decodedJWT.getClaim(CLAIM_USERNAME).asString());
    return authInfo;
}
  • verify方法不报错,说明token合法且未过期,解析的decodedJWT对象里面包含了我们创建时存储的payload载荷信息(也就是数据)。

3. JwtAlgorithm接口

JwtAlgorithm接口是 JwtTokenProvider 的重要依赖,主要包括获取【签名算法】和【验证方法】,定义如下:

代码语言:javascript
复制
public interface JwtAlgorithm {

    /**
     * 获取JWT使用的算法
     */
    Algorithm getAlgorithm();

    /**
     * 获取JWT使用的验证方法
     */
    JWTVerifier getJwtVerifier();
}

3.1 RSA算法实现

对应的是RSA算法的实现类JwtRsaAlgorithm,这里的公钥(publicKey)和私钥(privateKey)也是通过构造函数由外部指定,也是将依赖倒置!

代码语言:javascript
复制
public class JwtRsaAlgorithm implements JwtAlgorithm {

    private final Algorithm algorithm;
    private final JWTVerifier jwtVerifier;
    @Override
    public Algorithm getAlgorithm() {
        return algorithm;
    }
    @Override
    public JWTVerifier getJwtVerifier() {
        return jwtVerifier;
    }

    /**
     * 有参构造函数,将依赖倒置
     */
    public JwtRsaAlgorithm(String publicKey, String privateKey) {
        // 获取公钥对象
        RSAPublicKey rsaPublicKey;
        try {
            rsaPublicKey = RsaKeyUtils.getPublicKey(publicKey);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Get RSA public key error!", e);
        }
        // 获取私钥对象
        RSAPrivateKey rsaPrivateKey;
        try {
            rsaPrivateKey = RsaKeyUtils.getPrivateKey(privateKey);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Get RSA public key error!", e);
        }
        // 创建RSA256签名算法
        this.algorithm = Algorithm.RSA256(rsaPublicKey, rsaPrivateKey);
        // 构建JWTVerifier对象
        this.jwtVerifier = JWT.require(this.algorithm)
                .acceptLeeway(10)
                .acceptExpiresAt(5)
                .build();;
    }
}
  • 代码中的像Algorithm.RSA256JWT.require都是开源库提供的,用于生成最终的AlgorithmJWTVerifier;
  • RsaKeyUtils 是我们定义的通用的工具类,用于将base64编码的 公钥(publicKey)和私钥(privateKey)转为对应的Key对象,直接拷贝使用即可。
代码语言:javascript
复制
public class RsaKeyUtils {
    public static RSAPublicKey getPublicKey(String base64String) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] b = Base64.getDecoder().decode(base64String);
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(b);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key key = keyFactory.generatePublic(keySpec);
        return (RSAPublicKey) key;
    }

    public static RSAPrivateKey getPrivateKey(String base64String) throws NoSuchAlgorithmException, InvalidKeySpecException {
        byte[] b = Base64.getDecoder().decode(base64String);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(b);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key key = keyFactory.generatePrivate(keySpec);
        return (RSAPrivateKey)key;
    }
}

3.2 HMAC算法实现

支持的算法很多,我们再扩展JwtAlgorithm接口,实现一下HMAC算法,你可以实现你需要的算法!

对应的是HMAC算法的实现类JwtHmacAlgorithm,这里的秘钥(secret)也是通过构造函数由外部指定,也是将依赖倒置!

代码语言:javascript
复制
public class JwtHmacAlgorithm  implements JwtAlgorithm {

    private final Algorithm algorithm;
    private final JWTVerifier jwtVerifier;

    @Override
    public Algorithm getAlgorithm() {
        return algorithm;
    }
    @Override
    public JWTVerifier getJwtVerifier() {
        return jwtVerifier;
    }

    /**
     * 有参构造函数,将依赖倒置
     */
    public JwtHmacAlgorithm(String secret) {
        // 创建HMAC256签名算法
        this.algorithm = Algorithm.HMAC256(secret);
        // 构建JWTVerifier对象
        this.jwtVerifier = JWT.require(this.algorithm)
                .acceptLeeway(10)
                .acceptExpiresAt(5)
                .build();
    }
}
  • 使用Algorithm.HMAC256生成算法,再根据Algorithm生成JWTVerifier;

四、测试

因为我们写的类不依赖Spring容器,所以直接在JwtTokenProvider里写个main方法就可以测试,这里使用RSA算法测试,如下:

代码语言:javascript
复制
public static void main(String[] args) throws InterruptedException {
    String publicKey = "太长,省略,可以到 http://www.metools.info/code/c80.html 自行生成。。。";
    String privateKey = "太长,省略,可以到 http://www.metools.info/code/c80.html 自行生成。。。";
    // 创建RSA算法
    JwtAlgorithm jwtRsaAlgorithm = new JwtRsaAlgorithm(publicKey, privateKey);
    // 创建JWT提供者:10秒过期 + RSA算法
    TokenProvider jwtTokenProvider = new JwtTokenProvider(10, jwtRsaAlgorithm);
    // 测试保存到Token中的授权信息
    AuthContextInfo authContextInfo = new AuthContextInfo();
    authContextInfo.setUserId("123456");
    authContextInfo.setUserName("admin");
    // 创建token
    String token = jwtTokenProvider.create(authContextInfo);
    System.out.println("token:" + token);
    // 循环校验token何时过期
    while (true) {
        Thread.sleep(2000);
        // 校验token
        AuthContextInfo authInfo = jwtTokenProvider.verify(token);
        if (authInfo == null) {
            break;
        }
        System.out.println("校验ok:" + authInfo.toString());
    }
}

上面的测试代码,既包括了【创建Token】方法,也包括了【校验Token】方法,主逻辑如下:

  • 创建jwtTokenProvider:基于RSA算法的jwtRsaAlgorithm
  • 创建1个只有10秒有效的Token:jwtTokenProvider.create
  • 循环校验Token是否过期:jwtTokenProvider.verify 返回null就过期了

测试结果,刚好10秒过期!

测试JWT
测试JWT

五、Web层配置注入

在SpringBoot中,我们通常将类交给Spring管理,首先复习一下之前讲过的常用的组件注解:

  • @Service: 通常放在service层的服务类上
  • @Repository: 通常放在dal层的数据访问类上
  • @Controller: 通常放在web层控制器的类上
  • @Component: 代表通用的组件,从它派生了上面3个注解,用于各个实际的场景.

打上这些注解的类,在Spring中称之为Bean。

本文,我们将学习一种新的IOC注入方式:通过JavaConfig的方式注入Bean,即在类上加@Configuration注解,代表这是一个配置类,里面通过添加@Bean注解注入Bean对象,如下:

代码语言:javascript
复制
@Configuration
public class AuthConfig {

    @Bean
    public JwtAlgorithm jwtRsaAlgorithm(@Value("${auth.jwt.rsa.publicKey}") String publicKey, @Value("${auth.jwt.rsa.privateKey}") String privateKey ) {
        return new JwtRsaAlgorithm(publicKey, privateKey);
    }

    @Bean
    public TokenProvider jwtTokenProvider(@Value("${auth.jwt.expire}") int expire, JwtAlgorithm jwtAlgorithm) {
        return new JwtTokenProvider(expire, jwtAlgorithm);
    }
}

通过以上方式,我们就向Spring的IOC容器注入了TokenProvider、JwtAlgorithm,这样我们就可以通过@Autowired注解直接使用了!以后如果想将切换算法,只需要修改这里的配置类,而不用去修改已实现的RSA算法类。

这里还有一个知识点:@Value注解,用于读取配置文件(application.properties或application.yml)中读取字段的值,格式:@Value(“$配置字段名”),这里是用在方法参数

代码语言:javascript
复制
 @Value("${auth.jwt.expire}") int expire
 代表:expire = 配置文件中auth.jwt.expire的值

这里到了3个@Value,对应application.properties中的3个配置如下(以\换行):

代码语言:javascript
复制
auth.jwt.expire =300

auth.jwt.rsa.publicKey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArK+NOK/89rNAWeAguHti\
91QpMaHDZ6EaaySu5dyEvw6oUs4t8AiEc6HC7iTl1U2fxvuukk6P3e96V5w+fb+S\
UFUUaO+oocsKOOxwXcfJ1uQorMsEns1PjYB9weOOYYQoE2KY34AE6+zRT3w8uMXX\
pBmazZbPhUP8cGAOimUv4nSIK4n/nwBezEEeFM5dREaxabiDBe9HvOXmu8EfO2/P\
MsE5K9x/GP/wNbE+yzP+rC6rr3mgJNugUmE7BB1Usl7pS1myukiFz+PXoE/nibed\
k5FWzL5jeV8M8F7AZ404DdVhyN5dbLvwAI8jnnJ1nNRVEh5+1H0rvwSSlTAo+Po+\
bwIDAQAB

auth.jwt.rsa.privateKey=MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCsr404r/z2s0BZ\
4CC4e2L3VCkxocNnoRprJK7l3IS/DqhSzi3wCIRzocLuJOXVTZ/G+66STo/d73pX\
nD59v5JQVRRo76ihywo47HBdx8nW5CisywSezU+NgH3B445hhCgTYpjfgATr7NFP\
fDy4xdekGZrNls+FQ/xwYA6KZS/idIgrif+fAF7MQR4Uzl1ERrFpuIMF70e85ea7\
wR87b88ywTkr3H8Y//A1sT7LM/6sLquveaAk26BSYTsEHVSyXulLWbK6SIXP49eg\
T+eJt52TkVbMvmN5XwzwXsBnjTgN1WHI3l1su/AAjyOecnWc1FUSHn7UfSu/BJKV\
MCj4+j5vAgMBAAECggEBAJ9/djzJsChc4C8jKJW8wWgYQAQrmUR6NOCJfVGqIKIn\
c6kn7p4p/8yduGIlinM9wzoS9OcF0TP4IVQSaFXVP9sa+kMCOQtXchWprQ+xnOfy\
zO7shVP35maYK4+OEtBXNHzTMMgegm02yw1TfvJbKhXT4HvLs9kvNlbFIikJ1PSf\
kRdruq8/SiqDAiwtN4OUn7X3/pIx6b9P7hbO95aNUi1Dxb8xjQA05QVlqA8OwNyq\
ORUHI0ayZI6dmyTA5FUkZZf1tS0PzVLjubBOjZHRSq1a8Eg2qV+e/zDNPkuKQZ3g\
jyy6PamkRlSbfel6+8zacQVC8QRe1AAX68HFe/WKz2ECgYEA2WZySOPBJF85KuK1\
Tv8rgNAoRZZNZbH/0YT2OkBOprOX7bOtvSx+nTZPw0U7nR3nMqnJ9uk/gVfmkbD6\
WzHaSNEpxim2lT+A9jMC5FZcaQxJDHHBpUdMbPssvPGkE8i0XY+rxyQCugVp7+Jg\
mTHISfaZCSBmAG09qtp3Wuk8li0CgYEAy1iv53kChnVvBTckQYHWI5R5ByzPOEum\
EHLo8fvEvUSWaVlDDoPeFw1XtybNVBeyeu/c3HLi7/Z1836PwtpCCAF9XSIq8N/B\
PUR2hKDlg4j3m6BvR25Pu54ORbyevL1LugV+iGVfQ9lWjeV6XeYoN/jGTwSY/Hb+\
dc4rur8sBIsCgYBYlFx2hI460q3JYow7fs7r8mSmTeKFUCyK4yEshO1HESATU0W0\
Mb/5MJr5Vmk+0GNWikXnXAxrGDSzIigwJjTpvIfH3VEuqKxUJF7GSMXoa4AMGQGs\
5UsnkIQfDFotUXbkNFjqkCqoPvJ2Mofng5g3QsoCJPhKrjgVOGSvXx83lQKBgQCG\
0Y8WveFRumxYHd4Y3HdYcajoe+oLngRFJZqSTWV8QwwiXr8Z0Y4e5IbCdKRv26JG\
5d8d/cG+bT54qPGxs7lRy4MNi4jC2OcqssiNWIuy8M2RzgXZaybL8pft3oe0BSE+\
/UOONP+7YU6El5/Qv7bsnTEF1LuFr3M4MfBGSVdqzwKBgQDJBulXrWWQujQlQ+9/\
u7YoCwIr6N/ZL0fpnKtaQ7WfHs7zy6QUhu6skufFJKmWehOD6i+SWBmuhv4PPMCS\
IPhjChIh8AL8AVfSCjrksP0YENOHtbBhSE9bBHdH4u9VBy+6lbErSLl0867Qy4Z4\
LAN0Bjc+5MNy0vMQmqat/EKlHA==

web层对应的目录结构:

在这里插入图片描述
在这里插入图片描述

最后

OK,就说这么多了,我们下文见!

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-03-31,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 本文目录
    • 专栏介绍
    • 一、引入依赖
    • 二、TokenProvider 接口
    • 三、JwtTokenProvider 实现类
      • 1. 创建Token方法
        • 2. 校验Token方法
          • 3. JwtAlgorithm接口
            • 3.1 RSA算法实现
            • 3.2 HMAC算法实现
        • 四、测试
        • 五、Web层配置注入
        • 最后
        相关产品与服务
        容器服务
        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档