JWT json web token
1.身份验证的两种方式
1.1、cookie+session

原理过程:服务端产生session后,将sessionid通过cookie发送到客户端,客户端携带sessionid便能顺利找到session。
存在问题:集群环境下,如果访问没有session生成的机器,将会造成用户状态丢失;单点登录(一个系统如果有多个子系统,在其中一个子系统中登录,其他受信任的子系统无需再次登录,例如百度文库登陆后,百度知道不必再次登录)
解决方案:
session共享办法,集群搭建时可以配置,缺点:同步速度慢,消费性能。
session持久化,缺点:工程量较大,持久层挂掉依旧会造成用户状态丢失。
负载均衡 ip_hash:根据ip分配服务器,不会再访问别的服务器。
1.2、jwt验证机制
2、jwt验证机制
2.1工作原理:

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。
1 2 3 4 5
| { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分" }
|
以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
2.2.JWT 的数据结构
实际的 JWT 大概就像下面这样。(一般都是签名后的token)

它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
1 2 3
| Header(头部) Payload(负载) Signature(签名)
|
写成一行,就是下面的样子。
1
| Header.Payload.Signature
|
Header 部分是一个 JSON 对象,描述 JWT 的元数据(描述数据的数据),通常是下面的样子
1 2 3 4
| { "alg": "HS256", "typ": "JWT" }
|
上面代码中,alg属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。
2.2.2.Payload
Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。
1 2 3 4 5 6 7
| iss (issuer):签发人 exp (expiration time):过期时间 sub (subject):主题 aud (audience):受众 nbf (Not Before):生效时间 iat (Issued At):签发时间 jti (JWT ID):编号
|
除了官方字段,你还可以在这个部分定义私有字段,下面就是一个例子。
1 2 3 4 5
| { "sub": "1234567890", "name": "John Doe", "admin": true }
|
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。
2.2.3.Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
1 2 3 4
| HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
|
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用”点”(.)分隔,就可以返回给用户。
2.2.4.Base64URL
前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。
JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。这就是 Base64URL 算法。
2.3 springboot整合jwt
2.3.1pom文件中添加如下依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.2.0</version> </dependency>
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency> <!--解决jdk版本过高导致java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter--> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-impl</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>com.sun.xml.bind</groupId> <artifactId>jaxb-core</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>javax.activation</groupId> <artifactId>activation</artifactId> <version>1.1.1</version> </dependency>
|
2.3.2全局配置文件application.properties
1 2 3 4 5 6 7 8 9 10
| #jwt #header:凭证(校验的变量名) app.jwt.header = token #过期时间,单位:s app.jwt.expire = 7200
##设置密钥(随机字符串) app.jwt.secret= luguodejiamianqishi
|
2.3.3 jwt配置文件
2.3.3.1Jwt.java(jwt类用以创建或初始化jwt)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
| package com.hybs.config;
import java.util.Date;
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm;
/** * JWT类
*/ @Component @ConfigurationProperties(prefix = "app.jwt") public class JWT { private Logger logger = LoggerFactory.getLogger(getClass());
/** * 加密秘钥 */ private String secret; /** * 有效时间 */ private long expire; /** * 用户凭证 */ private String header;
/** * 获取:加密秘钥 */ public String getSecret() { return secret; }
/** * 设置:加密秘钥 */ public void setSecret(String secret) { this.secret = secret; }
/** * 获取:有效期(s) * */ public long getExpire() { return expire; } /** * 设置:有效期(s) * */ public void setExpire(long expire) { this.expire = expire; }
/** * 获取:凭证 * */ public String getHeader() { return header; } /** * 设置:凭证 * */ public void setHeader(String header) { this.header = header; }
/** * 生成Token签名 * @param userId 用户ID * @return 签名字符串 */ public String generateToken(long userId) { System.out.println("header=" + getHeader() + ", expire=" + getExpire() + ", secret=" + getSecret()); Date nowDate = new Date(); // 过期时间 Date expireDate = new Date(nowDate.getTime() + expire * 1000); return Jwts.builder().setHeaderParam("typ", "JWT").setSubject(String.valueOf(userId)).setIssuedAt(nowDate) .setExpiration(expireDate).signWith(SignatureAlgorithm.HS512, getSecret()).compact(); // 注意: JDK版本高于1.8, 缺少 javax.xml.bind.DatatypeConverter jar包,编译出错 }
/** * 获取签名信息 * @param token */ public Claims getClaimByToken(String token) { try { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } catch (Exception e) { logger.debug("validate is token error ", e); return null; } }
/** * 判断Token是否过期 * @param expiration * @return true 过期 */ public boolean isTokenExpired(Date expiration) { return expiration.before(new Date()); }
}
|
2.3.3.2JwtInterceptor (拦截器用以判断jwt是否有效以决定是否放行)
配置好拦截器后会以preHandle作为函数入口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| package com.hybs.config;
import io.jsonwebtoken.Claims; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;
/** * Token验证拦截器 */ @Component public class JwtInterceptor extends HandlerInterceptorAdapter {
@Autowired private JWT jwt;
public static final String USER_KEY = "userId";
@Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String servletPath = request.getServletPath(); System.out.println("ServletPath: " + servletPath); // 不需要验证,直接放行 boolean isNotCheck = isNotCheck(servletPath); if (isNotCheck) { return true; } // 需要验证 String token = getToken(request);
if (StringUtils.isEmpty(token)) { throw new RuntimeException(jwt.getHeader() + "失效,请重新登录"); } // 获取签名用户信息 Claims claims = jwt.getClaimByToken(token); System.out.println("TOKEN: " + claims); // 判断签名是否存在或过期 boolean b = claims==null || claims.isEmpty() || jwt.isTokenExpired(claims.getExpiration()); if (b) { throw new RuntimeException(jwt.getHeader() + "失效,请重新登录"); } // 将签名中获取的用户信息放入request中; request.setAttribute(USER_KEY, claims.getSubject()); return true; }
/** * 根据URL判断当前请求是否不需要校验, true:需要校验 */ public boolean isNotCheck(String servletPath) { // 若 请求接口 以 / 结尾, 则去掉 / servletPath = servletPath.endsWith("/") ? servletPath.substring(0,servletPath.lastIndexOf("/")) : servletPath; System.out.println("servletPath = " + servletPath); for (String path : NOT_CHECK_URL) { System.out.println("path = " + path); // path 以 /** 结尾, servletPath 以 path 前缀开头 if (path.endsWith("/**")) { String pathStart = path.substring(0, path.lastIndexOf("/")+1); System.out.println("pathStart = " + pathStart); if (servletPath.startsWith(pathStart)) { return true; } String pathStart2 = path.substring(0, path.lastIndexOf("/")); System.out.println("pathStart2 = " + pathStart2); if (servletPath.equals(pathStart2)) { return true; } } // servletPath == path if (servletPath.equals(path)) { return true; } } return false; }
/** * 获取请求Token */ private String getToken(HttpServletRequest request) { String token = request.getHeader(jwt.getHeader()); if (StringUtils.isEmpty(token)) { token = request.getParameter(jwt.getHeader()); } return token; }
/** * 不用拦截的页面路径(也可存入数据库中), 不要以 / 结尾 */ private static final String[] NOT_CHECK_URL = {"/app/user/test/**", "/app/user/login/**"};
}
|
2.3.3.3WebConfig(springboot配置拦截器使拦截器生效)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration public class WebConfig implements WebMvcConfigurer {
@Autowired private JwtInterceptor jwtInterceptor;
/** * APP接口拦截器 * */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor).addPathPatterns("/app/**"); }
}
|
2.3.3.4撰写控制器接口进行测试
基础控制器,根据当前用户token获取用户数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import com.hybs.config.JwtInterceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
public abstract class BaseController { protected Logger logger = LoggerFactory.getLogger(getClass());
/** * 获取当前登录用户ID */ protected Long getUserId(HttpServletRequest request) {
return Long.parseLong(request.getAttribute(JwtInterceptor.USER_KEY).toString()); } }
|
用户控制器,为新用户生成token
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import com.hybs.config.JWT; import com.hybs.util.HybsResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import java.util.HashMap;
@RestController @RequestMapping("/app/user") public class AppUserController extends BaseController{ @Autowired private JWT jwt; /** * 获取用户信息 * @return */ @GetMapping("info") public HybsResult info(HttpServletRequest request) { return HybsResult.ok(getUserId(request)); } /** * 用户登录 * @return */ @PostMapping("/login/{id}") public HybsResult login(@PathVariable("id") Integer id) { //生成token String token = jwt.generateToken(id); HashMap<String,Object> map = new HashMap<String,Object>(); map.put("expire", jwt.getExpire()); map.put("token", token); return HybsResult.ok(map); } }
|
2.3 整体步骤
- 用户登录时生成token
- 用户再次访问时请求头携带token
- 拦截器拦截到请求,解析请求头,获取到token
- 判断签名信息是否有效
- 将用户信息抽出放入request中
注:token根据密钥进行签名
第三部分是签名信息不是签名,如密钥