Web安全通讯之JWT的Java实现

本文用于个人学习笔记

上篇文章中目的是介绍 Json Web Token(以下简称 jwt) ,由于我对 Java 比较熟悉就介绍 Java 服务端 的实现方式,其他语言原理是相同的哈~

PS:如果不清楚JWT,请先看 《Web安全通讯之Token与JWT》

参考博客:各种语言版本的基于HMAC-SHA256的base64加密

  • 官网地址:https://jwt.io/
  • jwt github:https://github.com/jwtk/jjwt
  • Demo源码地址: https://github.com/wangcantian/SecurityCommDemo
  • JWT Jar 包下载:http://pan.baidu.com/s/1pLqJYUv

下面按照这几个方面来介绍它:

  • Java 基本实现
  • 开源库 jjwt 的使用
  • 源码解析 jjwt

废话不多说,撸起袖子就是干,上代码

Java 实现

private static final  String MAC_INSTANCE_NAME = "HMacSHA256";

public static String Hmacsha256(String secret, String message) throws NoSuchAlgorithmException, InvalidKeyException {
    Mac hmac_sha256 = Mac.getInstance(MAC_INSTANCE_NAME);
    SecretKeySpec key = new SecretKeySpec(secret.getBytes(), MAC_INSTANCE_NAME);
    hmac_sha256.init(key);
    byte[] buff = hmac_sha256.doFinal(message.getBytes());
    return Base64.encodeBase64URLSafeString(buff);
}

// java jwt
public void testJWT() throws InvalidKeyException, NoSuchAlgorithmException {
    String secret = "eerp";
    String header = "{\"type\":\"JWT\",\"alg\":\"HS256\"}";
    String claim = "{\"iss\":\"cnooc\", \"sub\":\"yrm\", \"username\":\"yrm\", \"admin\":true}";

    String base64Header = Base64.encodeBase64URLSafeString(header.getBytes());
    String base64Claim = Base64.encodeBase64URLSafeString(claim.getBytes());
    String signature = ShaUtil.Hmacsha256(secret, base64Header + "." + base64Claim);

    String jwt = base64Header + "." + base64Claim  + "." + signature;
    System.out.println(jwt);
}

使用开源库 jjwt 实现 JWT

jjwt 是 java 对 JWT 的封装,下面演示 Java 如何使用 jjwt

添加依赖

有两种方法添加

  1. 使用 Maven 仓库(推荐)

    io.jsonwebtoken
    jjwt
    0.7.0

  1. 直接导入 Jar 包,注意:由于开源包使用的 Json 解析框架是 Jackson ,因此要同时导入相关 Jar 包,一套 jar 包我已经帮你们准备好了 >>下载Jar包<<
签发 JWT
public static String createJWT() {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)                                      // JWT_ID
                .setAudience("")                                // 接受者
                .setClaims(null)                                // 自定义属性
                .setSubject("")                                 // 主题
                .setIssuer("")                                  // 签发者
                .setIssuedAt(new Date())                        // 签发时间
                .setNotBefore(new Date())                       // 失效时间
                .setExpiration(long)                                // 过期时间
                .signWith(signatureAlgorithm, secretKey);           // 签名算法以及密匙
        return builder.compact();
}
验证 JWT
public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(jwt)
        .getBody();
}

一般我们把验证操作作为中间件或者拦截器就行了


Java 服务端Demo没有用流行框架,基础的 JSP + Servlet + JavaBean

下面贴出主要的类:

  • TokenMgr.java
    验证和签发的 JWT 的操作类
