使用Gateway+JWT实现网关鉴权

JWT

JWT(JSON Web Token), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登陆(SSO)场景。JWT的声明通常被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也能够增长一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。javascript

JWT的组成

  • Header(头部) —— base64编码的Json字符串
  • Payload(载荷) —— base64编码的Json字符串
  • Signature(签名)—— 使用指定算法,经过Header和Payload加盐计算的字符串

header前端

jwt的头部承载两部分信息:java

{
  'typ': 'JWT', //声明类型
  'alg': 'RS256' //签名加密的算法
}

而后将头部进行base64加密(该加密是能够对称解密的),构成了第一部分.git

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

playloadgithub

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分:web

  • 标准中注册的声明 (==建议但不强制使用==) :
{ "iss": "JWT Builder", //jwt签发者
  "iat": 1416797419, // jwt的签发时间
  "exp": 1448333419,  //jwt的过时时间,这个过时时间必需要大于签发时间
  "aud": "www.bilibili.com", //接收jwt的一方
  "sub": "1837307557@qq.com",  //jwt所面向的用户
  "GivenName": "Levin", 
  "Surname": "Levin", 
  "Email": "1837307557@qq.com", 
  "Role": [ "ADMIN", "MEMBER" ],
  "nbf" : 1416797420 //定义在什么时间以前,该jwt都是不可用的,
  "jti" : "jwt的惟一身份标识,主要用来做为一次性token,从而回避重放攻击"
}
  • ==公共==的声明 :
    公共的声明能够添加任何的信息,通常添加用户的相关信息或其余业务须要的必要信息.但不建议添加敏感信息,由于该部分在客户端可解密.
  • ==私有==的声明 :
    私有声明是提供者和消费者所共同定义的声明,通常不建议存放敏感信息,由于base64是对称解密的,意味着该部分信息能够归类为明文信息。

定义一个payload:redis

// 包括须要传递的用户信息;
{ "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.gusibi.com", 
  "sub": "uid", 
  "nickname": "goodspeed", 
  "username": "goodspeed", 
  "scopes": [ "admin", "user" ] 
}

而后将其进行base64加密,获得Jwt的第二部分。算法

eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVk

signaturespring

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:数据库

// 根据头部alg算法与私有秘钥进行加密获得的签名字符串;
// 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(  
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    SECREATE_KEY
)

这个部分须要base64加密后的header和base64加密后的payload使用 "." 链接组成的字符串,而后经过header中声明的加密方式进行加盐secret组合加密,而后就构成了jwt的第三部分。

// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用"."链接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,因此,它就是你服务端的私钥,在任何场景都不该该流露出去。一旦客户端得知这个secret, 那就意味着客户端是能够自我签发jwt了。

加密及验证过程

加密:

生成头JSON,荷载(playload) JSON

将头JSON Base64编码 + 荷载JSON Base64编码 +secret 三者拼接进行加密获得签名

JSON Base64编码 + 荷载JSON Base64编码 + 签名 三者经过 "." 相链接

一条 hhh.ppp.sss 格式的JWT 即生成

解密:

取得Jwt hhh.ppp.sss 格式字符,经过 "." 将字符分为三段

对第一段进行Base64解析获得header json,获取加密算法类型

将第一段Header JSON Base64编码 + 第二段 荷载JSON Base64编码 + secret采用相应的加密算法加密获得签名

将步骤三获得的签名与步骤一分红的第三段也就是客户端传入的签名进行匹配,匹配成功说明该jwt为server自身产出;

获取playload内信息,经过信息能够作鉴权操做;

成功访问;

经过这些步骤,保证了第三方没法修改jwt,jwt只能自产自销,在分布式环境下服务接收到合法的jwt即可知是本系统内自身或其余服务发出的jwt,该用户是合法的;

X509

