转载

Web安全通讯之JWT的Java实现

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

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

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


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

  • 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

使用开源库 jjwt 实现 JWT

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

添加依赖

有两种方法添加 
1. 使用 Maven 仓库(推荐)

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  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();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
验证 JWT
public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(jwt)
        .getBody();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

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


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);
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 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 {

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.paul.sertest.servlet.LoginServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/api/login</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>CorsFilter</filter-name>
        <filter-class>com.paul.sertest.filter.CorsFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>CorsFilter</filter-name>
        <url-pattern>/api/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>SignFilter</filter-name>
        <filter-class>com.paul.sertest.filter.SignFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>utf-8</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>SignFilter</filter-name>
        <url-pattern>/api/check/*</url-pattern>
        <url-pattern>/api/bussin/*</url-pattern>
    </filter-mapping>

</web-app>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

其中 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;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

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

protected Header ensureHeader() {
        if (this.header == null) {
            this.header = new DefaultHeader();
        }
        return this.header;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果没有设置自定义 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);
        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

最终通过 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

接着同理将 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}. <b>This is not a JDK standard
     * algorithm and requires that a JCA provider like BouncyCastle be in the classpath.</b>  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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

那 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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

构造函数 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.");
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

原来在工厂中根据不同算法实例化不同的签名对象 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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28

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


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


文章最后发布于: 2017-11-14 10:30:17
展开阅读全文
0 个人打赏
私信求帮助

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 1024 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览