Web安全通讯之JWT的Java实现
上篇文章中目的是介绍 Json Web Token(以下简称 jwt) ,由于我对 Java 比较熟悉就介绍 Java 服务端 的实现方式,其他语言原理是相同的哈~
PS:如果不清楚JWT,请先看 《Web安全通讯之Token与JWT》
- 官网地址: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 仓库(推荐)
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
- 直接导入 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
<?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>
其中 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}. <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); }
那 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 数组,最后转字符串啦,签名代码到这里基本结束了。
上面提到的类如图:
JJWT 的验证代码就不讲解了,原理是:取出 header 部分和 playload 部分,根据 header 定义的算法再一次签名,比较这个签名是否和 JWT 自带的签名是否完全相同,验证是否成功。