A. jwt的token怎么生存的
引入依赖包加密解密方法。
在生产环境中,一般jwt会保存用户的名字和角色权限等信息。可以将token写到cookie里,每次前端访问后台时,可以在拦截器或者过滤器取到token,然后解密,先判断是否过期,过期就抛异常阻止其访问。然后取出信息保存到threadLocal里,方便以后调用这些信息,当后台访问完成后,从thredLocal删除此用户信息。
服务器认证以后,生成一个JSON格式的对象返回给客户端。之后客户端与服务端通信的时候,都要发回这个JSON对象。服务器完全根据这个对象认证用户身份。
B. JWT生成token及过期处理方案
## 业务场景
在前后分离场景下,越来越多的项目使用token作为接口的安全机制,APP端或者WEB端(使用VUE、REACTJS等构建)使用token与后端接口交互,以达到安全的目的。本文结合stackoverflow以及本身项目实践,试图总结出一个通用的,可落地的方案。
## 基本思路
- 单个token
1. token(A)过期设置为15分钟
2. 前端发起请求,后端验证token(A)是否过期;如果过期,前端发起刷新token请求,后端设置已再次授权标记为true,请求成功
3. 前端发起请求,后端验证再次授权标记,如果已经再次授权,则拒绝刷新token的请求,请求成功
4. 如果前端每隔72小时,必须重新登录,后端检查用户最后一次登录日期,如超过72小时,则拒绝刷新token的请求,请求失败
- 授权token加上刷新token
用户仅登录一次,用户改变密码,则废除token,重新登录
## 1.0实现
1.登录成功,返回access\_token和refresh\_token,客户端缓存此两种token;
2.使用access_token请求接口资源,成功则调用成功;如果token超时,客户端
携带refresh\_token调用中间件接口获取新的access\_token;
3.中间件接受刷新token的请求后,检查refresh_token是否过期。
如过期,拒绝刷新,客户端收到该状态后,跳转到登录页;
如未过期,生成新的access\_token和refresh\_token并返回给客户端(如有可能,让旧的refresh\_token失效),客户端携带新的access\_token重新调用上面的资源接口。
4.客户端退出登录或修改密码后,调用中间件注销旧的token(使access\_token和refresh\_token失效),同时清空客户端的access\_token和refresh\_toke。
后端表
id user\_id client\_id client\_secret refresh\_token expire\_in create\_date del_flag
## 2.0实现
场景: access\_token访问资源 refresh\_token授权访问 设置固定时间X必须重新登录
1.登录成功,后台jwt生成access\_token(jwt有效期30分钟)和refresh\_token(jwt有效期15天),并缓存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以根据sub-key,废除部分设备的token。),设置过期时间为1个月,保证最终所有token都能删除),返回后,客户端缓存此两种token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果token超时,中间件删除access\_token(废除);客户端再次携带refresh\_token调用中间件接口获取新的access_token;
3.中间件接受刷新token的请求后,检查refresh_token是否过期。
如过期,拒绝刷新,删除refresh_token(废除); 客户端收到该状态后,跳转到登录页;
如未过期,检查缓存中是否有refresh\_token(是否被废除),如果有,则生成新的access\_token并返回给客户端,客户端接着携带新的access_token重新调用上面的资源接口。
4.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access\_token和refresh\_token(废除)),同时清空客户端侧的access\_token和refresh\_toke。
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
6.以上3刷新access_token可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:失效,长时间未登录,频繁刷新)
2.0 变动
1.登录
2.登录拦截器
3.增加刷新access_token接口
4.退出登录
5.修改密码
## 3.0实现
场景:自动续期 长时间未使用需重新登录
1.登录成功,后台jwt生成access\_token(jwt有效期30分钟),并缓存到redis(hash-key为access\_token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token),设置access_token过期时间为7天,保证最终所有token都能删除),返回后,客户端缓存此token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果token超时,中间件删除access\_token(废除),同时生成新的access\_token并返回。客户端收到新的access_token,
再次请求接口资源。
3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access\_token(废除)),同时清空客户端侧的access\_token。
4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长时间未登录,频繁刷新)
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
3.0 变动
1.登录
2.登录拦截器
3.退出登录
4.修改密码
1.3 场景:token过期重新登录 长时间未使用需重新登录
1.登录成功,后台jwt生成access\_token(jwt有效期7天),并缓存到redis,key为 "user\_id:access\_token",value为access\_token(根据用户id,可以人工废除指定用户全部token),设置缓存过期时间为7天,保证最终所有token都能删除,请求返回后,客户端缓存此access_token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果token超时,中间件删除access\_token(废除),同时生成新的access\_token并返回。客户端收到新的access_token,
再次请求接口资源。
3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access\_token(废除)),同时清空客户端侧的access\_token。
4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长时间未登录,频繁刷新)
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
1.3 变动
1.登录
2.登录拦截器
3.退出登录
4.修改密码
# 解决方案
2.0 场景: access\_token访问资源 refresh\_token授权访问 设置固定时间X必须重新登录
1.登录成功,后台jwt生成access\_token(jwt有效期30分钟)和refresh\_token(jwt有效期15天),并缓
存到redis(hash-key为token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全
部token,也可以根据sub-key,废除部分设备的token。),设置过期时间为1个月,保证最终所有token都
能删除),返回后,客户端缓存此两种token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果
token超时,中间件删除access\_token(废除);客户端再次携带refresh\_token调用中间件接口获取新的
access_token;
3.中间件接受刷新token的请求后,检查refresh_token是否过期。
如过期,拒绝刷新,删除refresh_token(废除); 客户端收到该状态后,跳转到登录页;
如未过期,检查缓存中是否有refresh\_token(是否被废除),如果有,则生成新的access\_token并返回给
客户端,客户端接着携带新的access_token重新调用上面的资源接口。
4.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access\_token和refresh\_token(
废除)),同时清空客户端侧的access\_token和refresh\_toke。
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
6.以上3刷新access_token可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(
拒绝的场景:失效,长时间未登录,频繁刷新)
2.0 变动
1.登录
2.登录拦截器
3.增加刷新access_token接口
4.退出登录
5.修改密码
3.0 场景:自动续期 长时间未使用需重新登录
1.登录成功,后台jwt生成access_token(jwt有效期30分钟),并缓存到redis(hash-key为
access_token,sub-key为手机号,value为设备唯一编号(根据手机号码,可以人工废除全部token,也可以
根据sub-key,废除部分设备的token。),设置access_token过期时间为1个月,保证最终所有token都能删
除),返回后,客户端缓存此token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果
token超时,中间件删除access\_token(废除),同时生成新的access\_token并返回。客户端收到新的
access_token,
再次请求接口资源。
3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清
空客户端侧的access_token。
4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长
时间未登录,频繁刷新)
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
3.0 变动
1.登录
2.登录拦截器
3.退出登录
4.修改密码
4.0 场景:token过期重新登录 长时间未使用需重新登录
1.登录成功,后台jwt生成access_token(jwt有效期7天),并缓存到redis,key为
"user\_id:access\_token" + 用户id,value为access_token(根据用户id,可以人工废除指定用户全部
token),设置缓存过期时间为7天,保证最终所有token都能删除,请求返回后,客户端缓存此
access_token;
2.使用access\_token请求接口资源,校验成功且redis中存在该access\_token(未废除)则调用成功;如果
token超时,中间件删除access\_token(废除),同时生成新的access\_token并返回。客户端收到新的
access_token,
再次请求接口资源。
3.客户端退出登录或修改密码后,调用中间件注销旧的token(中间件删除access_token(废除)),同时清
空客户端侧的access_token。
4.以上2 可以增加根据登录时间判断最长X时间必须重新登录,此时则拒绝刷新token。(拒绝的场景:长
时间未登录,频繁刷新)
5.如手机丢失,可以根据手机号人工废除指定用户设备关联的token。
4.0 变动
1.登录
2.登录拦截器
3.退出登录
4.修改密码
## 最终实现
### 后端
1. 在登录接口中 如果校验账号密码成功 则根据用户id和用户类型创建jwt token(有效期设置为-1,即永不过期),得到A
2. 更新登录日期(当前时间new Date()即可)(业务上可选),得到B
3. 在redis中缓存key为ACCESS_TOKEN:userId:A(加上A是为了防止用户多个客户端登录 造成token覆盖),value为B的毫秒数(转换成字符串类型),过期时间为7天(7 * 24 * 60 * 60)
4. 在登录结果中返回json格式为{"result":"success","token": A}
5. 用户在接口请求header中携带token进行登录,后端在所有接口前置拦截器进行拦截,作用是解析token 拿到userId和用户类型(用户调用业务接口只需要传token即可),
如果解析失败(抛出SignatureException),则返回json(code = 0 ,info= Token验证不通过, errorCode = '1001');
此外如果解析成功,验证redis中key为ACCESS_TOKEN:userId:A 是否存在 如果不存在 则返回json(code = 0 ,info= 会话过期请重新登录, errorCode = '1002');
如果缓存key存在,则自动续7天超时时间(value不变),实现频繁登录用户免登陆。
6. 把userId和用户类型放入request参数中 接口方法中可以直接拿到登录用户信息
7. 如果是修改密码或退出登录 则废除access_tokens(删除key)
### 前端(VUE)
1. 用户登录成功,则把username存入cookie中,key为loginUser;把token存入cookie中,key为accessToken
把token存入Vuex全局状态中
2. 进入首页
C. JWT token封装以及自动刷新方案建议
什么是JWT
pom.xml
JWTUtil.java
用户登录操作
在前后分离场景下,越来越多的项目使用jwt token作为接口的安全机制,但存在jwt过期后,用户无法直接感知,假如在用户操作页面期间,突然提示登录,则体验很不友好,所以就有了token自动刷新需求;
方案:前端控制检测token,无感知刷新
用户登录成功的时候,一次性给他两个Token,分别为AccessToken和RefreshToken
AccessToken有效期较短,比如1天或者5天,用于正常请求
RefreshToken有效期可以设置长一些,例如10天、20天,作为刷新AccessToken的凭证
刷新方案:当AccessToken即将过期的时候,例如提前30分钟,客户端利用RefreshToken请求指定的API获取新的AccessToken并更新本地存储中的AccessToken
核心逻辑
1、登录成功后,jwt生成AccessToken; UUID生成RefreshToken并存储在服务端redis中,设置过期时间
2、接口返回3个字段AccessToken/RefreshToken/访问令牌过期时间戳
3、由于RefreshToken存储在服务端redis中,假如这个RefreshToken也过期,则提示重新登录;
老王的疑问:RefreshToken有效期那么长,和直接将AccessToken的有效期延长有什么区别
答:RefreshToken不像AccessToken那样在大多数请求中都被使用,主要是本地检测accessToken快过期的时候才使用,
一般本地存储的时候,也不叫refreshToken,前端可以取个别名,混淆代码让攻击者不能直接识别这个就是刷新令牌
缺点:前端每次请求需要判断token距离过期时间
优点:后端压力小,代码逻辑改动不大
刷新token方法未实现。
D. JWT-token—前后端分离架构的api安全问题
前后端分离架构带来的好处一搜一大堆,我们来看一下分离后后端接口的安全问题。
前后端分离架构现状:
这样的情况后端api是暴露在外网中,因为常规的web项目无论如何前端都是要通过公网访问到后台api的,带来的隐患也有很多。
1.接口公开,谁都可以访问
2.数据请求的参数在传输过程被篡改
3.接口被重复调用
...
session和cookie都是客户端与服务端通讯需要提供的认证,当客户端的值和服务器的值吻合时,才允许请求api,解决了第1个问题,但是当攻击者获取到了传输过程中的session或者cookie值后,就可以进行第2、3种攻击了
JWT标准的token包含三部分:
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等
将上面的JSON对象进行 [base64编码] 可以得到下面的字符串。这个字符串我们将它称作JWT的Header
Payload也是一个JSON对象。包含了一些其他的信息
这里面的前五个字段都是由JWT的标准所定义的。
将上面的JSON对象进行 [base64编码] 可以得到下面的字符串。这个字符串我们将它称作JWT的Payload
将上面的两个编码后的字符串都用句号 . 连接在一起(头部在前),就形成了
最后,我们将上面拼接完的字符串用 HS256算法 进行加密。在加密的时候,我们还需要提供一个 密钥(secret) 。如果我们用 mystar 作为密钥的话,那么就可以得到我们加密后的内容
这一部分叫做 签名
最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT
签名解决了数据传输过程中参数被篡改的风险
一般而言,加密算法对于不同的输入产生的输出总是不一样的,如果有人 对Header以及Payload的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。 而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
解决了篡改数据的问题,还有第3个问题,那就是攻击者不修改数据,只是重复攻击
比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道, 因为JWT机制是无状态的。
可以在Payload里增加时间戳并且前后端都参与来解决:
E. ID Token - JWT
我们来继续前两章( OAuth2 总结 , 对OpenID Connect的理解 )的讨论,进入对JWT的理解。先来简单回顾一下OAuth2和OpenID:
OpenID建立在OAuth之上,完成了认证和授权。而认证和授权的结果就体现在这个ID token之上,而这个ID token通常会是JWT。那么为什么会是JWT呢?我们通过以下几点来逐一解释。
JWT RFC 7519 给出了官方定义的一些字段:
当然也不限于此,可以根据自己的需要,添加其他字段。到此,我们可以看出JWT提供了ID token所需要的能力。一个完整的JTW是由三部分组成:Header,Payload,Signature。三者之间由 . 隔开,刚才提到的认证授权的字段就存在于Payload中。
Header中存放的是JTW的元数据,包含签名的算法以及token的类型,如下所示:
Signature部分是对前两部分的签名, 防止数据篡改 。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
算出签名以后,把 Header、Payload、Signature三个部分经过base64序列化为三个字符串,再讲三个字符串由 . 为间隔拼接成一个字符串返回给客户端。
ID Token需要有足够的安全性,JWT是如何做到的呢?
刚看到了签名部分,签名的作用只是为了防止数据篡改,而JWT默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。比如认证服务器中使用私钥进行加密,把公钥分发给其他应用服务器,应用服务拿到加密后的token后用公钥解密,获取到JWT,再用签名的秘钥来验证数据是否经过的篡改。
我们来看看还有神马其他备选么?Simple Web Tokens (SWT)和Security Assertion Markup Language Tokens (SAML)。
JWT vs SAML:JWT基于json,而SAML基于XML,在大小上就有足够的优势,更适用于HTML和HTTP。
JWT vs SWT:在安全性上,SWT只支持对称加密,而JWT和SAML支持公私钥的加密方式。
作为一个mobile developer,也想在这里对比一下原先的简单token模式和SSO中的JWT:
在没有该机制前,我们通常会使用一个随机产生的字符串作为token,认证成功后返回给前端,之后前端的每个请求带上这个token。若后台服务的是个单体应用没有什么问题,请求来了,验证一下token是否有效即可,但当认证服务和其他的应用服务是分离的,怎么做呢?应用服务受到请求,再向认证服务发起一个请求来验证验证token是否合法,是否有权限访问该应用服务。这样做到没有什么问题,只是当服务变多时,比如微服务下,势必会造成认证服务器的压力过大。
在使用该机制后,客户端通过认证服务登录,获得这个JWT,之后其他应用服务自身便可以验证这个token的是否有效,是否有权访问。
看似完美,但也有它自身的问题,我们来看一个场景:权限变更。某用户原先是一个超级管理员,可以访问所有服务,并可进行任意的删除,更改操作,他在这个状态下拿到了JWT。随后,由于权限更改为普通管理员,便不应该具有所有权限,但此时他开始时的JWT被缓存在客户端仍然可用,其他应用服务也并无法知道这个用户的权限已经被更改,后果可想而知了。解决的方式无非是将这个token的有效时间设置的短一些。