JTW官网:https://jwt.io/introduction/
目录
一、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依赖:
<!--引入JWT依赖,由于是基于Java,所以需要的是java-jwt-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
swagger配置:将token验证放在header里
@Configuration
@EnableSwagger2
public class swaggerConfig {
@Bean
public Docket createRestApi() {
ParameterBuilder tokenPar = new ParameterBuilder();
List<Parameter> pars = new ArrayList<Parameter>();
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 {
@Autowired
private UserDao userDao;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public 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<User>().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;
}
}
@Override
public void destroy() {
}
}
controller层里两个接口:一个登录、一个验证。注意一定要给token一个有效时间,否则签发的token将一直存在。
@RestController
@RequestMapping("login")
public class LoginController {
//过期时间
private static long time=1000*60;
//密钥
private static String secret="1234";
@Autowired
private 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<String, Object> payloadClaims = new HashMap();
private Map<String, Object> 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<String, Object> headerClaims, Map<String, Object> 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。