初次了解JWT,很基础,高手勿喷。
基于Token的身份验证用来替代传统的cookie+session身份验证方法中的session。
JWT是啥?
JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为
"A.B.C"
A由JWT头部信息header加密得到
B由JWT用到的身份验证信息json数据加密得到
C由A和B加密得到,是校验部分
怎样生成A?
header格式为:
{
"typ": "JWT",
"alg": "HS256"
}
它就是一个json串,两个字段是必须的,不能多也不能少。alg
字段指定了生成C的算法,默认值是HS256
将header用base64加密,得到A
通常,JWT库中,可以把A部分固定写死,用户最多指定一个alg
的取值
怎样计算B?
根据JWT claim set[用base64]加密得到的。claim set是一个json数据,是表明用户身份的数据,可自行指定字段很灵活,也有固定字段表示特定含义(但不一定要包含特定字段,只是推荐)。
这里偷懒,直接用php中的代码来表示claim set了,重在说明字段含义:
$token = array(
"iss" => "http://example.org", #非必须。issuer 请求实体,可以是发起请求的用户的信息,也可是jwt的签发者。
"iat" => 1356999524, #非必须。issued at。 token创建时间,unix时间戳格式
"exp" => "1548333419", #非必须。expire 指定token的生命周期。unix时间戳格式
"aud" => "http://example.com", #非必须。接收该JWT的一方。
"sub" => "jrocket@example.com", #非必须。该JWT所面向的用户
"nbf" => 1357000000, # 非必须。not before。如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟。
"jti" => '222we', # 非必须。JWT ID。针对当前token的唯一标识
"GivenName" => "Jonny", # 自定义字段
"Surname" => "Rocket", # 自定义字段
"Email" => "jrocket@example.com", # 自定义字段
"Role" => ["Manager", "Project Administrator"] # 自定义字段
);
JWT遵循RFC7519,里面提到claim set的json数据中,自定义字段的key是一个string,value是一个json数据。因此随意编写吧,很灵活。
个人初学,认为一个最基本最简单最常用的claim set为:
$token=array(
"user_id" => 123456, #用户id,表明用户
"iat" => 1356999524, #token发布时间
"exp" => 1556999524, #token过期时间
);
将claim set加密后得到B
,学名payload
怎样计算C?
将A.B
使用HS256加密(其实是用header中指定的算法),当然加密过程中还需要密钥(自行指定的一个字符串)。
加密得到C
,学名signature
,其实就是一个字符串。作用类似于CRC校验,保证加密没有问题。
好了,现在A.B.C
就是生成的token了。
怎样使用token?
可以放到HTTP请求的请求头中,通常是Authorization
字段。
也有人说放到cookie。不过移动端app用cookie似乎不方便。
token应用流程?
- 初次登录:用户初次登录,输入用户名密码
- 密码验证:服务器从数据库取出用户名和密码进行验证
- 生成JWT:服务器端验证通过,根据从数据库返回的信息,以及预设规则,生成JWT
- 返还JWT:服务器的HTTP RESPONSE中将JWT返还
- 带JWT的请求:以后客户端发起请求,HTTP REQUEST HEADER中的Authorizatio字段都要有值,为JWT
1 package com.feihe.util; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 import com.auth0.jwt.JWTSigner; 6 import com.auth0.jwt.JWTVerifier; 7 import com.auth0.jwt.internal.com.fasterxml.jackson.databind.ObjectMapper; 8 import com.feihe.util.ConfHelper; 9 10 public class JWT { 11 12 private static final String SECRET = "XX#$%()(#*!()!KL<><MQLMNQNQJQK sdfkjsdrow32234545fdf>?N<:{LWPW"; // 密码 13 14 private static final String EXP = "exp"; 15 16 private static final String PAYLOAD = "payload"; 17 18 /** 19 * get jwt String of object 20 * 加密,传入一个对象和有效期 21 * @param object the POJO object 22 * @param maxAge the milliseconds of life time 23 * @return the jwt token 24 */ 25 public static <T> String sign(T object) { 26 try { 27 final JWTSigner signer = new JWTSigner(SECRET); 28 final Map<String, Object> claims = new HashMap<String, Object>(); 29 ObjectMapper mapper = new ObjectMapper(); 30 String jsonString = mapper.writeValueAsString(object); // 数据部分 31 claims.put(PAYLOAD, jsonString); 32 String jwtExp = ConfHelper.getConf("JWT_EXP"); // 设置token有效时长 暂定30分钟 33 Long maxAge = Long.parseLong(jwtExp); 34 claims.put(EXP, System.currentTimeMillis() + maxAge); 35 return signer.sign(claims); 36 } catch(Exception e) { 37 return null; 38 } 39 } 40 41 42 /** 43 * get the object of jwt if not expired 44 * 解密,传入一个加密后的token字符串和解密后的类型 45 * @param jwt 46 * @return POJO object 47 */ 48 public static<T> T unsign(String jwt, Class<T> classT) { 49 final JWTVerifier verifier = new JWTVerifier(SECRET); 50 try { 51 final Map<String,Object> claims= verifier.verify(jwt); 52 if (claims.containsKey(EXP) && claims.containsKey(PAYLOAD)) { 53 long exp = (Long)claims.get(EXP); 54 long currentTimeMillis = System.currentTimeMillis(); 55 if (exp > currentTimeMillis) { // 验证token是否过期 56 String json = (String)claims.get(PAYLOAD); 57 ObjectMapper objectMapper = new ObjectMapper(); 58 return objectMapper.readValue(json, classT); 59 } 60 } 61 return null; 62 } catch (Exception e) { 63 return null; 64 } 65 } 66 67 }
当用户登录成功后将用户信息或想要放在token值的信息调用JWT.sign()进行处理
1 String token = JWT.sign(user); // 登录成功,进行JWT处理
当验证登陆时将token值进行解密即可
1 SysUser user = JWT.unsign(token, SysUser.class);
根据结果判断登录情况。
ps:最后额外附上Spring拦截的配置
1 <mvc:interceptors> 2 <mvc:interceptor> 3 <!-- 匹配的是url路径, 如果不配置或/**,将拦截所有的Controller --> 4 <mvc:mapping path="/**" /> 5 <!-- /register 注册 和 /login 登陆 不需要拦截--> 6 <!-- <mvc:exclude-mapping path="/user/register" /> --> 7 <mvc:exclude-mapping path="/user/login" /> 8 9 <bean class="com.feihe.filter.TokenInterceptor"></bean> 10 </mvc:interceptor> 11 <!-- 当设置多个拦截器时,先按顺序调用preHandle方法,然后逆序调用每个拦截器的postHandle和afterCompletion方法 --> 12 </mvc:interceptors>
1 package com.feihe.filter; 2 3 import java.io.PrintWriter; 4 import java.util.List; 5 6 import javax.servlet.http.HttpServletRequest; 7 import javax.servlet.http.HttpServletResponse; 8 9 import org.springframework.beans.factory.annotation.Autowired; 10 import org.springframework.web.servlet.HandlerInterceptor; 11 import org.springframework.web.servlet.ModelAndView; 12 13 import com.feihe.coa.bean.SysLogin; 14 import com.feihe.coa.bean.SysUser; 15 import com.feihe.coa.mapper.SysLoginMapper; 16 import com.feihe.common.ResponseData; 17 import com.feihe.util.JWT; 18 19 import net.sf.json.JSONObject; 20 21 public class TokenInterceptor implements HandlerInterceptor{ 22 23 @Autowired 24 private SysLoginMapper sysLoginMapper; 25 26 public void afterCompletion(HttpServletRequest request, 27 HttpServletResponse response, Object handler, Exception arg3) 28 throws Exception { 29 } 30 31 public void postHandle(HttpServletRequest request, HttpServletResponse response, 32 Object handler, ModelAndView model) throws Exception { 33 } 34 35 //拦截每个请求 36 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 37 response.setCharacterEncoding("utf-8"); 38 String token = request.getParameter("token"); 39 ResponseData responseData; 40 //token不存在 41 if(null != token) { 42 SysUser user = JWT.unsign(token, SysUser.class); 43 //解密token后的user是否为null,一般都是token过期 44 if(null != user) { 45 if(user.getUserId() != 0) { 46 SysLogin login = new SysLogin(); 47 login.setUserId(user.getUserId()); 48 login.setState(1); // 登入状态为1 49 // 判断是否存在该用户的“登入”状态 50 List<SysLogin> loginList = sysLoginMapper.selectByUidAndState(login); 51 if(null !=loginList && loginList.size() > 0){ 52 if(token.equals(loginList.get(0).getToken())){ 53 return true; 54 }else { 55 responseData = ResponseData.forbidden(); 56 responseMessage(response, response.getWriter(), responseData); 57 return false; 58 } 59 }else { 60 responseData = ResponseData.forbidden(); 61 responseMessage(response, response.getWriter(), responseData); 62 return false; 63 } 64 }else { 65 responseData = ResponseData.forbidden(); 66 responseMessage(response, response.getWriter(), responseData); 67 return false; 68 } 69 }else { 70 responseData = ResponseData.forbidden(); 71 responseMessage(response, response.getWriter(), responseData); 72 return false; 73 } 74 }else { 75 responseData = ResponseData.forbidden(); 76 responseMessage(response, response.getWriter(), responseData); 77 return false; 78 } 79 } 80 81 //请求不通过,返回错误信息给客户端 82 private void responseMessage(HttpServletResponse response, PrintWriter out, ResponseData responseData) { 83 responseData = ResponseData.forbidden(); 84 response.setContentType("application/json; charset=utf-8"); 85 String json = JSONObject.fromObject(responseData).toString(); 86 out.print(json); 87 out.flush(); 88 out.close(); 89 } 90 91 }