public class TokenMgr {
    
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.decode(Constant.JWT_SECERT);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }

    /**
     * 签发JWT
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public static String createJWT(String id, String subject, long ttlMillis) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)
                .setSubject(subject)
                .setIssuedAt(now)
                .signWith(signatureAlgorithm, secretKey);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date expDate = new Date(expMillis);
            builder.setExpiration(expDate);
        }
        return builder.compact();
    }
    
    /**
     * 验证JWT
     * @param jwtStr
     * @return
     */
    public static CheckResult validateJWT(String jwtStr) {
        CheckResult checkResult = new CheckResult();
        Claims claims = null;
        try {
            claims = parseJWT(jwtStr);
            checkResult.setSuccess(true);
            checkResult.setClaims(claims);
        } catch (ExpiredJwtException e) {
            checkResult.setErrCode(Constant.JWT_ERRCODE_EXPIRE);
            checkResult.setSuccess(false);
        } catch (SignatureException e) {
            checkResult.setErrCode(Constant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        } catch (Exception e) {
            checkResult.setErrCode(Constant.JWT_ERRCODE_FAIL);
            checkResult.setSuccess(false);
        }
        return checkResult;
    }
    
    /**
     * 
     * 解析JWT字符串
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
            .setSigningKey(secretKey)
            .parseClaimsJws(jwt)
            .getBody();
    }
    
    /**
     * 生成subject信息
     * @param user
     * @return
     */
    public static String generalSubject(SubjectModel sub){
        return GsonUtil.objectToJsonStr(sub);
    }
    
}
  • SignFilter.java
    验证 Token 的过滤器
    PS:Token 可以放在 URL、Cookie、请求头Auth或者body中以一种特定格式解析,这里只是规定把 Token 放在 URL 或者表单示例。
    CheckResult:验证结果模型,包含成功Claim、通过状态、失败码。由于验证结果基本三种状态:通过,不通过,通过但过期,因此多出失败码来区分开。其实验证结果状态还有很多,据需求决定。
