目录
1、JWT的结构
2、使用JWT
3、封装工具类
3.1、引入依赖
3.2、生成token
3.3、解析token
3.4、封装工具类
4、整合pringboot
5、前端页面解析token
1、JWT的结构
JWT 最后的形式就是个字符串,它由头部、载荷与签名这三部分组成,中间以「.」分隔。像下面这样:
Header:
标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。
Payload:
令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分
Signature:
前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
如:
第三部分内容:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),私钥);
签名目的:
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
如果JWT的前2部分的内容被修改了,服务器接收到token后,会先获取前两部分的内容+私钥加密后和token中的第三部分的签名进行对比。如果一致就是合法的
信息安全问题:
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏 感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第 三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。
2、使用JWT
1、在用户登录网站的时候,需要输入用户名、密码或者短信验证的方式登录,登录请求到达服务端的时候,服务端对账号、密码进行验证,然后计算出 JWT 字符串,返回给客户端。
2、客户端拿到这个 JWT 字符串后,存储到 COOKIE 或者 浏览器的 LocalStorage 中。
3、再次发送请求,比如请求用户设置页面的时候,在 HTTP 请求头中加入 JWT 字符串,或者直接放到请求主体中。
4、服务端拿到这串 JWT 字符串后,使用 base64的头部和 base64 的载荷部分加上私钥,通过HMACSHA256
算法计算签名部分,比较计算结果和传来的签名部分是否一致,如果一致,说明此次请求没有问题,如果不一致,说明请求过期或者是非法请求。
保证安全性的关键就是 私钥和HMACSHA256
或者与它同类型的加密算法,因为加密过程是不可逆的,所以不能根据传到前端的 JWT 传反解到密钥信息。另外,不同的头部和载荷加密之后得到的签名都是不同的,所以,如果有人改了载荷部分的信息,那最后加密出的结果肯定就和改之前的不一样的,所以,最后验证的结果就是不合法的请求。
私钥如果不小心泄露会怎么样?
就算私钥泄露了,强盗也无法修改token携带的信息。如果修改,后台还是无法校验通过。如果不做更改,直接用呢,那就没有办法了,为了更大程度上防止被强盗盗取,应该使用 HTTPS 协议而不是 HTTP 协议,这样可以有效的防止一些中间劫持攻击行为。
3、封装工具类
3.1、引入依赖
com.auth0java-jwt3.4.0
3.2、生成token
&#64;Testpublic void makeToken(){//1、日历类Calendar calendar&#61;Calendar.getInstance();//2、设置时间为 1小时过期calendar.add(Calendar.SECOND,60);Map map&#61;new HashMap<>();//3、生成令牌String token &#61; JWT.create().withHeader(map)//第一部分 头使用默认值 签名算法:HS256 type:jwt.withClaim("username", "rk")//第二部分.withClaim("age", 20).withIssuedAt(new Date(System.currentTimeMillis()))//设置令牌发放时间.withExpiresAt(calendar.getTime())//设置过期时间.sign(Algorithm.HMAC256("LUOLIN!&#64;$%#&#64;"));//第三部分 设置签名&#xff0c;LUOLIN!&#64;$%#&#64;为秘钥//4、输出令牌System.out.println(token);}
3.3、解析token
//解析token&#64;Testpublic void parseToken(){try {String token&#61;"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NTYzNzk5OTAsImlhdCI6MTY1NjM3OTkzMCwiYWdlIjoyMCwidXNlcm5hbWUiOiJyayJ9.-Pcm-dox8IG2ZT_gve7ApVHCOvM1M9mHDicgDBm8H0k";//1、创建验证对象JWTVerifier jwtVerifier &#61; JWT.require(Algorithm.HMAC256("LUOLIN!&#64;$%#&#64;")).build();//2、校验签名(校验token是否合法) DecodedJWT jwt &#61; jwtVerifier.verify(token);System.out.println("用户名:"&#43;jwt.getClaim("username").asString());System.out.println("年龄:"&#43;jwt.getClaim("age").asInt());SimpleDateFormat sdf&#61;new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");System.out.println("令牌发放时间:"&#43;sdf.format(jwt.getIssuedAt()));System.out.println("令牌过期时间:"&#43;sdf.format(jwt.getExpiresAt()));}catch (TokenExpiredException e){System.out.println("token已过期!");}catch (JWTVerificationException e) {System.out.println("token不合法!");}}
常见异常:
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
InvalidClaimException: 失效的payload异常
3.4、封装工具类
public class JWTUtils {private static String TOKEN &#61; "token!Q&#64;W3e4r";/*** 生成token* &#64;param map //传入payload* &#64;return 返回token*/public static String getToken(Map map){JWTCreator.Builder builder &#61; JWT.create();map.forEach((k,v)->{builder.withClaim(k,v);});Calendar instance &#61; Calendar.getInstance();instance.add(Calendar.SECOND,7);builder.withExpiresAt(instance.getTime());return builder.sign(Algorithm.HMAC256(TOKEN)).toString();}/*** 验证token* &#64;param token* &#64;return*/public static void verify(String token){JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);}/*** 获取token中payload* &#64;param token* &#64;return*/public static DecodedJWT getToken(String token){return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);}
}
4、整合pringboot
登录Controller:
&#64;GetMapping("/user/login")public Map login(User user){Map map &#61; new HashMap<>();try{//通过用户名和密码查找用户User userDB &#61; userService.login(user);Map payload &#61; new HashMap<>();payload.put("id",userDB.getId());payload.put("name",userDB.getName());//生成JWT的令牌String token &#61; JWTUtils.getToken(payload);//返回值赋值map.put("state",true);map.put("msg","登录成功!");map.put("token",token);//响应token}catch (Exception e){map.put("state",false);map.put("msg",e.getMessage());}return map;}
登录Service:
&#64;Autowiredprivate UserDAO userDAO;&#64;Override&#64;Transactional(propagation &#61; Propagation.SUPPORTS)public User login(User user) {//根据接收用户名密码查询数据库User userDB &#61; userDAO.login(user);if(userDB!&#61;null){return userDB;}throw new RuntimeException("登录失败");}
拦截器:
拦截器配置:
&#64;Configuration
public class InterceptorConfig implements WebMvcConfigurer {&#64;Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new JWTInterceptor())//将jwt拦截器添加进去.excludePathPatterns("/user/login")//放行登录接口.addPathPatterns("/**"); //拦截接口}
}
public class JWTInterceptor implements HandlerInterceptor {&#64;Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {//拦截处理Map map &#61; new HashMap<>();//获取请求头中令牌String token &#61; request.getHeader("token");try {JWTUtils.verify(token);//验证令牌return true;//放行请求} catch (SignatureVerificationException e) {e.printStackTrace();map.put("msg","无效签名!");}catch (TokenExpiredException e){e.printStackTrace();map.put("msg","token过期!");}catch (AlgorithmMismatchException e){e.printStackTrace();map.put("msg","token算法不一致!");}catch (Exception e){e.printStackTrace();map.put("msg","token无效!!");}map.put("state",false);//设置状态//将map 转化为json jacksonString json &#61; new ObjectMapper().writeValueAsString(map);response.setContentType("application/json;charset&#61;UTF-8");response.getWriter().println(json);return false;}
}
拦截器的作用主要就是校验token 是否合法&#xff0c;每次发起请求前都会先通过拦截器校验token。
测试接口:
&#64;PostMapping("/user/test")public Map test(HttpServletRequest request){Map map &#61; new HashMap<>();//处理自己业务逻辑String token &#61; request.getHeader("token");DecodedJWT verify &#61; JWTUtils.verify(token);log.info("用户id: [{}]",verify.getClaim("id").asString());log.info("用户name: [{}]",verify.getClaim("name").asString());map.put("state",true);map.put("msg","请求成功!");return map;}
拦截器用来校验token&#xff0c;其他接口就只需要获取token后来解析其中的值&#xff0c;处理自己的业务逻辑就行了
未登录的情况下访问其它接口会被拦截器拦截。
登录成功后返回token给前端界面。
登录成功后携带token继续访问其它接口&#xff0c;拦截器校验通过。test接口也获取解析出来了token中携带的值。
5、前端页面解析token
//jwt的token解码方式&#xff1a;
//首先拿到token码然后以点为分隔符转为数组
let token&#61;localStorage.getItem(‘token’).split(".");console.log(token);
//拿到第二段token也就是负载的那段 进行window.atob方法的 base64的解算&#xff0c;
然后再用decodeURIComponent字符串解码方法 解析出字符串 然后再转成JSON对象
由于atob()方法解码无法对中文解析 所以要再用escape()方法对其重新编码
然后再用decodeURI解码方式解析出来let str&#61;token[1]&#xff1b;
let user&#61;JSON.parse(decodeURIComponent(escape(window.atob(str))));
console.log(user.username);