X.509是常见通用的证书格式。全部的证书都符合为Public Key Infrastructure (PKI) 制定的 ITU-T X509 国际标准。X.509是国际电信联盟-电信(ITU-T)部分标准和国际标准化组织(ISO)的证书格式标准。做为ITU-ISO目录服务系列标准的一部分,X.509是定义了公钥证书结构的基本标准。1988年首次发布,1993年和1996年两次修订。当前使用的版本是X.509 V3,它加入了扩展字段支持,这极大地增进了证书的灵活性。X.509 V3证书包括一组按预约义顺序排列的强制字段,还有可选扩展字段,即便在强制字段中,X.509证书也容许很大的灵活性,由于它为大多数字段提供了多种编码方案.

JWT 最多见的几种签名算法HS256(HMAC-SHA256) 、RS256(RSA-SHA256) 还有 ES256(ECDSA-SHA256)。

这三种算法都是一种消息签名算法,获得的都只是一段没法还原的签名。区别在于消息签名签名验证须要的 「key」不一样。

  1. HS256 使用同一个「secret_key」进行签名与验证。一旦 secret_key 泄漏,就毫无安全性可言了。

    • 所以 HS256 只适合集中式认证,签名和验证都必须由可信方进行。
  2. RS256 是使用 RSA 私钥进行签名,使用 RSA 公钥进行验证。公钥即便泄漏也毫无影响,只要确保私钥安全就行。

    • RS256 能够将验证委托给其余应用,只要将公钥给他们就行。
  3. ES256 和 RS256 同样,都使用私钥签名,公钥验证。算法速度上差距也不大,可是它的签名长度相对短不少(省流量),而且算法强度和 RS256 差很少。

对于单体应用而言,HS256 和 RS256 的安全性没有多大差异。
而对于须要进行多方验证的微服务架构而言,显然 RS256/ES256 安全性更高。
只有 user 微服务须要用 RSA 私钥生成 JWT,其余微服务使用公钥便可进行签名验证,私钥获得了更好的保护。

无状态登陆

微服务集群中的每一个服务, 对外提供的都是Rest风格的接口, 而Rest风格的一个最重要的规范就是: 服务的无状态性, 即:

  • 服务端不保存任何客户端请求者状态信息
  • 客户端的每次请求必须具有自描述信息, 经过这些信息识别客户端身份

优势:

  • 客户端请求不依赖服务端的信息, 任何屡次请求不须要必须访问到同一台服务
  • 服务端的集群和状态对客户端透明
  • 服务端能够任意的迁移和伸缩
  • 减少服务端存储压力

JJWT

jjwt是一个Java对jwt的支持库,咱们使用这个库来建立、解码token

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

配合joda-time处理过时时间

<dependency>
     <groupId>joda-time</groupId>
     <artifactId>joda-time</artifactId>
     <version>2.9.6</version>
 </dependency>

生成JWT

客户端发送 POST 请求到服务器,提交登陆处理的Controller层
调用认证服务进行用户名密码认证,若是认证经过,返回完整的用户信息及对应权限信息
利用 JJWT 对用户、权限信息、秘钥构建Token
返回构建好的Token
下面是关键代码, 文章后面有所有的工具类

/**
     * 私钥加密生成token
     * @param user 载荷数据
     * @param privateKey 私钥字节数组
     * @param expireMinutes 过时时间,单位分钟
     * @return
     */
    public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

Jwts.builder() 返回了一个 DefaultJwtBuilder()

DefaultJwtBuilder属性

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
    private Header header; //头部
    private Claims claims; //声明
    private String payload; //载荷
    private SignatureAlgorithm algorithm; //签名算法
    private Key key; //签名key
    private byte[] keyBytes; //签名key的字节数组
    private CompressionCodec compressionCodec; //压缩算法

DefaultJwtBuilder包含了一些Header和Payload的一些经常使用设置方法

解析&验证JWT

使用私钥加密的jwt, 公钥和私钥均可以解密
使用公钥加密的jwt, 只有私钥能够解密
客户端向服务器请求,服务端读取请求头信息(request.header)获取Token
若是找到Token信息,则根据配置文件中的签名加密秘钥,调用JJWT Lib对Token信息进行解密和解码;
完成解码并验证签名经过后,对Token中的exp、nbf、aud等信息进行验证;
所有经过后,根据获取的用户的角色权限信息,进行对请求的资源的权限逻辑判断;
若是权限逻辑判断经过则经过Response对象返回;不然则返回HTTP 401;

