JWT原理
JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,编码之后的JWT看起来是这样的一串字符:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
由 . 分为三段,通过解码可以得到
1. 头部(Header)
// 包括类别(typ)、加密算法(alg);
{
"alg": "HS256",
"typ": "JWT"
}
jwt的头部包含两部分信息:
声明类型,这里是jwt
声明加密的算法 通常直接使用 HMAC SHA256
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
2. 载荷(payload)
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
标准中注册的声明
公共的声明
私有的声明
标准中注册的声明 (建议但不强制使用) :
iss: 该JWT的签发者,一般是服务器,是否使用是可选的;
iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;
exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;
aud: 接收该JWT的一方,是否使用是可选的;
sub: 该JWT所面向的用户,userid,是否使用是可选的;
其他还有:
nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
// 包括需要传递的用户信息;
{ "iss": "Online JWT Builder",
"iat": 1416797419,
"exp": 1448333419,
"aud": "www.gusibi.com",
"sub": "uid",
"nickname": "goodspeed",
"username": "goodspeed",
"scopes": [ "admin", "user" ]
}
将上面的JSON对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。
eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0
信息会暴露:由于这里用的是可逆的base64 编码,所以第二部分的数据实际上是明文的。我们应该避免在这里存放不能公开的隐私信息。
3. 签名(signature)
// 根据alg算法与私有秘钥进行加密得到的签名字串;
// 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
SECREATE_KEY
)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
var signature = HMACSHA256(encodedString, 'secret'); // pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o
将这三部分用.连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o
签名的目的:签名实际上是对头部以及载荷内容进行签名。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的密钥的话,得出来的签名也一定会是不一样的。
这样就能保证token不会被篡改。
具体的嵌入到springboot代码的过程:
1:jar包依赖
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version> 3.1 . 0 </version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version> 0.6 . 0 </version> </dependency> |
2:jwt加密和解密的工具类
package com.alienlab.news.utils; import com.alibaba.fastjson.JSONObject; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import java.security.Key; import java.util.Date; /** * The type Jwt utils. */ public class JwtUtils { public static Claims parseJWT(String jsonWebToken, String base64Security) { try { Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security)) .parseClaimsJws(jsonWebToken).getBody(); return claims; } catch (Exception ex) { return null ; } } //前三个参数为自己用户token的一些信息比如id,权限,名称等。不要将隐私信息放入(大家都可以获取到) public static String createJWT(String name, String userId, String role, String audience, String issuer, long TTLMillis, String base64Security) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //生成签名密钥 就是一个base64加密后的字符串? byte [] apiKeySecretBytes = DatatypeConverter.parseBase64Binary (base64Security); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm. getJcaName()); JSONObject jsonObject = new JSONObject(); jsonObject.put( "userName" , name); jsonObject.put( "userLoginName" , userId); //添加构成JWT的参数 JwtBuilder builder = Jwts.builder().setHeaderParam( "typ" , "JWT" ) .setIssuedAt(now) //创建时间 .setSubject(jsonObject.toString()) //主题,也差不多是个人的一些信息 .setIssuer(issuer) //发送谁 .setAudience(audience) //个人签名 .signWith(signatureAlgorithm, signingKey); //估计是第三段密钥 //添加Token过期时间 if (TTLMillis >= 0 ) { //过期时间 long expMillis = nowMillis + TTLMillis; //现在是什么时间 Date exp = new Date(expMillis); //系统时间之前的token都是不可以被承认的 builder.setExpiration(exp).setNotBefore(now); } //生成JWT return builder.compact(); } } |
3:使用的条件(该接口允许跨域 cors来配置跨域)
3.1:cors配置允许跨域
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import java.util.ArrayList; import java.util.List; /** * The type Cors config. */ @Configuration public class CorsConfig extends WebMvcConfigurerAdapter { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping( "/**" ).allowedOrigins( "*" ).allowCredentials( true ) .allowedMethods( "GET" , "POST" , "DELETE" , "PUT" ).maxAge( 3600 ); } private CorsConfiguration buildConfig() { CorsConfiguration corsConfiguration = new CorsConfiguration(); List<String> list = new ArrayList<>(); list.add( "*" ); corsConfiguration.setAllowedOrigins(list); corsConfiguration.addAllowedOrigin( "*" ); // 1 corsConfiguration.addAllowedHeader( "*" ); // 2 corsConfiguration.addAllowedMethod( "*" ); // 3 return corsConfiguration; } @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration( "/**" , buildConfig()); // 4 return new CorsFilter(source); } } |
3.2:拦截器拦截方法获取token
3.2.1:拦截器配置
import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * The type Api interceptor. */ public class ApiInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println( "拦截了" ); return true ; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println( "拦截了" ); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println( "拦截了" ); } } |
3.2.2: 拦截器管理工具
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; /** * The type My web app configurer. */ @Configuration public class MyWebAppConfigurer extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { //多个拦截器组成一个拦截器链 // addPathPatterns用于添加拦截规则 // excludePathPatterns用户排除拦截 registry.addInterceptor( new ApiInterceptor()).addPathPatterns( "/**" ); //对来自/user/** 这个链接来的请求进行拦截 super .addInterceptors(registry); } } |
4:token的发送与获取
ajax为例子:
beforeSend:function(request) {
// token,为登陆时获取到
request.setRequestHeader("token",token);
},
后台获取:
request.getHeader("token");
5:token验证机制
5.1:通过token解密是否成功可以判断token是否正确或者是否过期
5.2:解密完成,可以对比用户属性或者用户的固定token(缓存中或者放入数据库)
源码下载:https://github.com/fleapx/spring-security-jwt-guide
精彩代码视频及GIF动图讲解,请百度搜索:啄木鸟debugIT森林