前后端分离状态保持问题之JWT

问题原因

在传统的项目中我们利用,session+cookie来保持用户的登录状态,但这在前后端分离项⽬目中出现了问题;sessionid是使用cookie存储在客户端的,而cookie遵守同源策略,只在同源的请求中有效,这就导致了问题出现:前后端分离后静态资源完全可能(而且经常…)部署到另一个域下,导致cookie失效。

虽然我们可以在cookie中指定domain来解决,但是cookie必须针对性的设置作用域,这对于有多个不同域要共享cookie时,可操作性差,难以维护。

1、上述问题出现在前后端分离的web项目中,对于前后端分离的原生CS结构项目而言,很多客户端默认是不处理session和cookie的,需要进行相应的设置

2、在分布式或集群的项目中,共享session和cookie也是一大问题,必须引入第三方来完成session的 存储和共享(也可通过中间层做cookie转发如Nginx.Node.js),这也是传统单体服务无法支持分布式和集群的问题所在

正因为有这些问题,导致session+cookie的方式在某些项目中使用起来变得很麻烦,这时候就需要⼀一种新的状态维持的方式;

JWT

JWT全称(json WEB token),是基于json数据结构的数据验证⽅方式,其本质是对json数据进⾏行行加密后产⽣生的 字符串串

JWT的亮点:

	安全 
	稳定
	易用 
	⽀支持 JSON 

原理:

之所以使用session和cookie是因为HTTP的无状态性质,导致服务器无法识别多次请求是否来自同 一个用户

JWT可以对用户信息进行加密生成一个字符串,下发到客户端,客户端在后续请求中携带该字符串,服务器解析后取出用户信息,从而完成用户身份的识别,如下图:
在这里插入图片描述

JWT的数据结构

三个组成部分如下:

	Header(头部)
	Payload(负载)
	Signature(签名)

Header

Header 部分是一个 JSON 对象,描述 JWT 的元数据,例如签名算法等,像下面这样:

	{
	  "alg": "HS256",
	  "typ": "JWT"
	}
alg属性表示签名的算法,默认是 HMAC SHA256;

typ属性表示这个令牌(token)的类型统一写为JWT

最后使用base64URL算法转换为字符串;

Payload

Payload 部分也是一个 JSON 对象,用来存放真正需要传递的数据,JWT 规定了7个保留字段,如下:

iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号

服务器如果需要在Payload中添加用于识别用户身份的数据,也是键值对形式,注意不可使用保留字段,像下面这样:

{
  "sub": "test JWT",
  "name": "lbb",
  "isadmin": true
}

Payload同样使用base64URL算法转换为字符串;

强调:

Payload数据默是不加密的,攻击者可以通过相同的方式解析获取,若要将用户的关键数据放入其中则必须对其进行额外的加密

Signature
部分是对前两部分的签名,防止数据篡改。

签名时需要指定一个密钥(secret)。密钥只有服务器才知道,不能泄露给用户。然后使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的方式产生签名:

signature = HMACSHA256(base64UrlEncode(header) + "." +  base64UrlEncode(payload),  secret)

最后把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点".分隔返回给用户,如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJ0ZXN0IEpXVCIsImV4cCI6MTU4NjMyMzcxNCwidXNlcklkIjoic3dxMTMyZCJ9.
my8x7N5hIYZndAAUqL-wyeNY1dPU0rsNwDw_5xFsjAA

JWT是一个很长的字符串,分为成三个部分,中间用点.隔开
注意: JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。

总结:

优点

	满足REST Full的无状态要求(为了提高系统的扩展性,REST要求所有信息由请求端来提供,如此才使得JWT成为了分布式,集群构架的首选方式)
	
	在分布式,集群系统中使身份验证变得非常简单
	
	可用于其他数据交换
	
	合理的使用可减少数据库查询次数

缺点

	对于同样的数据JWT整体大小超过同样数据的cookie,这会增加网络负担
	
	服务器每次解析JWT都需要再次执行对应的算法,这将增加系统负担
	
	在传统单体服务,和WEBApp形式的前后端分离项目中使用JWT反而不如Session+cookie

注意事项:

		JWT的payload部分是不加密的,如果要放入关键数据则必须对其进行加密,或是将最后的JWT整体加密
				
		JWT本身用于认证,一旦泄露,则任何人都可以使用该令牌,获得其包含的所有权限,为了提高安全性.JWT的有效期不应太长,对于一些非常权限,建议在请求时再次验证

JWT的使用

使用JWT的步骤总体分为三步

	1、生成JWT
	
	2、验证JWT
	
	3、提取数据

案例:

所用的包:
在这里插入图片描述

生成JWT:

@WebServlet(name = "TokenServlet",urlPatterns = "/token")
public class TokenServlet extends HttpServlet {
    public static final String key = "QWDWQD456GTRHRHTGRRH232KYUVBZSRG";
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request,response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取参数
        String name = request.getParameter("name");
        String pwd = request.getParameter("pwd");
        //登录
        if ("lbb".equals(name) && "123456".equals(pwd)){//登陆成功
            JWTCreator.Builder builder = JWT.create();
            Algorithm algorithm = Algorithm.HMAC256(key);
            //支持链式调用
            String token = builder
                    .withSubject("test JWT") //设置主题
                    .withExpiresAt(new Date(new Date().getTime() + (1000 * 60 * 30)))//过期时间
                    .withClaim("userId", "swq132d") //负载数据(自定义)
                    .sign(algorithm);//签名,产生JWT,参数是算法

            //token放入响应头
            response.setHeader("token",token);
            response.getWriter().println("{\"succ\":ture}");
        }else {//登陆失败
            response.getWriter().println("{\"succ\":false}");
        }
    }
}

测试:
在这里插入图片描述
在这里插入图片描述
验证JWT及提取数据:

@WebServlet(name = "CheckServlet", urlPatterns = "/check")
public class CheckServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //取出token
        String token = request.getHeader("token");
        if (token != null) {//token有
            //验证token即是否过期或被篡改
            Algorithm algorithm = Algorithm.HMAC256(TokenServlet.key);
            JWTVerifier verifier = JWT.require(algorithm).build();
            try {
                verifier.verify(token);
                //验证成功,提取数据
                DecodedJWT decode = JWT.decode(token);
                String userId = decode.getClaim("userId").asString();
                //响应参数
                JSONObject param = new JSONObject();
                param.put("succ",true);
                param.put("userId",userId);
                response.getWriter().println(JSON.toJSONString(param));
            } catch (JWTDecodeException e) {//token失效或被篡改
                JSONObject param = new JSONObject();
                param.put("succ",false);
                param.put("err","失效或被篡改");
                response.getWriter().println(JSON.toJSONString(param));
            }
        } else {//没有token
            JSONObject param = new JSONObject();
            param.put("succ",false);
            param.put("err","没有token");
            response.getWriter().println(JSON.toJSONString(param));
        }
    }
}

测试:
在这里插入图片描述

发布了10 篇原创文章 · 获赞 22 · 访问量 642

猜你喜欢

转载自blog.csdn.net/bing_bg/article/details/105383367
今日推荐