/**
 * 公钥解析token
 * @param token 用户请求中的token
 * @param publicKey 公钥字节数组
 * @return
 * @throws Exception
 */
private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
    return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
            .parseClaimsJws(token);
}

Jwts.parser() 返回了DefaultJwtParser 对象

DefaultJwtParser() 属性

//don't need millis since JWT date fields are only second granularity:
private static final String ISO_8601_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private static final int MILLISECONDS_PER_SECOND = 1000;

private ObjectMapper objectMapper = new ObjectMapper();

private byte[] keyBytes; //签名key字节数组
private Key key; //签名key
private SigningKeyResolver signingKeyResolver; //签名Key解析器
private CompressionCodecResolver compressionCodecResolver = new DefaultCompressionCodecResolver(); //压缩解析器
Claims expectedClaims = new DefaultClaims(); //指望Claims
private Clock clock = DefaultClock.INSTANCE; //时间工具实例
private long allowedClockSkewMillis = 0;  //容许的时间偏移量

parse() 方法传入一个JWT字符串,返回一个JWT对象

解析过程

  1. 检查: 以分隔符" . "切分JWT的三个部分。若是分隔符数量错误或者载荷为空,将抛出 MalformedJwtException 异常。
  2. 头部解析: 将头部原始Json键值存入map。根据是否加密建立不一样的头部对象。jjwt的DefaultCompressionCodecResolver根据头部信息的压缩算法信息,添加不一样的压缩解码器。
  3. 载荷解析: 先对载荷进行Base64解码,若是有通过压缩,那么在解码后再进行解压缩。此时将值赋予payload。若是载荷是json形式,将json键值读入map,将值赋予claims 。

    if (payload.charAt(0) == '{' && payload.charAt(payload.length() - 1) == '}') { 
        //likely to be json, parse it:
        Map<String, Object> claimsMap = readValue(payload);
        claims = new DefaultClaims(claimsMap);
    }
  4. 签名解析: 若是存在签名部分,则对签名进行解析。

    • 首先根据头部的签名算法信息,获取对应的算法。
      若是签名部分不为空,可是签名算法为null或者'none',将抛出MalformedJwtException异常。
    • 获取签名key
    • 可能的异常

      • 若是同时设置了key属性和keyBytes属性,parser不知道该使用哪一个值去做为签名key解析,将抛出异常。
      • 若是key属性和keyBytes属性只存在一个,可是设置了signingKeyResolver,也不知道该去解析前者仍是使用后者,将抛出异常。
      • 若是设置了key(setSigningKey() 方法)则直接使用生成Key对象。若是两种形式( key和keyBytes )都没有设置,则使用SigningKeyResolver(经过setSigningKeyResolver()方法设置)获取key, 固然,获取key为null会抛出异常
    • 建立签名校验器
      JJWT实现了一个默认的签名校验器DefaultJwtSignatureValidator。该类提供了两个构造方法,外部调用的构造方法传入算法和签名key,再加上一个DefaultSignatureValidatorFactory工厂实例传递调用另外一个构造函数,以便工厂根据不一样算法建立不一样类型的Validator。

      public DefaultJwtSignatureValidator(SignatureAlgorithm alg, Key key) {
          this(DefaultSignatureValidatorFactory.INSTANCE, alg, key);
      }
      
      public DefaultJwtSignatureValidator(SignatureValidatorFactory factory, SignatureAlgorithm alg, Key key) {
          Assert.notNull(factory, "SignerFactory argument cannot be null.");
          this.signatureValidator = factory.createSignatureValidator(alg, key);
      }
    • 比对验证
      根据头部和载荷从新计算签名并比对。
      若是不匹配,抛出SignatureException异常
    • 时间校验
      根据当前时间和时间偏移判断是否过时。
      根据当前时间和时间偏移判断是够未到可接收时间
    • Claims参数校验
      即校验parser前面设置的因此require部分。校验完成后,以header,claims或者payload建立DefaultJwt对象返回
    • 至此,已经完成JWT Token的校验过程。校验经过后返回JWT对象。

