JWT 入门

使用JWT的原因

为了保护项目之中的数据资源,那么一定就需要采用认证检测机制,于是SpringCloud进行认证处理,就可以使用SpringSecurity 来实现了,但是如果你真的去使用了SpringSecurity进行开发,因为维护的成本实在是太高了。

在后来的时候有很多的开发者开始尝试通过OAuth2统一认证来进行SpringCloud认证与授权服务,这种操作也属于较早期的实现了,这种实现最大的问题在于随着版本的更新会出现代码不稳定的情况,很多的开发者就开始尝试自己去独立的实现认证与授权的操作机制,于是就有了JWT开发技术,使用JWT最大的特点在于不需要维护所有的数据的状态。

使用JWT最大的特点在于不需要维护所有的数据的状态,同时可以自己包含有过期的时间,以及通过附加数据的形式传送所需要的额外的数据内容((可以保存认证以及授权信息)。
在WEB开发中需要维护Session的状态,所以如果用户访问量过大,那么必然会出现Session内容过多而导致服务器处理性能下降的惨剧,所以后面就需要引入WEB服务器的集群,但是为了便于服务器集群之中的Session管理,那么又需要进行分布式的Session存储,总之就一点:有状态的用户需要维护Session,Session维护成本很高。

对于JWT需要提供有一个专属的开发组件,这些组件可以实现JWT数据的生成以及各种检测操作机制,最重要的一点所有的认证可以直接通过网关来进行过滤,可以考虑通过网关来进行认证状态的检查,而后每一个具体的微服务实现权限的排查。
在这里插入图片描述

JWT结构

基于Token的单点登录技术可以有效的节约开发成本,但是这里面最重要的一点就是JWT数据的组成结构定什么?在实际的项目开发中,JWT主要是为了实现用户认证数据的处理,所以第三方应用客户端要想进行用户统一登录的操作,只需要传入用户认证所需要的数据信息,即可成功的获取到Token令牌,考虑到令牌的安全性以及实用性,在每一个JWT数据中会包含有三类信息项: Header头部信息、Payload负载信息、Signature数字签名。

在这里插入图片描述

使用JWT的结构特点,可以有效的实现用户数据信息的携带,每次进行服务调用时都需要传递此JWT数据,目标微服务依靠此数据实现用户登录状态检测,同时也可以根据其保存的用户角色数据,来进行当前操作执行的合法性校验
在这里插入图片描述

JWT入门案例

在实际项目开发中,不同的项目会存在有不同的数字签名、发布者等数据信息,考虑到JWT使用的便捷性,可以直接通过application.yml配置JWT的相关属性内容,随后将这些属性注入到指定的配置类中

1、【microboot项目】创建一个新的子模块“microboot-jwt”子模块,随后修改build.gradle配置文件添加依赖:

dependencies.gradle

ext.versions = [
        jjwt                                : '0.9.1', // JWT依赖库
        jaxb                                : '2.3.1', // JAXB依赖库 JDK11需要加这个依赖,8不需要
]
ext.libraries = [
        // 以下的配置为JWT所需要的依赖库
        'jjwt': "io.jsonwebtoken:jjwt:${versions.jjwt}",
        'jaxb-api': "javax.xml.bind:jaxb-api:${versions.jaxb}"
]

build.gradle

project('microboot-jwt') { // 子模块
    dependencies { // 配置子模块依赖
        compile('org.springframework.boot:spring-boot-starter-web')
        compile(libraries.'fastjson')
        compile(libraries.'jjwt')
        compile(libraries.'jaxb-api')
    }
}

2、【microboot-jwt子模块】创建 application.yml配置文件,配置JWT的相关环境属性,同时考虑到了很多的信息需要自定义,同时也需要保存有应用的名称,所以要添加“spring.application.name”配置项

spring:
  application:
    name: microboot-jwt # 应用名称
muyan: # 自定义配置项
  config: # 配置项定义
    jwt: # 配置JWT相关属性
      sign: muyan # JWT证书签名
      issuer: MuyanYootk # 证书签发人
      secret: www.yootk.com # 加密密钥
      expire: 10 # 有效时间(单位:秒)

如果你现在要做的是一个非常大型的JWT系统,那么这个时候可以考虑通过数据库来实现以上的配置项

3、【microboot-jwt子模块】所有的配置项一定要被程序进行读取,可以创建有一个配置类

package com.yootk.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "muyan.config.jwt")
public class JWTConfigProperties { // 保存JWT配置项
    private String sign;
    private String issuer;
    private String secret;
    private long expire;
}

在之前application.yml中所配置的相关属性,实际上都是为了JWT数据服务准备的,因为JWT在整个的项目设计之中是一个核心的结构,所以最佳的做法是通过一个服务的包装类,来包裹jjwt依赖库里面所提供的各个组件。

4、【microboot-jwt子模块】既然要进行服务的开发,首先就应该要提供有一个完善的业务接口。

package com.yootk.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;

import javax.crypto.SecretKey;
import java.util.Map;

public interface ITokenService { // 实现JWT的相关操作接口
    public SecretKey generalKey(); // 获取当前JWT数据加密KEY

    /**
     * 生成一个合法的Token数据
     * @param id 这个Token的唯一ID(随意存储,本次可以考虑存储用户ID)
     * @param subject 所有附加的信息内容,本次直接接收了一个Map,但是最终存储的时候存放JSON
     * @return 返回一个有效的Token数据字符串
     */
    public String createToken(String id, Map<String, Object> subject);

    /**
     * 是根据Token的字符串内容解析出其组成的信息(头信息与附加信息)
     * @param token 要解析的Token完整数据
     * @return Jws接口实例
     * @throws JwtException 如果Token失效或者结构错误
     */
    public Jws<Claims> parseToken(String token) throws JwtException;
    /**
     * 校验当前传递的Token数据是否正确
     * @param token 要检查的Token数据
     * @return true表示合法、false表示无效
     */
    public boolean verifyToken(String token);
    /**
     * Token存在有效时间的定义,所以一定要提供有Token刷新机制
     * @param token 原始的Token数据
     * @return 新的Token数据
     */
    public String refreshToken(String token);
}

5、【microboot-jwt子模块】定义ITokenService业务接口子类,并且完成所有的抽象方法

package com.yootk.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Service
public class TokenServiceImpl implements ITokenService {
    @Autowired
    private JWTConfigProperties jwtConfigProperties; // JWT的相关配置属性
    @Value("${spring.application.name}")
    private String applicationName;
    private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法
    @Override
    public SecretKey generalKey() { // 获取加密KEY
        byte[] encodedKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    @Override
    public String createToken(String id, Map<String, Object> subject) {
        Date nowDate = new Date(); // 获取当前的日期时间
        // 当前的时间 + 失效时间配置的秒数 = 最终失效的日期时间
        Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 10000);
        Map<String, Object> claims = new HashMap<>(); // 附加的Claims信息
        claims.put("site", "www.yootk.com"); // 添加信息内容
        claims.put("book", "SpringBoot就业编程实战");  // 添加信息内容
        claims.put("company", "沐言科技");  // 添加信息内容
        Map<String, Object> headers = new HashMap<>(); // 保存的头信息
        headers.put("author", "李老师");
        headers.put("module", this.applicationName); // 保存应用的名称
        headers.put("desc", "喜欢你。");
        JwtBuilder builder = Jwts.builder().setClaims(claims)    // 保存Claims信息
                .setHeader(headers) // 保存Headedr信息
                .setId(id) // 保存ID内容
                .setIssuedAt(nowDate) // 证书签发日期时间
                .setIssuer(this.jwtConfigProperties.getIssuer()) // 证书签发者
                .setSubject(JSONObject.toJSONString(subject)) // 附加信息
                .signWith(this.signatureAlgorithm, this.generalKey()) // 签名算法
                .setExpiration(expireDate); // Token失效时间
        return builder.compact(); // 生成Token
    }

    @Override
    public Jws<Claims> parseToken(String token) throws JwtException {
        if (this.verifyToken(token)) {  // 检查当前的Token是否正确
            Jws<Claims> claims = Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token);
            return claims;
        }
        return null;
    }

    @Override
    public boolean verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();
            return true;// 没有异常,解析成功
        } catch (JwtException exception) {
            return false;
        }
    }

    @Override
    public String refreshToken(String token) {
        if (this.verifyToken(token)) {  // 正确的Token是可以进行刷新的
            Jws<Claims> claimsJws = this.parseToken(token); // 解析数据
            return this.createToken(claimsJws.getBody().getId(), JSONObject.parseObject(claimsJws.getBody().getSubject(), Map.class));
        }
        return null;
    }
}

6、【测试】

package com.yootk.test;

import com.alibaba.fastjson.JSONObject;
import com.yootk.StartJWTApplication;
import com.yootk.config.JWTConfigProperties;
import com.yootk.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@ExtendWith(SpringExtension.class) // Junit5测试工具
@WebAppConfiguration    // 表示需要启动Web配置才可以进行测试
@SpringBootTest(classes = StartJWTApplication.class)  // 定义要测试的启动类
public class TestTokenService {
    @Autowired
    private ITokenService tokenService;
    private String token = "eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3Njk5NzgsImlhdCI6MTYyNDc1OTk3OCwianRpIjoieW9vdGstNGQ2YzdkMzItZmE5Mi00ZTc4LWJkN2YtNzE1MGMxMDA3MDRlIn0.B7f11ckb4etMTcxzdzTh_1VubQSHnifl43t2-3atrD4";

    @Test
    public void testCreate() { // 创建Token数据
        Map<String, Object> map = new HashMap<>(); // 保存subject数据信息
        map.put("mid", "muyan");
        map.put("name", "沐言科技-李兴华");
        map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 保存角色数据
        String id = "yootk-" + UUID.randomUUID(); // 随机生成ID
        System.out.println(this.tokenService.createToken(id, map));
    }
    @Test
    public void testParse() {
        Jws<Claims> claims = this.tokenService.parseToken(token); // 解析得到的Token数据
        claims.getHeader().forEach((name, value) -> {
            System.out.println("【JWT头信息】name = " + name + "、value = " + value);
        });
        System.err.println("------------------------------------------------------------------");
        claims.getBody().forEach((name, value) -> {
            System.out.println("【JWT主题信息】name = " + name + "、value = " + value);
        });
        System.err.println("------------------------------------------------------------------");
        Map<String, Object> map = JSONObject.parseObject(claims.getBody().get("sub").toString(), Map.class); // 用户配置的信息
        map.entrySet().forEach(entry -> {
            System.out.println("【用户数据】key = " + entry.getKey() + "、value = " + entry.getValue());
        });
    }
    @Test
    public void testVerifyJWT() {
        System.out.println(this.tokenService.verifyToken(token));
    }
    @Test
    public void testRefreshToken() {
        System.out.println(this.tokenService.refreshToken(token));
    }
}

Token拦截

本次的Token主要是为了保护最终微服务的资源的,但是所有的资源保护都有一个基个前提,那么就是要有相应的拦截处理,也就是说可以通过拦截器的形式来进行资源的保护,但是毕竟有些资源是需要保护的,而有一些资源是需要保护的,此时可以考虑通过一个自定义注解的形式来区分保护与非保护资源。

1、【microboot-jwt子模块】创建一个用于区分是否要使用Token保护的注解

package com.yootk.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD}) // 该注解主要用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface JWTCheckToken { // JWT的检查注解
    public boolean required() default true; // 是否要启用Token检查
}

2、【microboot-jwt子模块】为了便于该注解的使用,可以创建一个MessageAction程序类,并且进行信息响应

package com.yootk.action;

import com.yootk.annotation.JWTCheckToken;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/message/*") // 父路径
public class MessageAction {
    @RequestMapping("echo")
    @JWTCheckToken // 这个资源需要被检查
    public Object echo(String msg) {
        return "【ECHO】" + msg;
    }
}

3、【microboot-jwt子模块】如果最终需要此注解生效,那么就需要定义拦截器。

package com.yootk.interceptor;

import com.yootk.annotation.JWTCheckToken;
import com.yootk.service.ITokenService;
import jdk.jfr.Frequency;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class JWTAuthenticationInterceptor implements HandlerInterceptor { // 认证拦截器
    // Token可以通过参数传递也可以通过头信息传递
    private static final String TOKEN_NAME = "yootkToken"; // Token参数名称
    @Autowired
    private ITokenService tokenService; // Token业务接口

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) { // 不处理拦截操作
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler; // 类型转换
        Method method = handlerMethod.getMethod(); // 获取当前要执行的Action方法反射对象
        if (method.isAnnotationPresent(JWTCheckToken.class)) {  // 判断该方法上是否提供有指定的注解
            JWTCheckToken checkToken = method.getAnnotation(JWTCheckToken.class); // 获取指定注解
            if (checkToken.required()) {    // true表示要进行Token检查
                String token = this.getToken(request); // 获取Token数据
                if (!this.tokenService.verifyToken(token)) {    // 验证失败
                    throw new RuntimeException("Token数据无效,无法访问。");
                }
            }
        }
        return true;
    }
    public String getToken(HttpServletRequest request) {
        String token = request.getParameter(TOKEN_NAME); // 通过参数获取头信息
        if (token == null || "".equals(token)) {    // 没有接收到Token
            token = request.getHeader(TOKEN_NAME); // 通过头信息获取
        }
        return token;
    }
}

4、【microboot-jwt子模块】如果要想让拦截器生效,则需要定义一个配置类。

package com.yootk.config;

import com.yootk.interceptor.JWTAuthenticationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer { // 拦截配置类

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.getJWTAuthenticationInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public HandlerInterceptor getJWTAuthenticationInterceptor() {
        return new JWTAuthenticationInterceptor();
    }
}

5、【浏览器】直接通过浏览器进行访问

localhost:8080/message/echo?msg=www.yootk.com&yootkToken=eyJhdXRob3IiOiLniIblj6_niLHnmoTlsI_mnY7ogIHluIgiLCJtb2R1bGUiOiJtdXlhbi15b290ay10b2tlbiIsImFsZyI6IkhTMjU2IiwiZGVzYyI6IuaIkeaYr-S4gOS4quW-iOaZrumAmueahOiAgeW4iO-8jOWWnOasouaVmeWtpu-8jOiupOecn-aQnuecn-ato-eahOaVmeiCsuOAgiJ9.eyJzdWIiOiJ7XCJyaWRzXCI6XCJVU0VSO0FETUlOO0RFUFQ7RU1QO1JPTEVcIixcIm5hbWVcIjpcIuaykOiogOenkeaKgC3mnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImJvb2siOiJTcHJpbmdCb2905bCx5Lia57yW56iL5a6e5oiYIiwiaXNzIjoiTXV5YW5Zb290ayIsImNvbXBhbnkiOiLmspDoqIDnp5HmioAiLCJleHAiOjE2MjQ3NzE1MTYsImlhdCI6MTYyNDc2MTUxNiwianRpIjoieW9vdGstZGJlZDhhMmYtMjBhYi00Njg1LWI2ZDktNmEzNzI0Y2RhZDZiIn0.DDpaXGOdTYDlUZgOMB59uVlgYTZogDYjLW-pvNuvizE

此时通过JWT数据实现的认证检查处理,要比之前使用SpringSecurity、OAuth2、Shiro都简单许多,一个字符串就可以轻松搞定所有的问题了。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AloneDrifters

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值