1 简介
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
2 格式
- JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
- A由JWT头部信息header经过base64加密得到
#默认的头信息 { "alg": "HS256", "typ": "JWT" } #官网测试:https://jwt.io/ #base64加密后的字符串为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- B是payload,存放有效信息的地方,这些信息包含三个部分:
- 标准中注册的声明 (建议但不强制使用)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明
- 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
- 私有的声明
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
#存放的数据: { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } #base64后的字符串为: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- C由A和B通过加密算法得到,用作对token进行校验,看是否有效
- 这个部分需要base64加密后的header和base64加密后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
#secret为:oldlu #得到的加密字符串为:DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc #整体的token为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
3 流程
4 示例
导入依赖:
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency>
编写测试用例:
package com.tanhua.sso.service; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.junit.Test; import java.util.Date; import java.util.HashMap; import java.util.Map; public class TestJWT { String secret = "oldlu"; @Test public void testCreateToken(){ Map<String, Object> header = new HashMap<String, Object>(); header.put(JwsHeader.TYPE, JwsHeader.JWT_TYPE); header.put(JwsHeader.ALGORITHM, "HS256"); Map<String, Object> claims = new HashMap<String, Object>(); claims.put("mobile", "1333333333"); claims.put("id", "2"); // 生成token String jwt = Jwts.builder() .setHeader(header) //header,可省略 .setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等 .signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐 .setExpiration(new Date(System.currentTimeMillis() + 3000)) //设置过期时间,3秒后过期 .compact(); System.out.println(jwt); } @Test public void testDecodeToken(){ String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMzMzMzMzMzMzIiwiaWQiOiIyIiwiZXhwIjoxNjA1NTEzMDA2fQ.1eG3LpudD4XBycUG39UQDaKVBQHgaup-E1OLWo_m8m8"; try { // 通过token解析数据 Map<String, Object> body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); System.out.println(body); //{mobile=1333333333, id=2, exp=1605513392} } catch (ExpiredJwtException e) { System.out.println("token已经过期!"); } catch (Exception e) { System.out.println("token不合法!"); } } }
2、校验token
在整个系统架构中,只有SSO保存了JWT中的秘钥,所以只能通过SSO系统提供的接口服务进行对token的校验,所以在SSO系统中,需要对外开放接口,通过token进行查询用户信息,如果返回null说明用户状态已过期或者是非法的token,否则返回User对象数据。
2.1、UserController
/** * 校验token,根据token查询用户数据 * * @param token * @return */ @GetMapping("{token}") public User queryUserByToken(@PathVariable("token") String token) { return this.userService.queryUserByToken(token); }
2.2、UserService
public User queryUserByToken(String token) { try { // 通过token解析数据 Map<String, Object> body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); User user = new User(); user.setId(Long.valueOf(body.get("id").toString())); //需要返回user对象中的mobile,需要查询数据库获取到mobile数据 //如果每次都查询数据库,必然会导致性能问题,需要对用户的手机号进行缓存操作 //数据缓存时,需要设置过期时间,过期时间要与token的时间一致 //如果用户修改了手机号,需要同步修改redis中的数据 String redisKey = "TANHUA_USER_MOBILE_" + user.getId(); if(this.redisTemplate.hasKey(redisKey)){ String mobile = this.redisTemplate.opsForValue().get(redisKey); user.setMobile(mobile); }else { //查询数据库 User u = this.userMapper.selectById(user.getId()); user.setMobile(u.getMobile()); //将手机号写入到redis中 //在jwt中的过期时间的单位为:秒 long timeout = Long.valueOf(body.get("exp").toString()) * 1000 - System.currentTimeMillis(); this.redisTemplate.opsForValue().set(redisKey, u.getMobile(), timeout, TimeUnit.MILLISECONDS); } return user; } catch (ExpiredJwtException e) { log.info("token已经过期! token = " + token); } catch (Exception e) { log.error("token不合法! token = "+ token, e); } return null; }
2.3、测试
数据已经存储到redis中
2.4、查询好友动态
查询好友动态其实就是查询自己的时间线表,好友在发动态时已经将动态信息写入到了自己的时间线表中。