工具类

JWTUtils

import com.uni.entity.ShopUser;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.*;
import java.security.PrivateKey;
import java.security.PublicKey;


/**
 *  JWT 的工具类:包含了建立和解码的工具
 */
@Slf4j
public class JWTUtils {

    /**
     * 私钥加密token
     * @param user 载荷数据
     * @param privateKey 私钥
     * @param expireMinutes 过时时间,单位分钟
     * @return
     */
    public static String generateToken(ShopUser user, PrivateKey privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, privateKey)
                .compact();
    }

    /**
     * 私钥加密token
     * @param user 载荷数据
     * @param privateKey 私钥字节数组
     * @param expireMinutes 过时时间,单位分钟
     * @return
     */
    public static String generateToken(ShopUser user, byte[] privateKey, Integer expireMinutes) throws Exception{

        return Jwts.builder()
                .claim(JWTConstants.JWT_KEY_ID, user.getId())
                .claim(JWTConstants.JWT_KEY_USER_NAME, user.getUserName())
                .claim(JWTConstants.JWT_KEY_ROLE, user.getRole())
                .setExpiration(DateTime.now().plusMinutes(expireMinutes).toDate())
                .signWith(SignatureAlgorithm.RS256, RsaUtils.getPrivateKey(privateKey))
                .compact();
    }

    /**
     * 使用公钥解析token
     * @param token 用户请求中的token
     * @param publicKey 公钥对象
     * @return
     */
    public static Jws<Claims> parserToken(String token, PublicKey publicKey){
        return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
    }

    /**
     * 公钥解析token
     * @param token 用户请求中的token
     * @param publicKey 公钥字节数组
     * @return
     * @throws Exception
     */
    private static Jws<Claims> parserToken(String token, byte[] publicKey) throws Exception {
        return Jwts.parser().setSigningKey(RsaUtils.getPublicKey(publicKey))
                .parseClaimsJws(token);
    }

    /**
     * 获取token中的用户信息
     * @param token 用户请求中的令牌
     * @param publicKey 公钥
     * @return 用户信息
     * @throws Exception
     */
    public static ShopUser getInfoFromToken(String token, PublicKey publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();

        Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID);
        String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME);
        Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE);
        return new ShopUser(user_id, user_name, user_role);
    }

    /**
     * 获取token中的用户信息
     * @param token 用户请求中的token
     * @param publicKey 公钥字节数组
     * @return 用户信息
     * @throws Exception
     */
    public static ShopUser getInfoFromToken(String token, byte[] publicKey) throws Exception {
        Jws<Claims> claimsJws = parserToken(token, publicKey);
        Claims body = claimsJws.getBody();

        Long user_id = (Long) body.get(JWTConstants.JWT_KEY_ID);
        String user_name = (String) body.get(JWTConstants.JWT_KEY_USER_NAME);
        Integer user_role = (Integer) body.get(JWTConstants.JWT_KEY_ROLE);
        return new ShopUser(user_id, user_name, user_role);
    }

    /* 测试解析token */
    public static void main(String[] args) throws Exception {
        PublicKey publicKey = RsaUtils.getPublicKey("D://rsa//rsa.pub");
        Jws<Claims> claimsJws = parserToken("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0.FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg", publicKey);
        System.out.println(claimsJws.getSignature());
        System.out.println(claimsJws.toString());
    }

}

RsaUtils

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * rsa非对称加密
 * 私钥加密,解密须要公钥
 */
public class RsaUtils {

    /**
     * 从文件中读取公钥
     * @param filename 公钥保存路径,相对于classpath
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPublicKey(bytes);
    }

    /**
     *  获取公钥
     * X.509是定义了公钥证书结构的基本标准
     * @param bytes 公钥的字节形式
     * @return 公钥对象
     * @throws Exception
     */
    public static PublicKey getPublicKey(byte[] bytes) throws Exception {
        X509EncodedKeySpec spec = new X509EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePublic(spec);
    }

