1、jwt简介
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库J
2、JWT的主要应用场景
- 身份认证
在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 - 信息交换
在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
3、JWT的结构
Header
在header中通常包含了两部分:token类型和采用的加密算法。
{ "alg": "HS256", "typ": "JWT" }
接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。
Payload
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim: reserved, public 和 private.
- Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者), exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间)。
- Public claims:根据需要定义自己的字段,注意应该避免冲突
- Private claims:这些是自定义的字段,可以用来在双方之间交换信息
负载使用的例子:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
上述的负载需要经过Base64Url编码后作为JWT结构的第二部分。
Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建:
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名用于验证消息的发送者以及消息是没有经过篡改的。
4、jwt实例
4.1、构建项目
pom中加入相关依赖
<!--jwt依赖--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
4.2、创建token实体
package com.vesus.springbootjwt.model; import javax.persistence.*; import java.io.Serializable; @Entity @Table(name = "api_token_infos") public class TokenInfo implements Serializable { @Id @GeneratedValue @Column(name = "ati_id") private Long id; @Column(name = "ati_app_id") private String appId; @Column(name = "ati_token") private byte[] token; @Column(name = "ati_build_time") private String buildTime; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAppId() { return appId; } public void setAppId(String appId) { this.appId = appId; } public byte[] getToken() { return token; } public void setToken(byte[] token) { this.token = token; } public String getBuildTime() { return buildTime; } public void setBuildTime(String buildTime) { this.buildTime = buildTime; } }
4.3、创建token拦截器,验证token的正确性
package com.vesus.springbootjwt.intercept; import com.vesus.springbootjwt.model.TokenInfo; import com.vesus.springbootjwt.service.TokeninfoService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.BeanFactory; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.PrintWriter; import java.security.SignatureException; public class JwtTokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //自动排除生成token的路径,并且如果是options请求是cors跨域预请求,设置allow对应头信息 if(request.getRequestURI().equals("/api/token")||RequestMethod.OPTIONS.toString().equals(request.getMethod())){ return true ; } //获取头信息 final String authHeader = request.getHeader("X-YAuth-Token") ; try { //如果取出的token信息为空 if (authHeader==null||authHeader.trim()==""){ throw new SignatureException("无法获取X-YAuth-Token!"); } //获取jwt实体对象接口实例 final Claims claims = Jwts.parser().setSigningKey("Authv1.0.0").parseClaimsJws(authHeader).getBody(); //从数据库中获取token TokenInfo token = getBean(TokeninfoService.class,request).findOne(claims.getSubject()); String tokenval = new String(token.getToken()); if (tokenval==null||tokenval.trim()==""){ throw new SignatureException("无法获取token信息,请重新获取!"); } //token是否与客户端传来的一致 if(!tokenval.equals(authHeader)){ throw new SignatureException("无法获取token信息,请重新获取!"); } }catch (SignatureException | ExpiredJwtException e){ //输出对象 PrintWriter writer = response.getWriter(); //输出error消息 writer.write("需要输入token"); writer.close(); return false; } //出现异常时 catch (final Exception e) { //输出对象 PrintWriter writer = response.getWriter(); //输出error消息 writer.write(e.getMessage()); writer.close(); return false; } return true; } /** * 根据传入的类型获取spring管理的对应bean * @param clazz 类型 * @param request 请求对象 * @param <T> * @return */ private <T> T getBean(Class<T> clazz ,HttpServletRequest request){ BeanFactory factory = (BeanFactory) WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext()); return factory.getBean(clazz); } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
4.4、创建token的控制器,用来获取新的token
package com.vesus.springbootjwt.controller; import com.vesus.springbootjwt.model.TokenInfo; import com.vesus.springbootjwt.model.TokenResult; import com.vesus.springbootjwt.model.UserInfo; import com.vesus.springbootjwt.service.TokeninfoService; import com.vesus.springbootjwt.service.UserService; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Date; import java.util.concurrent.TimeUnit; @RestController @RequestMapping(value = "/api") public class TokenController { @Autowired private TokeninfoService tokeninfoService ; @Autowired private UserService userService ; /** * 获取token,更新token * @param appId 用户编号 * @param appSecret 用户密码 * @return */ @RequestMapping(value = "/token" ,method = {RequestMethod.POST,RequestMethod.GET}) public TokenResult token(@RequestParam String appId , @RequestParam String appSecret){ TokenResult token = new TokenResult(); //判断appid是否为空 if(appId == null || appId.trim() == "") { token.setFlag(false); token.setMsg("appId is not found!"); } //判断appSecret是否为空 else if(appSecret == null || appSecret.trim() == "") { token.setFlag(false); token.setMsg("appSecret is not found!"); }else{ UserInfo userInfo = userService.findOne(appId); //如果用户不存在 if (userInfo == null) { token.setFlag(false); token.setMsg("appId : " + appId + ", 缺失!"); }//验证appSecret是否存在 else if (!new String(userInfo.getAppSecret()).equals(appSecret.replace(" ","+"))) { token.setFlag(false); token.setMsg("appSecret无效!"); } else { TokenInfo tokenInfo = tokeninfoService.findOne(appId); //返回token值 String tokenStr = null; if (tokenInfo==null){ //生成Token tokenStr = createNewToken(appId) ; //将token保持到数据库 tokenInfo = new TokenInfo(); tokenInfo.setAppId(userInfo.getAppId()); tokenInfo.setBuildTime(String.valueOf(System.currentTimeMillis())); tokenInfo.setToken(tokenStr.getBytes()); tokeninfoService.saveToken(tokenInfo); } else { //判断数据库中token是否过期,如果没有过期不需要更新直接返回数据库中的token即可 //数据库中生成时间 long dbBuildTime = Long.valueOf(tokenInfo.getBuildTime()); //当前时间 long currentTime = System.currentTimeMillis(); //如果当前时间 - 数据库中生成时间 < 7200 证明可以正常使用 long second = TimeUnit.MILLISECONDS.toSeconds(currentTime - dbBuildTime); if (second > 0 && second < 7200) { tokenStr = new String(tokenInfo.getToken()); } //超时 else{ //生成newToken tokenStr = createNewToken(appId); //更新token tokenInfo.setToken(tokenStr.getBytes()); //更新生成时间 tokenInfo.setBuildTime(String.valueOf(System.currentTimeMillis())); //执行更新 tokeninfoService.saveToken(tokenInfo); } } //设置返回token token.setToken(tokenStr); } } return token; } /** * 创建新token * @param appId * @return */ private String createNewToken(String appId){ //获取当前时间 Date now = new Date(System.currentTimeMillis()); //过期时间 Date expiration = new Date(now.getTime()+7200000); return Jwts.builder().setSubject(appId) .setIssuedAt(now).setIssuer("Online YAuth Builder") .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256,"Authv1.0.0") .compact(); } }
4.5、启动应用
拿到token
访问http://localhost:8080/api/user,带上获取的token
源码:https://gitee.com/vesus198/springboot-demo/tree/master/springboot-jwt