public class SignFilter implements Filter {

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1,
            FilterChain arg2) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) arg0;
        HttpServletResponse httpServletResponse = (HttpServletResponse) arg1;
        
        
        String tokenStr = httpServletRequest.getParameter("token");
        if (tokenStr == null || tokenStr.equals("")) {
            PrintWriter printWriter = httpServletResponse.getWriter();
            printWriter.print(ResponseMgr.err());
            printWriter.flush();
            printWriter.close();
            return;
        }
        
        // 验证JWT的签名,返回CheckResult对象
        CheckResult checkResult = TokenMgr.validateJWT(tokenStr);
        if (checkResult.isSuccess()) {
            Claims claims = checkResult.getClaims();
            SubjectModel model = GsonUtil.jsonStrToObject(claims.getSubject(), SubjectModel.class);
            httpServletRequest.setAttribute("tokensub", model);
            httpServletRequest.getRequestDispatcher("/success.jsp").forward(httpServletRequest, httpServletResponse);
        } else {
            switch (checkResult.getErrCode()) {
            // 签名过期,返回过期提示码
            case Constant.JWT_ERRCODE_EXPIRE:
                PrintWriter printWriter = httpServletResponse.getWriter();
                printWriter.print(ResponseMgr.loginExpire());
                printWriter.flush();
                printWriter.close();
                break;
            // 签名验证不通过
            case Constant.JWT_ERRCODE_FAIL:
                PrintWriter printWriter2 = httpServletResponse.getWriter();
                printWriter2.print(ResponseMgr.noAuth());
                printWriter2.flush();
                printWriter2.close();
                break;
            default:
                break;
            }
        }
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {

    }

}
  • web.xml


    
    
        LoginServlet
        com.paul.sertest.servlet.LoginServlet
    
    
    
        LoginServlet
        /api/login
    
  
    
        CorsFilter
        com.paul.sertest.filter.CorsFilter
    
  
    
        CorsFilter
        /api/*
    
    
    
        SignFilter
        com.paul.sertest.filter.SignFilter
        
            encoding
            utf-8
        
    
  
    
        SignFilter
        /api/check/*
        /api/bussin/*
    
  

其中 CorsFilter 对 API 接口的响应头添加 Content-Type : text/json 以及编码格式等等,所以它对 /SERTEXT/api/* 的地址进行拦截,避免影响请求静态页面;Filter 的执行顺序是根据解析 web.xml 文件中节点的 先后顺序 决定的,需要把 CorsFilter 放首位,因为假如某处抛出异常会导致返回数据乱码。

看看 jjwt 的源码

PS:源码从 GIT 仓库 Clone 下来就行了
从使用示例代码看得出 jjwt 使用了 Builder模式 以及灵活多变的 链式调用 ,builder() 出 JwtBuilder 对象。
在进行一系列链式 set 方法后执行 compact() 方法返回我们想要的结果,来看看它到底是怎么签名的:

DefaultJwtBuilder.java

@Override
public String compact() {
        ...
        ...
        // 进行参数判断

        Header header = ensureHeader();

        Key key = this.key;
        if (key == null && !Objects.isEmpty(keyBytes)) {
            key = new SecretKeySpec(keyBytes, algorithm.getJcaName());
        }

        JwsHeader jwsHeader;

        if (header instanceof JwsHeader) {
            jwsHeader = (JwsHeader)header;
        } else {
            jwsHeader = new DefaultJwsHeader(header);
        }

        // 构造密匙对象
        if (key != null) {
            jwsHeader.setAlgorithm(algorithm.getValue());
        } else {
            //no signature - plaintext JWT:
            jwsHeader.setAlgorithm(SignatureAlgorithm.NONE.getValue());
        }

        if (compressionCodec != null) {
            jwsHeader.setCompressionAlgorithm(compressionCodec.getAlgorithmName());
        }

        String base64UrlEncodedHeader = base64UrlEncode(jwsHeader, "Unable to serialize header to json.");

        String base64UrlEncodedBody;

        if (compressionCodec != null) {

            byte[] bytes;
            try {
                bytes = this.payload != null ? payload.getBytes(Strings.UTF_8) : toJson(claims);
            } catch (JsonProcessingException e) {
                throw new IllegalArgumentException("Unable to serialize claims object to json.");
            }

            base64UrlEncodedBody = TextCodec.BASE64URL.encode(compressionCodec.compress(bytes));

        } else {
            base64UrlEncodedBody = this.payload != null ?
                    TextCodec.BASE64URL.encode(this.payload) :
                    base64UrlEncode(claims, "Unable to serialize claims object to json.");
        }

        // 这里已经组成了实现 Header 和 Playload 部分
        String jwt = base64UrlEncodedHeader + JwtParser.SEPARATOR_CHAR + base64UrlEncodedBody;

        if (key != null) { //jwt must be signed:

            JwtSigner signer = createSigner(algorithm, key);

            String base64UrlSignature = signer.sign(jwt);

            jwt += JwtParser.SEPARATOR_CHAR + base64UrlSignature;
        } else {
            // no signature (plaintext), but must terminate w/ a period, see
            // https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-25#section-6.1
            jwt += JwtParser.SEPARATOR_CHAR;
        }

        return jwt;
    }

首先会进行 payload 以及 key 的判断,原则是 payload 与 自定义 claims 不能为 null 以及不能同时赋值参数,key 和 keyBytes 不能同时存在;然后通过 ensureHeader() 获取 Header 对象。

protected Header ensureHeader() {
        if (this.header == null) {
            this.header = new DefaultHeader();
        }
        return this.header;
}

如果没有设置自定义 Header ,则实例一个默认 Header 对象 ---- DefaultHeader,其中 Header 接口是个继承 Map 接口的集合,符合了 header 部分键值对形式。
然后 Header 实例会被“转换”为 JwsHeader 实例,其中 JwsHeader 接口继承 Header 接口,多定义了“签名”和“密匙ID”这个两个属性。

        JwsHeader jwsHeader;

        if (header instanceof JwsHeader) {
            jwsHeader = (JwsHeader)header;
        } else {
            jwsHeader = new DefaultJwsHeader(header);
        }

最终通过 base64UrlEncode() 方法的到 base64url 编码后的 header 字符串。

protected String base64UrlEncode(Object o, String errMsg) {
        byte[] bytes;
        try {
            // 使用 Jackson 框架将对象序列化
            bytes = toJson(o);
        } catch (JsonProcessingException e) {
            throw new IllegalStateException(errMsg, e);
        }
        // 将 byte 数组转化为 base64url 编码的 byte 数组
        return TextCodec.BASE64URL.encode(bytes);
}

接着同理将 payload 或 claims base64url 编码组成 playload 部分。
最后就是签名部分了。createSigner(algorithm, key) 方法实例一个 DefaultJwtSigner 对象,该对象进行统一的签名和编码操作,它的构造函数会传入签名算法枚举 SignatureAlgorithm 对象,定义所有算法的名字、描述、组类等等。

public enum SignatureAlgorithm {

    /** JWA name for {@code No digital signature or MAC performed} */
    NONE("none", "No digital signature or MAC performed", "None", null, false),

    /** JWA algorithm name for {@code HMAC using SHA-256} */
    HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),

    ........
    ........

    /**
     * JWA algorithm name for {@code RSASSA-PSS using SHA-512 and MGF1 with SHA-512}. This is not a JDK standard
     * algorithm and requires that a JCA provider like BouncyCastle be in the classpath.  BouncyCastle will be used
     * automatically if found in the runtime classpath.
     */
    PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false);
}

DefaultJwtSigner 怎么分别实现具体算法呢?

public class DefaultJwtSigner implements JwtSigner {

    private static final Charset US_ASCII = Charset.forName("US-ASCII");

    private final Signer signer;

    public DefaultJwtSigner(SignatureAlgorithm alg, Key key) {
        this(DefaultSignerFactory.INSTANCE, alg, key);
    }

    public DefaultJwtSigner(SignerFactory factory, SignatureAlgorithm alg, Key key) {
        Assert.notNull(factory, "SignerFactory argument cannot be null.");
        this.signer = factory.createSigner(alg, key);
    }

    @Override
    public String sign(String jwtWithoutSignature) {

        byte[] bytesToSign = jwtWithoutSignature.getBytes(US_ASCII);

        byte[] signature = signer.sign(bytesToSign);

        return TextCodec.BASE64URL.encode(signature);
    }
}

构造函数 DefaultJwtSigner 中有个单例签名工厂 ---- DefaultSignerFactory,让我们来看看这个工厂都做了些什么

public class DefaultSignerFactory implements SignerFactory {

    public static final SignerFactory INSTANCE = new DefaultSignerFactory();

    @Override
    public Signer createSigner(SignatureAlgorithm alg, Key key) {
        Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
        Assert.notNull(key, "Signing Key cannot be null.");

        switch (alg) {
            case HS256:
            case HS384:
            case HS512:
                return new MacSigner(alg, key);
            case RS256:
            case RS384:
            case RS512:
            case PS256:
            case PS384:
            case PS512:
                return new RsaSigner(alg, key);
            case ES256:
            case ES384:
            case ES512:
                return new EllipticCurveSigner(alg, key);
            default:
                throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
        }
    }
}

原来在工厂中根据不同算法实例化不同的签名对象 Signer,看来具体签名算法就是放在 Signer 接口的实现类了,由于我们上面使用 HMacSHA256 算法,关心 MacSigner 类就好了,让我们看看它是怎么做的:

public class MacSigner extends MacProvider implements Signer {

    ... ...

    @Override
    public byte[] sign(byte[] data) {
        Mac mac = getMacInstance();
        return mac.doFinal(data);
    }

    protected Mac getMacInstance() throws SignatureException {
        try {
            return doGetMacInstance();
        } catch (NoSuchAlgorithmException e) {
            String msg = "Unable to obtain JCA MAC algorithm '" + alg.getJcaName() + "': " + e.getMessage();
            throw new SignatureException(msg, e);
        } catch (InvalidKeyException e) {
            String msg = "The specified signing key is not a valid " + alg.name() + " key: " + e.getMessage();
            throw new SignatureException(msg, e);
        }
    }

    protected Mac doGetMacInstance() throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance(alg.getJcaName());
        mac.init(key);
        return mac;
    }
}

是不是很熟悉?也是通过 Mac.getInstance(ALG_NAME) 获取 Mac 对象后调用其 mac.doFinal(data) 获取签名后的 byte 数组,最后转字符串啦,签名代码到这里基本结束了。
上面提到的类如图:

Web安全通讯之JWT的Java实现_第1张图片


JJWT 的验证代码就不讲解了,原理是:取出 header 部分和 playload 部分,根据 header 定义的算法再一次签名,比较这个签名是否和 JWT 自带的签名是否完全相同,验证是否成功。

你可能感兴趣的:(Web安全通讯之JWT的Java实现)