    /**
     * 从文件中读取私钥
     * @param filename 私钥保存路径,相对于classpath
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(String filename) throws Exception {
        byte[] bytes = readFile(filename);
        return getPrivateKey(bytes);
    }

    /**
     * 获取私钥
     * @param bytes 私钥的字节形式
     * @return 私钥对象
     * @throws Exception
     */
    public static PrivateKey getPrivateKey(byte[] bytes) throws Exception {
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(bytes);
        KeyFactory factory = KeyFactory.getInstance("RSA");
        return factory.generatePrivate(spec);
    }

    /**
     * 根据密文,生成rsa公钥和私钥,并写入指定文件
     * @param publicKeyFilename 公钥文件路径
     * @param privateKeyFilename 私钥文件路径
     * @param secret 生成密钥的密文
     * @throws Exception
     */
    public static void generateKey(String publicKeyFilename,
                                   String privateKeyFilename, String secret) throws Exception {

        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        SecureRandom secureRandom = new SecureRandom(secret.getBytes());
        keyPairGenerator.initialize(1024, secureRandom);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        //获取公钥并写出
        byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
        writeFile(publicKeyFilename, publicKeyBytes);
        //获取私钥并写出
        byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
        writeFile(privateKeyFilename, privateKeyBytes);
    }

    private static byte[] readFile(String filename) throws Exception {
        return Files.readAllBytes(new File(filename).toPath());
    }

    private static void writeFile(String destPath, byte[] bytes) throws IOException{
        File dest = new File(destPath);
        if (!dest.exists()){
            dest.createNewFile();
        }
        Files.write(dest.toPath(), bytes);
    }

    /* 测试公私钥获取 */
    public static void main(String[] args) throws Exception {
        //公私钥路径
        String pubKeyPath = "D:\\rsa\\rsa.pub";
        String priKeyPath = "D:\\rsa\\rsa.pri";

        //明文
        String secret = "sc@Login(Auth}*^31)&czxy%";
        //RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);

        /* 解密 */
        PublicKey publicKey = RsaUtils.getPublicKey(pubKeyPath);
        System.out.println("公钥: " + publicKey);
        PrivateKey privateKey = RsaUtils.getPrivateKey(priKeyPath);
        System.out.println("私钥: " + privateKey);
        //签名验证器
        DefaultJwtSignatureValidator validator = new DefaultJwtSignatureValidator(SignatureAlgorithm.RS256, publicKey);
        boolean valid = validator.isValid("eyJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoxMjczOTEyMTE1MDI3MTE2MDMyLCJ1c2VyX3JvbGUiOjAsImV4cCI6MTU5MzMxODM2OH0", "FqXgDP6b3qoTrAXteCHxQ2IUnryh_7XfeUHPTW8bXiLpXVDn1zigBJTGcxFhivcy0aIACBs32i0ynbBc5DUli6chesvIE7HfbAl9IiBj0D6Ujde-HnQdHcrzjPt783fy-5Voj4HJZWHrAH9SCPkKqs6VUUR6Ba8QHJeoJtkmUXg");
        System.out.println(valid);
    }
}

JWTProperties

package com.uni.config;

import com.uni.util.RsaUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;

/**
 * 初始化公钥和私钥
 */
@Slf4j
@Data
@PropertySource("classpath:application.yml")
@ConfigurationProperties(prefix = "jwt")
@Configuration
public class JWTProperties {

    private String secret; // 密文

    private String pubKeyPath;// 公钥

    private String priKeyPath;// 私钥

    private Integer expire;// token过时时间

    private String[] skipAuthUrls; //跳过的url

    private PublicKey publicKey; // 公钥

    private PrivateKey privateKey; // 私钥

    //被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,而且只会被服务器调用一次
    @PostConstruct
    public void init() {
        try {
            log.info("公钥地址: " + pubKeyPath);
            log.info("私钥地址: " + priKeyPath);
            File pubKey = new File(pubKeyPath);
            File priKey = new File(priKeyPath);

            if (!pubKey.exists() || !priKey.exists()) {
                // 生成公钥和私钥并写入文件
                RsaUtils.generateKey(pubKeyPath, priKeyPath, secret);
            }
            // 获取公钥和私钥
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            log.error("初始化公钥和私钥失败! " + e);
            throw new RuntimeException();
        }
    }
}

