JTW官网:https://jwt.io/introduction/
目录
一、JWT基本介绍
1、什么是JWT
2、什么时候使用 JWT
3、JWT的结构
4、JWT 的特点
二、代码示例
三、源码分析
一、JWT基本介绍
1、什么是JWT
JSON Web Token(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,作为 JSON 对象在各方之间安全的传递信息。这个信息可以通过数字签名进行验证并信任。JWTs 可以使用密钥(结合 HMAC 算法)或者 使用 RSA 、 ECDSA 加密的公钥私钥对进行签名。
2、什么时候使用 JWT
- 授权:这是 JWT 最普遍的使用场景。当用户登录之后,每次请求中都包含 JWT ,服务端允许用户访问那些只有携带 token 才能访问的路由、服务、资源。目前在单点登录中广泛使用到 JWT ,因为它体积小,且能够在不同域名之间使用。
- 信息交换: JWT 是一种能够在各方之间安全传输信息的好方式。因为 JWTs 能够签名,比如使用公钥私钥对,你能够确定发送者的身份。另外,签名是使用 Header 和 Payload 通过特定算法计算而来,所以你也可以验证内容是否被篡改。
3、JWT的结构
JWT 包含三部分,之间以点(.)连接(头部.负载.签名)
- Header(头部)
- Payload(负载)
- Signature(签名)
demo:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1ODk3NzI5OTgsInVzZXJuYW1lIjoiYWFhIn0.X_wNgENIpih0SAk2pirwmusp0i0dgEXrfmDVhh__L74
Header
Header部分 是一个 JSON 对象,典型的header包含两部分:
alg:使用的签名算法,比如 HMAC SHA256 或 RSAtyp:token的类型,比如 JWT
{"alg": "HS256","typ": "JWT"
}
用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第一部分
Payload
Payload 部分也是 JSON 对象,用来存放数据。JWT 有7个官方字段:
- iss (issuer):签发人
- exp (expiration time):过期时间,以秒为单位
- iat (Issued At):签发时间,能够算出JWT的存在时间
- nbf (Not Before):生效时间
- jti (JWT ID):JWT 的唯一标识。用来防止 JWT 重复。
- sub (subject):主题(很少使用)
- aud (audience):token的受众(很少被使用)
{"sub": "1234567890","name": "John Doe","admin": true
}
除了官方设定的字段,自己也可以自定义字段, JWT 默认不加密,任何人都可以读取,所以不要把敏感信息存放在这个部分。
用 Base64Url 将这个 JSON 对象编码后,作为 JWT 的第二部分
Signature
使用 Header 指定的算法对 Header、Payload、密钥三部分进行签名,生成的字符串作为 JWT 的第三部分。
签名可以用来验证数据是否被篡改。
4、JWT 的特点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
- JWT 不加密的情况下,不能将敏感数据写入 JWT
- JWT 不仅可以用于认证,也可以用于交换信息
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦签发了 JWT ,在到期之前就会始终有效(目前有这样一个疑惑)
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
- JWT是无状态的,不能用于登录登出判断。
- 不要将token存于数据库,否则在分布式系统内失去了token存在的意义。
二、代码示例
写一个代码示例,和shiro分离开,自己写一个登录验证。
使用swagger工具进行接口测试。
JWT依赖:
io.jsonwebtoken jjwt 0.9.1 com.auth0 java-jwt 3.4.0
swagger配置:将token验证放在header里
@Configuration
@EnableSwagger2
public class swaggerConfig {@Beanpublic Docket createRestApi() {ParameterBuilder tokenPar = new ParameterBuilder();List pars = new ArrayList();tokenPar.name("Authorization").description("Authorization").modelRef(new ModelRef("string")).parameterType("header").required(false).build();pars.add(tokenPar.build());return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("mptest.mybatistest")).paths(PathSelectors.any()).build().globalOperationParameters(pars) ;}@SuppressWarnings("deprecation")private ApiInfo apiInfo() {return new ApiInfoBuilder().title("个人测试").description("个人测试用api").termsOfServiceUrl("termsOfServiceUrl").contact("测试").version("1.0").build();}}
通过过滤器来过滤token是否正确。只过滤验证的接口,不过滤登录接口。
将token中的账号解析出来,与数据库比较,再将密码作为密钥进行解密验证。token验证不通过则拦截,我只做了简单处理,具体逻辑没有写。
@WebFilter(filterName = "JWTFilter",urlPatterns = "/login/verity")
public class JWTFilter implements Filter {@Autowiredprivate UserDao userDao;@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest request1=(HttpServletRequest) request;try{ String token = request1.getHeader("Authorization");DecodedJWT jwt = JWT.decode(token);String username=jwt.getClaim("username").asString();User user = userDao.selectOne(new QueryWrapper().eq("username",username).last("limit 1"));JWTVerifier verifier= JWT.require(Algorithm.HMAC256(user.getPassword())).build();jwt = verifier.verify(token);chain.doFilter(request, response);}catch (Exception e){ //之后修改的内容HttpServletResponse httpServletResponse = (HttpServletResponse) response;String json="{\"code\":401,\"message\":\"token验证失败\"}";httpServletResponse.setHeader("Content-type", "application/json;charset=UTF-8");httpServletResponse.getWriter().write(json);return;}}@Overridepublic void destroy() {}
}
controller层里两个接口:一个登录、一个验证。注意一定要给token一个有效时间,否则签发的token将一直存在。
@RestController
@RequestMapping("login")
public class LoginController {//过期时间private static long time=1000*60;//密钥private static String secret="1234";@Autowiredprivate UserDao userDao;@PostMapping("/login")public String login(User user){//指定签名算法,header部分Algorithm algorithm=Algorithm.HMAC256(user.getPassword().getBytes(StandardCharsets.UTF_8));//生成有效时间 Date expire=new Date(System.currentTimeMillis()+time);String token = JWT.create().withClaim("username",user.getUsername()).withExpiresAt(expire).sign(algorithm);System.out.println("token:"+token);return token;}@GetMapping("/verity")public String verity(){String s = "验证成功";return s;}
}
登录测试:获得token

验证测试:正确情况

验证测试:错误情况

三、源码分析
controller中有这么一行代码:
String token = JWT.create().withClaim("username",user.getUsername()).withExpiresAt(expire).sign(algorithm);
以它为起点来分析token是怎么生成的。
查看sign方法:进入到JWTCreator类
public String sign(Algorithm algorithm) throws IllegalArgumentException, JWTCreationException {if (algorithm == null) {throw new IllegalArgumentException("The Algorithm cannot be null.");} else {this.headerClaims.put("alg", algorithm.getName());this.headerClaims.put("typ", "JWT");String signingKeyId = algorithm.getSigningKeyId();if (signingKeyId != null) {this.withKeyId(signingKeyId);}return (new JWTCreator(algorithm, this.headerClaims, this.payloadClaims)).sign();}}
sign方法首先将签名的方法和jwt类型放入到了headerClaims中,然后我们看一下headerClaims这个参数。
内部静态类中有这两个map集合。headerClaims 用来存头部信息,payloadClaims 用来存负载信息。
private final Map payloadClaims = new HashMap();private Map headerClaims = new HashMap();
JWTCreator类中withClaim是用于添加负载信息的。
public JWTCreator.Builder withClaim(String name, String value) throws IllegalArgumentException {this.assertNonNull(name);this.addClaim(name, value);return this;}
JWT官方规定好的方法名和添加自定义的方法名不同。如到期时间:
public JWTCreator.Builder withExpiresAt(Date expiresAt) {this.addClaim("exp", expiresAt);return this;}
还是这个方法sign(Algorithm algorithm),返回值构造了一个JWTCreator
private JWTCreator(Algorithm algorithm, Map headerClaims, Map payloadClaims) throws JWTCreationException {this.algorithm = algorithm;try {ObjectMapper mapper = new ObjectMapper();SimpleModule module = new SimpleModule();module.addSerializer(ClaimsHolder.class, new PayloadSerializer());mapper.registerModule(module);mapper.configure(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY, true);this.headerJson = mapper.writeValueAsString(headerClaims);this.payloadJson = mapper.writeValueAsString(new ClaimsHolder(payloadClaims));} catch (JsonProcessingException var6) {throw new JWTCreationException("Some of the Claims couldn't be converted to a valid JSON format.", var6);}}
该构造方法主要是将map集合转化成了字符串
构造完成后调用了一个无参的sign方法
private String sign() throws SignatureGenerationException {String header = Base64.encodeBase64URLSafeString(this.headerJson.getBytes(StandardCharsets.UTF_8));String payload = Base64.encodeBase64URLSafeString(this.payloadJson.getBytes(StandardCharsets.UTF_8));String content = String.format("%s.%s", header, payload);byte[] signatureBytes = this.algorithm.sign(content.getBytes(StandardCharsets.UTF_8));String signature = Base64.encodeBase64URLSafeString(signatureBytes);return String.format("%s.%s", content, signature);}
该方法将头部和负载信息进行base64加密后以.隔开拼接成字符串,然后再将该字符串进行签名,将签名结果以同样的方式进行拼接,最后返回一个完整的token。