JWT 简介
什么是 JWT
JWT 是 JSON Web Token 的缩写,是为了在网络应用环境间传递声明而执行的一种基于 JSON
的开放标准((RFC 7519)。定义了一种简洁的,自包含的方法用于通讯双方之间以 JSON
对象的形式安全的传递信息。由于数字签名的存在,这些信息是可信的,JWT 可使用 HMAC
算法或者是 RSA
的公私秘钥对进行签名。javascript
JWT请求流程
- 用户使用帐号和密码发起 POST 请求;
- 服务器使用私钥建立一个 JWT;
- 服务器返回这个 JWT 给浏览器;
- 浏览器将该 JWT 串在请求头中像服务器发送请求;
- 服务器验证该 JWT;
- 返回响应的资源给浏览器。
JWT 的主要应用场景
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每一个请求中包含 JWT,能够用来验证用户身份以及对路由,服务和资源的访问权限进行验证。因为它的开销很是小,能够轻松的在不一样域名的系统中传递,全部目前在单点登陆(SSO)中比较普遍的使用了该技术。 信息交换在通讯的双方之间使用 JWT 对数据进行编码是一种很是安全的方式,因为它的信息是通过签名的,能够确保发送者发送的信息是没有通过伪造的。java
JWT 数据结构
JWT 是由三段信息构成的,将这三段信息文本用 .
链接一块儿就构成了 JWT 字符串。git
JWT 的三个部分依次为头部:Header,负载:Payload 和签名:Signature。算法
Header
Header 部分是一个 JSON 对象,描述 JWT 的元数据,一般是下面的样子。spring
{ "alg": "HS256", "typ": "JWT" }
上面代码中,alg
属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ
属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT
。shell
最后,将上面的 JSON 对象使用 Base64URL 算法转成字符串。数据库
Payload
Payload 部分也是一个 JSON 对象,用来存放实际须要传递的有效信息。有效信息包含三个部分:json
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :api
- iss (issuer):签发人
- exp (expiration time):过时时间,必需要大于签发时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号,JWT 的惟一身份标识,主要用来做为一次性
token
,从而回避重放攻击。
公共的声明 :浏览器
公共的声明能够添加任何的信息,通常添加用户的相关信息或其余业务须要的必要信息。但不建议添加敏感信息,由于该部分在客户端可解密。
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,通常不建议存放敏感信息,由于 base64
是对称解码的,意味着该部分信息能够归类为明文信息。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,须要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。而后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
算出签名之后,把 Header、Payload、Signature 三个部分拼成一个字符串,每一个部分之间用"点"(.
)分隔,就能够返回给用户。
Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本相似,但有一些小的不一样。
JWT 做为一个令牌(token),有些场合可能会放到 URL(好比 api.example.com/?token=xxx
)。Base64 有三个字符 +
、 /
和 =
,在 URL 里面有特殊含义,因此要被替换掉:=
被省略、+
替换成 -
,/
替换成 _
。这就是 Base64URL 算法。
JWT 的使用方式
客户端收到服务器返回的 JWT 以后须要在本地作保存。此后,客户端每次与服务器通讯,都要带上这个 JWT。通常的的作法是放在 HTTP 请求的头信息 Authorization
字段里面。
Authorization: Bearer <token>
这样每一个请求中,服务端就能够在请求头中拿到 JWT 进行解析与认证。
JWT 的特性
-
JWT 默认是不加密,但也是能够加密的。生成原始 Token 之后,能够用密钥再加密一次。
-
JWT 不加密的状况下,不能将秘密数据写入 JWT。
-
JWT 不只能够用于认证,也能够用于交换信息。有效使用 JWT,能够下降服务器查询数据库的次数。
-
JWT 的最大缺点是,因为服务器不保存 session 状态,所以没法在使用过程当中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期以前就会始终有效,除非服务器部署额外的逻辑。
-
JWT 自己包含了认证信息,一旦泄露,任何人均可以得到该令牌的全部权限。为了减小盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
-
为了减小盗用,JWT 不该该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
基于 nimbus-jose-jwt 简单封装
nimbus-jose-jwt 是最受欢迎的 JWT 开源库,基于Apache 2.0开源协议,支持全部标准的签名(JWS)和加密(JWE)算法。nimbus-jose-jwt 支持使用对称加密(HMAC)和非对称加密(RSA)两种算法来生成和解析 JWT 令牌。
下面咱们对 nimbus-jose-jwt 进行简单的封装,提供如下功能的支持:
- 支持使用 HMAC 和 RSA 算法生成和解析 JWT 令牌
- 支持私有信息直接做为 Payload,以及标准信息+私有信息做为 Payload。内置支持后者。
- 提供工具类及可扩展接口,方便自定义扩展开发。
pom 中添加依赖
首先咱们在 pom.xml 中引入 nimbus-jose-jwt 的依赖。
<dependency> <groupid>com.nimbusds</groupid> <artifactid>nimbus-jose-jwt</artifactid> <version>8.20</version> </dependency>
JwtConfig
这个类用于统一管理相关的参数配置。
public class JwtConfig { // JWT 在 HTTP HEADER 中默认的 KEY private String tokenName = JwtUtils.DEFAULT_TOKEN_NAME; // HMAC 密钥,用于支持 HMAC 算法 private String hmacKey; // JKS 密钥路径,用于支持 RSA 算法 private String jksFileName; // JKS 密钥密码,用于支持 RSA 算法 private String jksPassword; // 证书密码,用于支持 RSA 算法 private String certPassword; // JWT 标准信息:签发人 - iss private String issuer; // JWT 标准信息:主题 - sub private String subject; // JWT 标准信息:受众 - aud private String audience; // JWT 标准信息:生效时间 - nbf,将来多长时间内生效 private long notBeforeIn; // JWT 标准信息:生效时间 - nbf,具体哪一个时间生效 private long notBeforeAt; // JWT 标准信息:过时时间 - exp,将来多长时间内过时 private long expiredIn; // JWT 标准信息:过时时间 - exp,具体哪一个时间过时 private long expiredAt; }
hmacKey
字段用于支持 HMAC 算法,只要该字段不为空,则使用该值做为 HMAC 的密钥对 JWT 进行签名与验证。
jksFileName
、jksPassword
、certPassword
三个字段用于支持 RSA 算法,程序将读取证书文件做为 RSA 密钥对 JWT 进行签名与验证。
其余几个字段用于设置 Payload 中须要携带的标准信息。
JwtService
JwtService 是提供 JWT 签名与验证的接口,内置了 HMACJwtServiceImpl 提供 HMAC 算法的实现和 RSAJwtServiceImpl 提供 RSA 算法的实现。两种算法在获取密钥的方式上是有差异的,这里也提出来成了接口方法。后续若是要自定义实现,只须要再写一个具体实现类。
public interface JwtService { /** * 获取 key * * @return */ Object genKey(); /** * 对信息进行签名 * * @param payload * @return */ String sign(String payload); /** * 验证并返回信息 * * @param token * @return */ String verify(String token); }
public class HMACJwtServiceImpl implements JwtService { private JwtConfig jwtConfig; public HMACJwtServiceImpl(JwtConfig jwtConfig) { this.jwtConfig = jwtConfig; } @Override public String genKey() { String key = jwtConfig.getHmacKey(); if (JwtUtils.isEmpty(key)) { throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, new NullPointerException("HMAC need a key")); } return key; } @Override public String sign(String info) { return JwtUtils.signClaimByHMAC(info, genKey(), jwtConfig); } @Override public String verify(String token) { return JwtUtils.verifyClaimByHMAC(token, genKey(), jwtConfig); } }
public class RSAJwtServiceImpl implements JwtService { private JwtConfig jwtConfig; private RSAKey rsaKey; public RSAJwtServiceImpl(JwtConfig jwtConfig) { this.jwtConfig = jwtConfig; } private InputStream getCertInputStream() throws IOException { // 读取配置文件中的证书路径 String jksFile = jwtConfig.getJksFileName(); if (jksFile.contains("://")) { // 从本地文件读取 return new FileInputStream(new File(jksFile)); } else { // 从 classpath 读取 return getClass().getClassLoader().getResourceAsStream(jwtConfig.getJksFileName()); } } @Override public RSAKey genKey() { if (rsaKey != null) { return rsaKey; } InputStream is = null; try { KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); is = getCertInputStream(); keyStore.load(is, jwtConfig.getJksPassword().toCharArray()); Enumeration<string> aliases = keyStore.aliases(); String alias = null; while (aliases.hasMoreElements()) { alias = aliases.nextElement(); } RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey(alias, jwtConfig.getCertPassword().toCharArray()); Certificate certificate = keyStore.getCertificate(alias); RSAPublicKey publicKey = (RSAPublicKey) certificate.getPublicKey(); rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).build(); return rsaKey; } catch (IOException | CertificateException | UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) { e.printStackTrace(); throw new KeyGenerateException(JwtUtils.KEY_GEN_ERROR, e); } finally { if (is != null) { try { is.close(); } catch (IOException e) { e.printStackTrace(); } } } } @Override public String sign(String payload) { return JwtUtils.signClaimByRSA(payload, genKey(), jwtConfig); } @Override public String verify(String token) { return JwtUtils.verifyClaimByRSA(token, genKey(), jwtConfig); } }
JwtUtils
JwtService 的实现类中比较简洁,由于主要的方法都在 JwtUtils 中提供了。以下是 Payload 中只包含私有信息时,两种算法的签名与验证明现。可使用这些方法方便的实现本身的扩展。
/** * 使用 HMAC 算法签名信息(Payload 中只包含私有信息) * * @param info * @param key * @return */ public static String signDirectByHMAC(String info, String key) { try { JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.HS256) .type(JOSEObjectType.JWT) .build(); // 创建一个载荷 Payload Payload payload = new Payload(info); // 将头部和载荷结合在一块儿 JWSObject jwsObject = new JWSObject(jwsHeader, payload); // 创建一个密匙 JWSSigner jwsSigner = new MACSigner(key); // 签名 jwsObject.sign(jwsSigner); // 生成 token return jwsObject.serialize(); } catch (JOSEException e) { e.printStackTrace(); throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e); } } /** * 使用 RSA 算法签名信息(Payload 中只包含私有信息) * * @param info * @param rsaKey * @return */ public static String signDirectByRSA(String info, RSAKey rsaKey) { try { JWSSigner signer = new RSASSASigner(rsaKey); JWSObject jwsObject = new JWSObject( new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID()).build(), new Payload(info) ); // 进行加密 jwsObject.sign(signer); return jwsObject.serialize(); } catch (JOSEException e) { e.printStackTrace(); throw new PayloadSignException(JwtUtils.PAYLOAD_SIGN_ERROR, e); } } /** * 使用 HMAC 算法验证 token(Payload 中只包含私有信息) * * @param token * @param key * @return */ public static String verifyDirectByHMAC(String token, String key) { try { JWSObject jwsObject = JWSObject.parse(token); // 创建一个解锁密匙 JWSVerifier jwsVerifier = new MACVerifier(key); if (jwsObject.verify(jwsVerifier)) { return jwsObject.getPayload().toString(); } throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null")); } catch (JOSEException | ParseException e) { e.printStackTrace(); throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e); } } /** * 使用 RSA 算法验证 token(Payload 中只包含私有信息) * * @param token * @param rsaKey * @return */ public static String verifyDirectByRSA(String token, RSAKey rsaKey) { try { RSAKey publicRSAKey = rsaKey.toPublicJWK(); JWSObject jwsObject = JWSObject.parse(token); JWSVerifier jwsVerifier = new RSASSAVerifier(publicRSAKey); // 验证数据 if (jwsObject.verify(jwsVerifier)) { return jwsObject.getPayload().toString(); } throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, new NullPointerException("Payload can not be null")); } catch (JOSEException | ParseException e) { e.printStackTrace(); throw new TokenVerifyException(JwtUtils.TOKEN_VERIFY_ERROR, e); } }
JwtException
定义统一的异常类,能够屏蔽 nimbus-jose-jwt 以及其余诸如加载证书错误抛出的异常,而且在其余项目集成咱们封装好的库的时候,方便的进行异常处理。
在 JwtService 实现的不一样阶段,咱们封装了不一样的 JwtException 子类,来方便外部根据须要作对应的处理。如异常是 KeyGenerateException,则处理成服务器处理错误;如异常是 TokenVerifyException,则处理成 Token 验证失败,无权限。
JwtContext
JWT 用于用户认证,常常在 Token 验证完成后,程序中须要获取到当前登陆的用户信息, JwtContext 中提供了经过线程局部变量保存信息的方法。
public class JwtContext { private static final String KEY_TOKEN = "token"; private static final String KEY_PAYLOAD = "payload"; private static ThreadLocal<map<object, object>> context = new ThreadLocal<>(); private JwtContext() {} public static void set(Object key, Object value) { Map<object, object> locals = context.get(); if (locals == null) { locals = new HashMap<>(); context.set(locals); } locals.put(key, value); } public static Object get(Object key) { Map<object, object> locals = context.get(); if (locals != null) { return locals.get(key); } return null; } public static void remove(Object key) { Map<object, object> locals = context.get(); if (locals != null) { locals.remove(key); if (locals.isEmpty()) { context.remove(); } } } public static void removeAll() { Map<object, object> locals = context.get(); if (locals != null) { locals.clear(); } context.remove(); } public static void setToken(String token) { set(KEY_TOKEN, token); } public static String getToken() { return (String) get(KEY_TOKEN); } public static void setPayload(Object payload) { set(KEY_PAYLOAD, payload); } public static Object getPayload() { return get(KEY_PAYLOAD); } }
@AuthRequired
在项目实战中,并非全部 Controller 中的方法都必须传 Token,经过 @AuthRequired 注解来区分方法是否须要校验 Token。
/** * 应用于 Controller 中的方法,标识是否拦截进行 JWT 验证 */ @Target({ElementType.METHOD, ElementType.TYPE}) public @interface AuthRequired { boolean required() default true; }
Spring Boot 集成 JWT 实例
有了上面封装好的库,咱们在 SpringBoot 项目中集成 JWT。建立好 Spring Boot 项目后,咱们编写下面主要的类。
JwtDemoInterceptor
在 Spring Boot 项目中,经过自定义 HandlerInterceptor 的实现类能够对请求和响应进行拦截,咱们新建 JwtDemoInterceptor 类进行拦截。
public class JwtDemoInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(JwtDemoInterceptor.class); private static final String PREFIX_BEARER = "Bearer "; @Autowired private JwtConfig jwtConfig; @Autowired private JwtService jwtService; /** * 预处理回调方法,实现处理器的预处理(如检查登录),第三个参数为响应的处理器,自定义 Controller * 返回值: * true 表示继续流程(如调用下一个拦截器或处理器); * false 表示流程中断(如登陆检查失败),不会继续调用其余的拦截器或处理器,此时咱们须要经过 response 来产生响应。 */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 若是不是映射到方法直接经过 if(!(handler instanceof HandlerMethod)){ return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); // 检查是否有 @AuthRequired 注解,有且 required() 为 false 则跳过 if (method.isAnnotationPresent(AuthRequired.class)) { AuthRequired authRequired = method.getAnnotation(AuthRequired.class); if (!authRequired.required()) { return true; } } String token = request.getHeader(jwtConfig.getTokenName()); logger.info("token: {}", token); if (StringUtils.isEmpty(token) || token.trim().equals(PREFIX_BEARER.trim())) { return true; } token = token.replace(PREFIX_BEARER, ""); String payload = jwtService.verify(token); // 设置线程局部变量中的 token JwtContext.setToken(token); JwtContext.setPayload(payload); return true; } /** * 后处理回调方法,实现处理器的后处理(但在渲染视图以前),此时咱们能够经过 modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView 也可能为null。 */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中咱们能够在此记录结束时间并输出消耗时间,还能够进行一些资源清理,相似于 try-catch-finally 中的 finally * 但仅调用处理器执行链中 preHandle 返回 true 的拦截器的 afterCompletion。 */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { JwtContext.removeAll(); } }
preHandle
、postHandle
、afterCompletion
三个方法的具体做用,能够看代码上的注释。
preHandle
中这段代码中的逻辑以下:
- 拦截被 @AuthRequired 注解的方法,只要不是
required = false
都会进行 Token 的校验。 - 从请求中解析出 Token,对 Token 进行验证。若是验证异常,会在方法中抛出异常。
- Token 验证经过,会在线程局部变量中设置相关信息,以便后续程序获取处理。
afterCompletion
中这段代码对线程变量进行了清理。
InterceptorConfig
定义 InterceptorConfig,经过 @Configuration 注解,Spring 会加载该类,并完成装配。
addInterceptors
方法中设置拦截器,并拦截全部请求。
jwtDemoConfig
方法中注入 JwtConfig,并设置了 HMACKey。
jwtDemoService
方法会根据注入的 JwtConfig 配置,生成具体的 JwtService,这里是 HMACJwtServiceImpl。
@Configuration public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtDemoInterceptor()).addPathPatterns("/**"); } @Bean public JwtDemoInterceptor jwtDemoInterceptor() { return new JwtDemoInterceptor(); } @Bean public JwtConfig jwtDemoConfig() { JwtConfig jwtConfig = new JwtConfig(); jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb"); return jwtConfig; } @Bean public JwtService jwtDemoService() { return JwtUtils.obtainJwtService(jwtDemoConfig()); } }
编写测试 Controller
@RestController public class UserController { @Autowired private ObjectMapper objectMapper; @Autowired private JwtService jwtService; @GetMapping("/sign") @AuthRequired(required = false) public String sign() throws JsonProcessingException { UserDTO userDTO = new UserDTO(); userDTO.setName("fatfoo"); userDTO.setPassword("112233"); userDTO.setSex(0); String payload = objectMapper.writeValueAsString(userDTO); return jwtService.sign(payload); } @GetMapping("/verify") public UserDTO verify() throws IOException { String payload = (String) JwtContext.getPayload(); return objectMapper.readValue(payload, UserDTO.class); } }
sign
方法对用户信息进行签名并返回 Token;因为 @AuthRequired(required = false)
拦截器将不会对其进行拦截。
verify
方法在 Token 经过验证后,获取解析出的信息并返回。
用 Postman 进行测试
访问 sign 接口,返回签名 Token。
在 Header 中添加 Token 信息,请求 verify 接口,返回用户信息。
测试 RSA 算法实现
上面咱们只设置了 JwtConfig 的 hmacKey 参数,使用的是 HMAC 算法进行签名和验证。本节咱们演示 RSA 算法进行签名和验证的实现。
生成签名文件
使用 Java 自带的 keytool 工具能够方便的生成证书文件。
➜ resources git:(master) ✗ keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks 输入密钥库口令: 密钥库口令过短 - 至少必须为 6 个字符 输入密钥库口令: ronjwt 再次输入新口令: ronjwt 您的名字与姓氏是什么? [Unknown]: ron 您的组织单位名称是什么? [Unknown]: ron 您的组织名称是什么? [Unknown]: ron 您所在的城市或区域名称是什么? [Unknown]: Xiamen 您所在的省/市/自治区名称是什么? [Unknown]: Fujian 该单位的双字母国家/地区代码是什么? [Unknown]: CN CN=ron, OU=ron, O=ron, L=Xiamen, ST=Fujian, C=CN是否正确? [否]: 是 输入 <jwt> 的密钥口令 (若是和密钥库口令相同, 按回车): Warning: JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore jwt.jks -destkeystore jwt.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。
文件生成后,复制到项目的 resources 目录下。
设置 JwtConfig 参数
修改上节 InterceptorConfig 中的 jwtDemoConfig
方法,这是 jksFileName、jksPassword、certPassword 3 个参数。
@Bean public JwtConfig jwtDemoConfig() { JwtConfig jwtConfig = new JwtConfig(); // jwtConfig.setHmacKey("cb9915297c8b43e820afd2a90a1e36cb"); jwtConfig.setJksFileName("jwt.jks"); jwtConfig.setJksPassword("ronjwt"); jwtConfig.setCertPassword("ronjwt"); return jwtConfig; }
不要设置 hmacKey 参数,不然会加载 HMACJwtServiceImpl。由于 JwtUtils#obtainJwtService
方法实现以下:
/** * 获取内置 JwtService 的工厂方法。 * * 优先采用 HMAC 算法实现 * * @param jwtConfig * @return */ public static JwtService obtainJwtService(JwtConfig jwtConfig) { if (!JwtUtils.isEmpty(jwtConfig.getHmacKey())) { return new HMACJwtServiceImpl(jwtConfig); } return new RSAJwtServiceImpl(jwtConfig); }
这样就能够进行 RSA 算法签名与验证的测试了。运行程序并使用 Postman 测试,请自行查看区别。
- End -
本文只是 Spring Boot 集成 JWT 的第一篇,在后续咱们还将继续对这个库进行封装,构建 spring-boot-starter,自定义 @Enable 注解来方便在项目中引入。
第二篇已更新,《一行代码就能够实现 Jwt 登陆认证,爽呆了》
请关注个人公众号:精进Java(ID:craft4j),第一时间获取知识动态。
若是你对项目的完整源码感兴趣,能够在公众号中回复 jwt
来获取。