配置以下:

jwt:
  secret: sc@Login(Auth}*^31)&czxy% # 登陆校验的明文
  pubKeyPath: D://rsa//rsa.pub # 公钥地址
  priKeyPath: D://rsa//rsa.pri # 私 钥地址
  expire: 30 # 过时时间,单位分钟
  skipAuthUrls:
    - /auth/**
    - ...

JWTConstants

public class JWTConstants {

    public static final String JWT_HEADER_KEY = "Authorization";

    public static final String JWT_KEY_ID = "user_id";

    public static final String JWT_KEY_USER_NAME = "user_name";

    public static final String JWT_KEY_ROLE = "user_role";
}

JWTModel

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class JWTModel {

    private Long userId;

    private String userName;

    private String jwt;
}

用户登陆

import com.uni.config.JWTProperties;
import com.uni.entity.Dto;
import com.uni.entity.ShopUser;
import com.uni.service.ShopUserService;
import com.uni.util.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;


@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthAPI {

    @Autowired
    private ShopUserService shopUserService;

    @Autowired
    private JWTProperties jwtProperties;

    @PostMapping("/login")
    public Dto doLogin(@RequestBody ShopUser user){
        ShopUser result = null;
        // 验证用户明和密码
        if (ObjectUtils.isNotEmpty(user)) {
             result = shopUserService.login(user);
        }
        if (ObjectUtils.isEmpty(result)){
            return DtoUtil.returnFail("用户名或密码错误", "401");
        }
        try {
            //生成token
            String token = JWTUtils.generateToken(
                    result, jwtProperties.getPrivateKey(), 30);
            return DtoUtil.returnSuccess("登陆成功",
                    new JWTModel(result.getId(), result.getUserName(), token));
        } catch (Exception e) {
            log.error("生成token失败! ", e);
            return DtoUtil.returnFail("登陆失败", "500");
        }
    }
}

网关鉴权

import com.uni.config.JWTProperties;
import com.uni.util.JWTConstants;
import com.uni.util.JWTUtils;
import com.uni.util.RsaUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Minutes;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.Ordered;

/**
 * 请求鉴权过滤器
 */
@Slf4j
@Component
public class AccessGateWayFilter implements GlobalFilter, Ordered {

    private ObjectMapper objectMapper;

    @Autowired
    private JWTProperties jwtProperties;

    @Autowired
    private AntPathMatcher antPathMatcher; //路径匹配器

    public AccessGateWayFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath();

        //跳过不须要验证的url
        for (String skip : jwtProperties.getSkipAuthUrls()) {
            if (antPathMatcher.match(skip, url))
                return chain.filter(exchange);
        }

        //获取token
        String token = exchange.getRequest().getHeaders().getFirst(JWTConstants.JWT_HEADER_KEY);
        ServerHttpResponse response = exchange.getResponse();

        if (StringUtils.isBlank(token)){
            //没有token
            return authError(response, "请登陆");
        } else {
            try {
                //解析token
                Jws<Claims> claims = JWTUtils.parserToken(token, jwtProperties.getPublicKey());
                DateTime now = DateTime.now();
                DateTime exp = new DateTime(claims.getBody().getExpiration());
                
                log.debug(claims.getBody().getExpiration().toString());

                /* 
                    根据具体业务
                    用户信息&权限验证 
                */
                //claims.getBody()获取载荷
                //JWTUtils.getInfoFromToken()获取token中的用户信息

                if (valid){ //签名验证经过
                    return chain.filter(exchange);
                }else {
                    return authError(response, "认证无效");
                }
            } catch (Exception e) {
                log.error("检查token时异常: " + e);
                if (e.getMessage().contains("JWT expired"))
                    return authError(response, "认证过时");
                else
                    return authError(response, "认证失败");
            }
        }
    }

    /**
     * 认证错误输出
     * @param response 响应对象
     * @param msg 错误信息
     * @return 响应信息
     */
    private Mono<Void> authError(ServerHttpResponse response, String msg) {
        response.setStatusCode(HttpStatus.UNAUTHORIZED);
        response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
        Dto returnFail = DtoUtil.returnFail(msg, HttpStatus.UNAUTHORIZED.toString());
        String returnStr = "";
        try {
            returnStr = objectMapper.writeValueAsString(returnFail);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        DataBuffer buffer = response.bufferFactory().wrap(returnStr.getBytes(StandardCharsets.UTF_8));
        return response.writeWith(Flux.just(buffer));
    }


    @Override
    public int getOrder() {
        return -999;
    }
}

