问题原因
在传统的项目中我们利用,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));
}
}
}
测试: