第十三章 JWT

本文涉及的产品
传统型负载均衡 CLB,每月750个小时 15LCU
密钥管理服务KMS,1000个密钥,100个凭据,1个月
网络型负载均衡 NLB,每月750个小时 15LCU
简介: 第十三章 JWT

前言


JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。


1、概念


  • 基于JSON的开发标准
  • 用户信息加密到token里,服务器不保存任何用户信息


2、与Cookie/session的对比


  • 在传统的用户登录认证中,因为http是无状态的,所以都是采用session方式。用户登录成功,服务端会保证一个session,当然会给客户端一个sessionId,客户端会把sessionId保存在cookie中,每次请求都会携带这个sessionId。
  • JWT方式校验方式更加简单便捷化,无需通过redis缓存,而是直接根据token取出保存的用户信息,以及对token可用性校验,单点登录,验证token更为简单。


3、JWT构成与交互流程


第一部分为头部(header),第二部分我们称其为载荷(payload),第三部分是签证(signature)。【中间用 . 分隔】

一个标准的JWT生成的token格式如下:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI1IiwiaWF0IjoxNTY1NTk3MDUzLCJleHAiOjE1NjU2MDA2NTN9.qesdk6aeFEcNafw5WFm-TwZltGWb1Xs6oBEk5QdaLzlHxDM73IOyeKPF_iN1bLvDAlB7UnSu-Z-Zsgl_dIlPiw


3.1、Jwt的头部承载两部分信息


声明类型,这里是jwt

声明加密的算法,通常直接使用HMACSHA256,就是HS256了

{

“alg”: “HS256”,

“typ”: “JWT”

}

然后将头部进行base64编码构成了第一部分:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9


然后我们看第二部分:载荷


载荷,即承载的意思。也就是说这里是承载消息具体内容的地方。

(你在令牌上附带的信息:比如用户的姓名,这样以后验证了令牌之后就可以直接从这里获取信息而不用再查数据库了)

内容又可以分为3种标准

标准中注册的声明

公共的声明

私有的声明

【标准声明】

iss: jwt签发者

sub: jwt所面向的用户

aud: 接收jwt的一方

exp: jwt的过期时间,这个过期时间必须要大于签发时间

nbf: 定义在什么时间之前,该jwt都是不可用的.

iat: jwt的签发时间

jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

【公共声明】

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

【私有声明】

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息

对Payload进行Base64加密就得到了JWT第二部分的内容。

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)

payload (base64后的)

secret

第三部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。

(对前两部分的签名,防止数据篡改)

验证流程:

① 在头部信息中声明加密算法和常量, 然后把header使用json转化为字符串

② 在载荷中声明用户信息,同时还有一些其他的内容;再次使用json 把载荷部分进行转化,转化为字符串

③ 使用在header中声明的加密算法和每个项目随机生成的secret来进行加密, 把第一步分字符串和第二部分的字符串进行加密, 生成新的字符串。词字符串是独一无二的。

④ 解密的时候,只要客户端带着JWT来发起请求,服务端就直接使用secret进行解密。


4、缺点


  • 安全性
  • 性能
  • 一次性


5、Spring boot与JWT整合


5.1、添加模块


authority-service


5.2、添加依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <version>2021.0.4.0</version>
</dependency>


5.3、修改配置文件

server:
  port: 7777
spring:
  application:
    name: auth-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848  #Nacos server 的地址
config:
  jwt:
    # 加密密钥
    secret: tigerkey
    # token有效时长
    expire: 3600
    # header 名称
    header: token


5.4、添加类 config

package com.xxxx.springCloud.auth.config;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
@ConfigurationProperties(prefix = "config.jwt")
@Data
public class JwtConfig {
    /**
     * 密钥
     */
    private String secret;
    /**
     * 过期时间
     */
    private Long expire;
    /**
     * 头部
     */
    private String header;

    /**
     * 生成token
     * @param subject
     * @return
     */
    public String createToken(String subject){
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);

        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setSubject(subject)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();

    }

    /**
     * 获取token中的注册信息
     * @param token
     * @return
     */
    public Claims getTokenClaim(String token){
        try{
            return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
        }catch (Exception e){
            return null;
        }

    }

    /**
     * 验证token是否过期
     * @param expirationTime
     * @return
     */
    public boolean isTokenExpired(Date expirationTime){
        if(null == expirationTime){
            return true;
        }else{
            return expirationTime.before(new Date());
        }
    }

    /**
     * 获取token的失效时间
     * @param token
     * @return
     */
    public Date getExpirationDateFromToken(String token){
        Claims tokenClaim = this.getTokenClaim(token);
        if(tokenClaim == null){
            return null;
        }else{
            return this.getTokenClaim(token).getExpiration();
        }

    }

    /**
     * 获取token中的用户名
     * @param token
     * @return
     */
    public String getUserNameFromToken(String token){
        return this.getTokenClaim(token).getSubject();
    }

    /**
     * 获取token中发布时间
     * @param token
     * @return
     */
    public Date getIssuedDateFromToken(String token){
        return this.getTokenClaim(token).getIssuedAt();
    }

}


5.5、添加controller

package com.xxxx.springCloud.auth.controller;

import com.xxxx.springCloud.auth.config.JwtConfig;
import com.xxxx.springCloud.common.entity.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/auth")
public class AuthController {
    @Autowired
    private JwtConfig jwtConfig;
    @PostMapping("/login")
    public Map<String,String> login(@RequestBody UserInfo userInfo){
        String token = jwtConfig.createToken(userInfo.getUserAccount());
        Map<String, String> map = new HashMap<String, String>();
        map.put("token",token);
        return map;
    }

    /**
     * token是否过期
     * @param token
     * @return
     */
    @PostMapping("/isTokenExpiration")
    public Boolean isTokenExpiration(@RequestParam String token){
        return this.jwtConfig.isTokenExpired(this.jwtConfig.getExpirationDateFromToken(token));
    }
}


5.6、gateway工程 改造


5.6.1、新增线程类UrlThread

package com.xxxx.springCloud.gateway.hread;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import java.util.concurrent.Callable;

public class UrlThread implements Callable<String> {

    private final LoadBalancerClient loadBalancerClient;

    public UrlThread(LoadBalancerClient loadBalancerClient) {
        this.loadBalancerClient = loadBalancerClient;
    }

    @Override
    public String call() throws Exception {
        ServiceInstance serviceInstance = this.loadBalancerClient.choose("auth-service");
        return  "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/auth/isTokenExpiration";
    }

}


5.6.2、新增GlobalBeanConf类

@Configuration
public class GlobalBeanConf {
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}


5.6.3、全局过滤器改造

//负载均衡获取微服务实例
private final   LoadBalancerClient loadBalancerClient;
//远程调用
private final RestTemplate restTemplate;


public DrfGlobalFilter(LoadBalancerClient loadBalancerClient, RestTemplate restTemplate) {
    this.loadBalancerClient = loadBalancerClient;
    this.restTemplate = restTemplate;
}
//启动获取url的线程
FutureTask<String> stringFutureTask = new FutureTask<String>(new UrlThread(this.loadBalancerClient));
new Thread(stringFutureTask).start();
String url = stringFutureTask.get();

Boolean aBoolean = this.restTemplate.postForObject(url+"?token="+token, null,Boolean.class);
package com.xxxx.springCloud.gateway.filter;

import com.xxxx.springCloud.common.dto.TokenDTO;
import com.xxxx.springCloud.gateway.service.AuthService;
import com.xxxx.springCloud.gateway.thread.ValidateTokenTask;
import lombok.SneakyThrows;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;

@Component
public class DrfGlobalFilter implements GlobalFilter, Ordered {

    private final AuthService authService;
    private ExecutorService executorService;

    public DrfGlobalFilter(AuthService authService) {
        this.authService = authService;
        this.executorService = Executors.newFixedThreadPool(5);
    }


    @SneakyThrows
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        //如果登录请求,不用验证token
        String path = request.getURI().getPath();
        if(!path.contains("login")){
            HttpHeaders headers = request.getHeaders();
            String token = headers.getFirst("token");
            //token为空表示没有登录,否则已经登录
            if(StringUtils.isBlank(token)){
                ServerHttpResponse response = exchange.getResponse();
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }else{
                TokenDTO tokenDTO = new TokenDTO();
                tokenDTO.setToken(token);
                //token验证不通过,返回给前端401
                FutureTask<Boolean> booleanFutureTask = new FutureTask<>(new ValidateTokenTask(this.authService, token));
                this.executorService.submit(booleanFutureTask);
                Boolean aBoolean = booleanFutureTask.get();

                if(aBoolean){
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                    return response.setComplete();
                }
            }
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
package com.xxxx.springCloud.gateway.service;

import com.xxxx.springCloud.common.dto.TokenDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(value = "auth-service")
public interface AuthService {
    @PostMapping("/auth/validateToken")
    public Boolean validateToken(TokenDTO tokenDTO);
}

package com.xxxx.springCloud.gateway.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

import java.util.stream.Collectors;

@Configuration
public class GlobalConf {
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }

}
package com.xxxx.springCloud.gateway.thread;

import com.xxxx.springCloud.common.dto.TokenDTO;
import com.xxxx.springCloud.gateway.service.AuthService;

import java.util.concurrent.Callable;

public class ValidateTokenTask implements Callable<Boolean> {
    private final AuthService authService;
    private final String token;

    public ValidateTokenTask(AuthService authService, String token) {
        this.authService = authService;
        this.token = token;
    }

    @Override
    public Boolean call() throws Exception {
        TokenDTO tokenDTO = new TokenDTO();
        tokenDTO.setToken(this.token);
        return this.authService.validateToken(tokenDTO);
    }
}
相关文章
|
6月前
|
存储 JSON 算法
无懈可击的身份验证:深入了解JWT的工作原理
无懈可击的身份验证:深入了解JWT的工作原理
946 0
|
JSON 安全 Java
JWT的原理及实际使用
JWT的原理及实际使用
138 0
|
3月前
|
存储 JSON 安全
在项目中到底应不应该用jwt?
JSON Web Tokens(JWT)是一种开放标准,用于在网络上传输安全信息。它常用于身份验证场景,用户登录后,服务器生成JWT并返回给客户端。客户端在后续请求中携带此令牌,服务器验证其有效性来确认用户身份。JWT具有无状态、可扩展和安全的特点,支持跨域认证,但也有令牌大小、续期复杂等缺点。是否使用JWT取决于项目需求,多数公司在采用,除非有特殊理由避免。以下是Go语言中使用JWT的一个示例。
72 0
|
4月前
|
存储 JSON 算法
JWT原理与在身份验证中的应用
JWT原理与在身份验证中的应用
|
5月前
|
JSON 算法 Go
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
go语言后端开发学习(一)——JWT的介绍以及基于JWT实现登录验证
|
存储 JSON 算法
JWT的原理及实际应用
JWT的原理及实际应用
169 1
|
存储 JSON 前端开发
了解什么是JWT的原理及实际应用
了解什么是JWT的原理及实际应用
1343 0
|
存储 JSON 算法
关于一篇什么是JWT的原理与实际应用
关于一篇什么是JWT的原理与实际应用
103 0
|
安全 NoSQL Redis
oatuth2.0 + JWT 案例
上一节,我们讲述了Oauth2.0 配合security的使用,配合redis去存token,或者配合Jwt去存token。这篇文章,我们主要来系统的串起来讲一下,从宏观的层面来讲述一下单点登录,并且来实现一个demo。
|
存储 JSON 算法
JWT的原理
JWT的原理
154 0