刷新JWT

令牌的刷新要作到用户无感知的效果, 推荐使用前端拦截器刷新令牌的方式

Web应用程序

一个好的模式是在它过时以前刷新令牌。

将令牌过时时间设置为一周,并在每次用户打开Web应用程序并每隔一小时刷新令牌。若是用户超过一周没有打开过应用程序,那他们就须要再次登陆,这是可接受的Web应用程序UX(用户体验)。

要刷新令牌,API须要一个新的端点,它接收一个有效的、没有过时的JWT、并返回与新的到期字段相同的签名的JWT。而后Web应用程序会将令牌存储在某处。

移动/本地应用程序

大多数本地应用程序的登陆有且仅有一次。

这里面的出发点是,刷新令牌永远不会过时,而且能够始终为有效的JWT进行更换。

永远不会过时的令牌的问题是它失去了令牌的意义。譬如,若是你电话丢了,你该怎么办?所以,它须要由用户以某种方式进行识别,应用程序须要提供撤销访问的方法。咱们决定使用设备的名称,例如“maryo的iPad”。而后用户能够去应用程序,并撤销访问“maryo的iPad”。

另外一种方法是撤销特定事件的刷新令牌,其中一个有趣的事件是更改密码。

咱们认为JWT对于这些用例无效,所以咱们使用随机生成的字符串,并将它们存储在咱们这边。

注销

没有办法完美的将jwt失效

jwt 的目的原本就是为了在服务器不存任何的东西, 用加解密 的 cpu 时间来换取之前要保存的空间 , 说白了就是用 cpu 时间换内存空间(这个内存能够是 session, 也多是 redis 这种)

可能的解决方案:

  • 将JWT存储在数据库中。您能够检查哪些令牌有效以及哪些令牌已被撤销,但这在我看来彻底违背了使用JWT的目的。
  • 从客户端删除令牌。这将阻止客户端进行通过身份验证的请求,但若是令牌仍然有效且其余人能够访问它,则仍可使用该令牌。这引出了个人下一点。
  • 令牌生命周期短。让令牌快速到期。根据应用,多是几分钟或半小时。当客户端删除其令牌时,会有一个很短的时间窗口仍然可使用它。从客户端删除令牌并具备短令牌生存期不须要对后端进行重大修改。可是令牌生命周期短意味着用户因令牌已过时而不断被注销。
  • 旋转代币。也许引入刷新令牌的概念。当用户登陆时,为他们提供JWT和刷新令牌。将刷新令牌存储在数据库中。对于通过身份验证的请求,客户端可使用JWT,可是当令牌过时(或即将过时)时,让客户端使用刷新令牌发出请求以换取新的JWT。这样,您只需在用户登陆或要求新的JWT时访问数据库。当用户注销时,您须要使存储的刷新令牌无效。不然,即便用户已经注销,有人在监听链接时仍然能够得到新的JWT。
  • 建立JWT黑名单。根据过时时间,当客户端删除其令牌时,它可能仍然有效一段时间。若是令牌生存期很短,则可能不是问题,但若是您仍但愿令牌当即失效,则能够建立令牌黑名单。当后端收到注销请求时,从请求中获取JWT并将其存储在内存数据库中。对于每一个通过身份验证的请求,您须要检查内存数据库以查看令牌是否已失效。为了保持较小的搜索空间,您能够从黑名单中删除已通过期的令牌。
参考:
https://www.jianshu.com/p/6bf...
https://blog.csdn.net/weixin_...
https://blog.csdn.net/github_...