如何理解企业家才能才能初步的理解JWT

出处:/blogs
名字都是用来唬人的。
先解释两个名词,CSRF 和 JWT。
CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,其中一个页面通过窃取另一个页面的 cookie 来发送伪造的请求,因为 cookie 是随着请求自动发送到服务端的。
JWT (JSON Web Token),通过某种算法将两个 JSON 对象加密成一个字符串,该字符串能代表唯一用户。
CSRF 的产生
首先通过一个图来理解 CSRF 是什么现象。
想要攻击成功,这三步缺一不可。
第一,登录受害者网站。如果受害者网站是基于 cookie 的用户验证机制,那么当用户登录成功后,浏览器就会保存一份服务端的 SESSIONID。
第二,这时候在同一个浏览器打开攻击者网站,虽然说它无法获取 SESSIONID 是什么(因为设置了 http only 的 cookie 是无法被 JavaScript 获取的),但是从浏览器向受害者网站发出的任何请求中,都会携带它的 cookie,无论是从哪个网站发出。
第三,利用这个原理,在攻击者网站发出一个请求,命令受害者网站进行一些敏感操作。由于此时发出的请求是处于 session 中的,所以只要该用户有权限,那么任何请求都会被执行。
比如,打开优酷,并登录。再打开攻击者网站,它里面有个
&img& 标签是这样的:
&img src=&/follow/123& /&
这个 api 只是个例子,具体的 url 和参数都可以通过浏览器的开发者工具(Network 功能)事先确定。假如它的作用是让该登录的用户关注由 123 确定的一个节目或者用户,那么通过 CSRF 攻击,这个节目的关注量就会不断上升。
解释两点。第一,为什么举这个例子,而不是银行这种和金钱有关的操作?很简单,因为它容易猜。对于攻击者来说,没有什么是一定能成功的,比如 SQL 注入,攻击者他不知道某网站的数据库是怎么设计的,但是他一般会通过个人经验去尝试,比如很多网站把用户的主键设置为 user_id,或 sys_id 等。
银行的操作往往经过多重确认,比如图形验证码、手机验证码等,光靠 CSRF 完成一次攻击基本上是天方夜谭。但其他类型的网站往往不会刻意去防范这些问题。虽然金钱上的利益很难得到,但 CSRF 能办到的事情还是很多,比如利用别人发虚假微博、加好友等,这些都能对攻击者产生利益。
第二,如何确保用户打开优酷之后,又打开攻击者网站?做不到。否则任何人打开优酷之后,都会莫名其妙地去关注某个节目了。但是你要知道,这个攻击成本仅仅是一条 API 调用而已,它在哪里都能出现,你从任何地方下载一张图片,让你请求这个地址,看也不看就点确定,请求不就发出去了吗?
CSRF 的防御
对于如何防范 CSRF,一般有三种手段。
判断请求头中的 Referer
这个字段记录的是请求的来源。比如
上调用了百度的接口
那么在百度的服务端,就可以通过 Referer 判断这个请求是来自哪里。
在实际应用中,这些跟业务逻辑无关的操作往往会放在拦截器中(或者说过滤器,不同技术使用的名词可能不同)。意思是说,在进入到业务逻辑之前,就应该要根据 Referer 的值来决定这个请求能不能处理。
在 Java Servlet 中可以用 Filter(古老的技术);用 Spring 的话可以建拦截器;在 Express 中是叫中间件,通过 request.get(&referer&) 来取得这个值。每种技术它走的流程其实都一样。
但要注意的是,Referer 是浏览器设置的,在浏览器兼容性大不相同的时代中,如果存在某种浏览器允许用户修改这个值,那么 CSRF 漏洞依然存在。
在请求参数中加入 csrf token
讨论 GET 和 POST 两种请求,对于 GET,其实也没什么需要防范的。为什么?因为 GET 在“约定”当中,被认为是查询操作,查询的意思就是,你查一次,查两次,无数次,结果都不会改变(用户得到的数据可能会变),这不会对数据库造成任何影响,所以不需要加其他额外的参数。
所以这里要提醒各位的是,尽量遵从这些约定,不要在 GET 请求中出现 /delete, /update, /edit 这种单词。把“写”操作放到 POST 中。
对于 POST,服务端在创建表单的时候可以加一个隐藏字段,也是通过某种加密算法得到的。在处理请求时,验证这个字段是否合法,如果合法就继续处理,否则就认为是恶意操作。
&form method=&post& action=&/delete&&
&!-- 其他字段 --&
&input type=&hidden& name=&csrftoken& value=&由服务端生成&/&
这个 html 片段由服务端生成,比如 JSP,PHP 等,对于 Node.js 的话可以是 Jade 。
这的确是一个很好的防范措施,再增加一些处理的话,还能防止表单重复提交。
可是对于一些新兴网站,很多都采用了“单页”的设计,或者退一步,无论是不是单页,它的 HTML 可能是由 JavaScript 拼接而成,并且表单也都是异步提交。所以这个办法有它的应用场景,也有局限性。
新增 HTTP Header
思想是,将 token 放在请求头中,服务端可以像获取 Referer 一样获取这个请求头,不同的是,这个 token 是由服务端生成的,所以攻击者他没办法猜。
这篇文章的另一个重点——JWT——就是基于这个方式。抛开 JWT 不谈,它的工作原理是这样的:
解释一下这四个请求,类型都是 POST 。
通过 /login 接口,用户登录,服务端传回一个 access_token,前端把它保存起来,可以是内存当中,如果你希望用来模拟 session 的话。也可以保存到 localStorage 中,这样可以实现自动登录。
调用 /delete 接口,参数是某样商品的 id。仔细看,在这个请求中,多了一个名为 Authoriaztion 的 header,它的值是之前从服务端传回来的 access_token,在前面加了一个“Bearer”(这是和服务端的约定,约定就是说,说好了加就一起加,不加就都不加……)
调用 /logout 接口,同样把 access_token 加在 header 中传过去。成功之后,服务端和前端都会把这个 token 置为失效,或直接删除。
再调用 /delete 接口,由于此时已经没有 access_token 了,所以服务端判断该请求没权限,返回 401 。
各位有没有发现,从头至尾,整个过程没有涉及 cookie,所以 CSRF 是不可能发生的!
关于 JWT 的约定
如果不关心 JWT,那文章完全可以结束了,因为看到这里,除了章节标题提到的内容之外,各位还可以引申出几点:第一,在设计 API 时多斟酌一下;第二,利用 token 做单点登录;第三,cookie 和 token 这两种用户验证机制的不同。
而 JWT,其实就是对新增的 HTTP Header 的约定。就比如 GET 请求中的参数,约定了用
& 分隔,但是用别的可以吗?当然可以,你用 逗号 或者 分号 也行啊,服务端再规定一个转义的规则就行了。只不过,约定是为了让所有人更规范地做事情,如果按照约定行事的话,那从一个工具换到另一个工具,自己需要改的代码就很少。这里就不深入谈了。
三个组成部分
对 JWT 的术语和内容有最官方的说明。
JWT 的每个部分都是字符串,由 点 分隔,所以它的格式是这样的:
XXX1.XXX2.XXX3
整个字符串是 URL-safe 的,所以可以直接用在 GET 请求的参数中。
第一部分 JWT Header
它是一个 JSON 对象,表示这个整个字符串的类型和加密算法,比如
&typ&:&JWT&,
&alg&:&HS256&
经过 base64url 加密之后变成
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
第二部分 JWT Claims Set
它也是一个 JSON 对象,能唯一表示一个用户,比如
&iss&: &123&,
经过 base64url 加密之后变成
eyJpc3MiOiIxMjMiLCJleHAiOjE0NDE1OTM4NTB9
在官网有详细的属性说明,尽量使用里面提到的 Registered Claim Names,这样可以提高阅读性。这里的 iss 表示 issuer,就是发起请求的人,它的值是跟业务相关的,所以由你的应用去决定。exp 表示 expiration time,即什么时候过期,注意,这个值是秒数,而不是毫秒数,所以是在整型范围内的。
第三部分 JWS Signature
这个签名的计算跟第一部分中的 alg 属性有关,假如是 HS256,那么服务端需要保存一个私钥,比如 secret 。然后,把第一部分和第二部分生成的两个字符串用 点 连接之后,再结合私钥,用 HS256 加密可以得到如下字符串
AOtbon6CebgO4WO9iJ4r6ASUl1pACYUetSIww-GQ72w
现在就集齐三个部分了,用
. 连接,得到完整的 token 。
例子 1/2:以 Express 作为服务端
对于服务端来说,已经存在各种库去支持 JWT 了,推荐几个如下:
maven com.auth0 / java-jwt / 0.4
composer require lcobucci/jwt
gem install jwt
Install-Package System.IdentityModel.Tokens.Jwt
npm install jsonwebtoken
如果之前有 Node.js 和 Express 的学习经历的话,那对下面的代码应该很容易理解。
var express = require(&express&),
= require(&jsonwebtoken&);
var router
= express.Router(),
PRIVATE_KEY = &secret&;
router.post(&/login&, function(req, res, next) {
// 生成 JWT
var token = jwt.sign({
iss: &123&
}, PRIVATE_KEY, {
expiresInMinutes: 60
// 将 JWT 返回给前端
res.send({
access_token: token
router.post(&/delete&, function(req, res, next) {
= req.get(&Authorization&),
// 判断请求头中是否有 Authoriaztion 字段,为了缩短代码就减少了别的验证
if (auth) {
token = /Bearer (.+)/.exec(auth)[1];
res.send(jwt.decode(token));
res.sendStatus(401);
关于 jsonwebtoken 的使用可以看它的
例子中定义了两个 API。
/login,会返回一个 JWT 字符串。其中包含了一个用户 id,和存活时间,这个时间会被转换成 exp 和 iat (issue at, 发起请求的时间),两者之差就是存活时间。
/delete,验证请求头中是否有 Authorization 字段,并且是否合法,如果是的话就处理请求,否则返回 401 。
注意一下,服务端期待的 Authoriaztion 请求头是这样的格式:
Authorization: Bearer XXX1.XXX2.XXX3
这个跟 JWT 无关,是 OAuth 2.0 的一种格式。因为 Authorization 这个字段也是约定的,它由 token 的类型和值组成,类型除了上文提到的 Bearer,还有 Basic、MAC 等。
例子 2/2:以 Backbone 作为前端
前端的工作分两方面,一是存储 jwt,二是在所有的请求头中增加 Authoriaztion 。
如果是重构已有的代码,第二个工作可能有点难度,除非旧代码中的表单都是异步提交,并且请求的方法是自己包装过的,因为只有这样才有机会去修改请求头。
在若干星期之前的
中,写了怎么在 Angular 中拦截请求。现在就以 Backbone 为例。
// 先保存原始的 sync 方法
var sync = Backbone.
Backbone.sync = function (method, model, options) {
var token = window.localStorage.getItem(&jwt&);
// 如果存在 token,就把它加到请求头中
if (token) {
options.headers.Authorization = &Bearer & +
// 调用原始的 sync 方法
sync(method, model, options);
对跨域的额外处理
在跨域的应用场景中,需要服务端做一些额外的设置,这些设置是加在响应头上的。
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: Authorization
第一个表示允许来自任何域名的请求。第二个表示允许一些
自定义的请求头,因为 Authoriaztion 是自定义的,所以必须加上这个配置,如果各位使用了其他的请求头,请同样加上。
如果服务端用了 nginx,那么这些配置可以写在 nginx.conf 文件中。如果是在代码中配置,那么无论是 Java,还是 Node.js,都有 response.setHeader 方法。
我对 Web 安全方面的了解还不太深,所以没有太多经验可谈。安全性是一个在平常不太受重视的领域,因为完成一个项目的优先级从来都是:功能 & 颜值 & 性能, 安全 。至少得保证用户在使用过程中不会出错,然后再做得酷炫或清新一点,性能和安全只有在满足了前两项,或者迫在眉睫的时候才去考虑。当服务器承受不了那么高的负载了,才会去增加更多的服务器,但业务功能从一开始就不能少。
可是这样做有错吗?并没有吧。在特定的场景,做特定的处理,或许是性价比最高的决策了。
这篇文章中反复提到的一个词是“
约定”,它貌似和“
具体情况具体分析”这个观点矛盾了,额……。
约定是人与人之间的共识,比如说 GET 请求,那么对方的第一反应就是查询,当有人破坏约定,用 GET 请求去做删除操作时,就会让别人很难理解(当有一大堆人这么做的时候,就不难理解了吧……)。或者当我们提到 JWT 的时候,那它就应该是由三个部分组成,如果有人仅仅是按照自己的算法来生成一个 token,同样可以唯一标识用户,那他必须得像共事的人解释,这个算法的安全性、使用方法等。
另一方面,如果真心觉得按照“约定”办事没必要,太麻烦,并且可以接受“耍小聪明”的后果的话,那就按自己的想法去做吧(真的不再考虑一下了吗)。
为什么 HTML5 新增了那么多语义化的标签,是因为一切都在朝着更规范的方向走。
相关 [web jwt csrf] 推荐:
- SegmentFault 最新的文章
先解释两个名词,CSRF 和 JWT. CSRF (Cross Site Request Forgery),它讲的是你在一个浏览器中打开了两个标签页,其中一个页面通过窃取另一个页面的 cookie 来发送伪造的请求,因为 cookie 是随着请求自动发送到服务端的. JWT (JSON Web Token),通过某种算法将两个 JSON 对象加密成一个字符串,该字符串能代表唯一用户.
- 互联网 - ITeye博客
转自: /hyddd/archive//1432744.html.   CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF.
session 和cookie的目的相同,都是为了克服http协议无状态的缺陷,但完成的方法不同. session通过cookie,在客户端保存session id,而将用户的其他会话消息保存在服务端的session对象中,与此相对的,cookie需要将所有信息都保存在客户端. 因此cookie存在着一定 的安全隐患,例如本地cookie中保存的用户名密码被破译,或cookie被其他网站收集(例如:1.
- 酷勤网-挖经验 [expanded by feedex.net]
酷勤网 ? 程序员的那点事.   CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF.   你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求.
CSRF(Cross-site request forgery)跨站请求伪造,由于目标站无token/referer限制,导致攻击者可以用户的身份完成操作达到各种目的. 根据HTTP请求方式,CSRF利用方式可分为两种. 0x01 GET类型的CSRF. 这种类型的CSRF一般是由于程序员安全意识不强造成的.
- 互联网 - ITeye博客
CSRF 攻击的应对之道. 牛 刚, 软件工程师, IBM. 童 强国, 高级软件工程师, IBM. 简介: CSRF(Cross Site Request Forgery, 跨站域请求伪造)是一种网络的攻击方式,该攻击可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击站点,从而在并未授权的情况下执行在权限保护之下的操作,有很大的危害性.
- 文章 – 伯乐在线
CSRF(Cross-site request forgery,中文为
跨站请求伪造)是一种利用网站可信用户的权限去执行未授权的命令的一种恶意攻击. 通过
伪装可信用户的请求来利用信任该用户的网站,这种攻击方式虽然不是很流行,但是却难以防范,其危害也不比其他安全漏洞小. 本文将简要介绍CSRF产生的原因以及利用方式,然后对如何避免这种攻击方式提供一些可供参考的方案,希望广大程序猿们都能够对这种攻击方式有所了解,避免自己开发的应用被别人利用.
- CSDN博客架构设计推荐文章
在Spring MVC应用中实施CSRF防御,一般会采用 EYAL
LUPU的方案,该方案的基本思路是在生成表单时在其中插入一个随机数作为签名,在表单提交后对其中的签名进行验证,根据验证的结果区分该表单是否是经由应用签署的合法表单. 如果签名不正确或不存在签名,则说明请求可能已被劫持. EYAL LUPU方案的巧妙之处在于,通过使用HandlerInterceptorAdapter和Spring3.1中新引入的ReuqestDataValueProcessor这一对组合,使得签名和验证的过程无缝地集成到现有应用中.
- CSDN博客研发管理推荐文章
首先说明一下什么是CSRF(Cross Site Request Forgery). 跨站请求伪造是指攻击者可以在第三方站点制造HTTP请求并以用户在目标站点的登录态发送到目标站点,而目标站点未校验请求来源使第三方成功伪造请求. JS控制浏览器发送请求的时候,浏览器是根据目标站点,而不是来源站点,来发送cookie的,如果当前会话中有目标站点的cookie,就发送出去.
- 牛X阿德马
本文的目的是展示CSRF漏洞的危害,以D-link的DIR-600路由器(硬件版本:BX,固件版本:2.16)的CSRF漏洞为例. D-link的CSRF漏洞已经是公开的,本文将详细描述一下整个D-link CSRF漏洞的利用,如何通过CSRF漏洞实现远程管理访问D-link路由器. 如果某些request请求中没有csrf
token或不需要密码授权,会存在CSRF漏洞,该漏洞允许攻击者伪造登录用户发送请求,因此可以导致用户执行攻击者想要的操作请求.
坚持分享优质有趣的原创文章,并保留作者信息和版权声明,任何问题请联系:@。君,已阅读到文档的结尾了呢~~
初步认识计算机认识,初步认识,计算机
扫扫二维码,随身浏览文档
手机或平板扫扫即可继续访问
初步认识计算机
举报该文档为侵权文档。
举报该文档含有违规或不良信息。
反馈该文档无法正常浏览。
举报该文档为重复文档。
推荐理由:
将文档分享至:
分享完整地址
文档地址:
粘贴到BBS或博客
flash地址:
支持嵌入FLASH地址的网站使用
html代码:
&embed src='/DocinViewer-4.swf' width='100%' height='600' type=application/x-shockwave-flash ALLOWFULLSCREEN='true' ALLOWSCRIPTACCESS='always'&&/embed&
450px*300px480px*400px650px*490px
支持嵌入HTML代码的网站使用
您的内容已经提交成功
您所提交的内容需要审核后才能发布,请您等待!
3秒自动关闭窗口微服务架构中的身份验证问题 :JSON Web Tokens( JWT)
本文翻译自:
软件安全是一件很负责的问题,由于微服务系统中每个服务都要处理安全问题,所以在微服务场景下会更加复杂,一般我们会四种面向微服务系统的身份验证方案。
在传统的单体架构中,单个服务保存所有的用户数据,可以校验用户,并在认证成功后创建HTTP会话。在微服务架构中,用户是在和服务集合交互,每一个用户都有可能需要知道请求的用户是谁。一种简单的方案是在微服务中,采用与单体系统中相同的模式,但问题是如何让所有的服务访问用户的数据
解决这个问题大致2个思路:(1)使用共享数据库时,更新数据库表会成为一个难题,因为所有的服务必须同时升级以便能够对接修改后的表解构;(2)将相同的数据分发给所有的服务时,当某个用户已经被认证,如何让每个服务都知晓这个状态是个问题。
方案1:单点登录(SSO)方案, 采用单点登录方案,意味着每个面向用户的服务都必须与认证服务交互,这会产生大量非常琐碎的网络流量,同时这个防范实现起来也相当的复杂。在其他方面,选择SSO方案安全性会很好,用户登录状态是不透明的,可防止攻击者从状态中推断任何有用的信息。
方案2:分布式会话方案,原理主要是将关于用户信息存储在共享内存中,并通常由用户会话作为key来实现简单的分布式哈希映射。当用户访问微服务时,用户数据可以从共享存储中获取。该方案的另外一个优点就是用户登录状态不是透明的。当使用分布式数据库时,它也是一个高度可用且可可扩展的解决方案。这种方案的优点是在于共享存储需要一定保护机制,因此需要通过安全链接来访问,这时解决方案的实现就通常具有相当高的负责性了。
方案3:token客户端令牌方案,此令牌在客户端生成,由身份验证服务进行签名,并且必须包含足够的信息,以便可以在所有微服务中建立用户身份。令牌会附加到每一个请求上,为微服务提供身份验证。这种解决方案安全性相对较好,但身份验证注销是一个大大的问题,缓解这种情况的方法可以使用短期令牌access_token 和频繁检查认证服务器等。对于客户端令牌的编码方案,可以使用 JSON Web Tokens( JWT), 它足够简单且支持程度也比较好。
方案4:客户端令牌与API网关结合,这个方案意味着所有的请求都通过网关,从而有效地隐藏了微服务。在请求时,网关将原始用户令牌转换为内部会话(session)ID令牌。在这种情况下,注销就不在是个大大的问题, 因为网关在注销时可以撤销用户的令牌。这种方案虽然支持程度比较好,但是实现起来还是可能相当的复杂。
个人推荐方案:客户端令牌(JWT) + API网关结合的方案,因为这个方案通常使用起来比较容易,且性能也不错。SSO方案虽然能满足需求,但尽量避免使用,若分布式会话方案所需要的相关技术已经应用在你的场景中,那么这个方案也是比较有趣的。在考虑方案的时候,应该考虑注销的重要性。
api网关,参考这篇,
这篇文章将会知指导你如何用spring boot实现JWT的授权。
文章将会涉及到下面2个方面:
1. Ajax 授权
2. JWT token 授权
请在你细读本篇文章的时候,先看看Github 上的简单项目:。
这个项目是使用H2 内存数据库来存储简单的用户信息。为了让事情变的更加简单,我已经配置了spring boot在自动加载Application自动启动的时候,已经创建了数据设备和配置了spring boot的相关配置(/ jwt-demo / src / main /资源/ data.sql)。
先预览一下下面的项目结构:
+—profile
—endpoint
+—security
+—extractor
—verifier
+—endpoint
+—exceptions
+—repository
—resources
—templates
当我们在谈论Ajax授权的时候,我们通常会联想到的是用户是通过JSON 的方式提供凭证,并以XMLHttpRequest 的方式发送的场景。
在本教程的第一部分,Ajax实现身份验证是遵循Spring Security 框架的标准模式。
下面列表的东西,将是要我们去实现的:
1. AjaxLoginProcessingFilter
2. AjaxAuthenticationProvider
3. AjaxAwareAuthenticationSuccessHandler
4. AjaxAwareAuthenticationFailureHandler
5. RestAuthenticationEntryPoint
6. WebSecurityConfig
在我们实现这些细节的时候,让我们细看一下,下面的 request/response 的授权流程。
Ajax 授权请求的例子
身份验证API允许用户传入凭据,来获得身份验证令牌token。
在我们的例子中,客户端启动验证过程通过调用身份验证API(/API/auth/login)。
我们可以写这样的Http 请求:
POST /api/auth/login HTTP/1.1
Host: localhost:9966
X-Requested-With: XMLHttpRequest
Content-Type: application/json
Cache-Control: no-cache
"username": "",
"password": "test1234"
终端可以用CURL:
curl -X POST -H "X-Requested-With: XMLHttpRequest" -H "Content-Type: application/json" -H "Cache-Control: no-cache" -d '{
"username": "",
"password": "test1234"
}' "http://localhost:9966/api/auth/login"
Ajax 授权相应的例子
如果客户端请求的凭证被通过,授权API将会返回Http响应包括下面的一些细节:
1. Http 状态 "200 OK"
2. 带有 JWT的access_toke 和 refresh_token 都会在 response body中被包含了。
JWT Refresh token
用来获取新的 access_token, 刷新token 可以用这样的API来处理:/api/auth/token.(刷新可以用来防止access_token 的过期)
获取的HTTP 响应格式:
"token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMDMzMzA4LCJleHAiOjE0NzIwMzQyMDh9.41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ",
"refreshToken": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfUkVGUkVTSF9UT0tFTiJdLCJpc3MiOiJodHRwOi8vc3ZsYWRhLmNvbSIsImp0aSI6IjkwYWZlNzhjLTFkMmUtNDg2OS1hNzdlLTFkNzU0YjYwZTBjZSIsImlhdCI6MTQ3MjAzMzMwOCwiZXhwIjoxNDcyMDM2OTA4fQ.SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg"
JWT Access Token
JWT访问令牌可用于身份验证和授权:
1. 身份验证是由验证JWT访问令牌签名。如果签名是有效的,访问API请求的资源是理所当然。
2. 授权是通过查找特权JWT scope属性的值来判断。(译者:scope,一般会是版本号或平台,安卓,ios,wap等或不同的系统的id, 具体看自家的场景和需求).
解码JWT token有三个部分:Header(请求头), Claims(要求) and Signature(签名)
"alg": "HS512"
Claims(要求):
"sub": "",
"scopes": [
"ROLE_ADMIN",
"ROLE_PREMIUM_MEMBER"
"iss": "",
签名base64 encoded)
41rxtplFRw55ffqcw1Fhy2pnxggssdWUU8CDOherC0Kw4sgt3-rw_mPSWSgQgsR0NLndFcMPh7LSQt5mkYqROQ
JWT Refresh Token
Refresh token 是长寿令牌用于请求新的访问令牌。Refresh token过期时间是超过access_token的过期时间。
在本次教程中,我们将使用 jti 声称来维持黑名单,或撤销令牌列表。JWT ID(jti) 声称被 RFC7519
定义了,目的是为了唯一地标识单个刷新令牌。
解码刷新令牌有三个部分: Header(请求头), Claims(要求), Signature(签名) 如下所示:
"alg": "HS512"
"sub": "",
"scopes": [
"ROLE_REFRESH_TOKEN"
"iss": "",
"jti": "90afe78c-1d2e-d754b60e0ce",
Signature (base64 encoded)
SEEG60YRznBB2O7Gn_5X6YbRmyB3ml4hnpSOxqkwQUFtqA6MZo7_n2Am2QhTJBJA1Ygv74F2IxiLv0urxGLQjg
AjaxLoginProcessingFilter( Ajax 登录处理过滤器)
首先,需要继承AbstractAuthenticationProcessingFilter, 目的是为了提供一般常用的Ajax 身份验证请求。反序列化JSON和基本验证的主要任务都是在的。AjaxLoginProcessingFilter#attemptAuthentication这个方法里完成的。在成功验证JSON的主要检验逻辑是委托给AjaxAuthenticationProvider类实现。
在一个成功校验中, AjaxLoginProcessingFilter#successfulAuthentication
方法会被调用。
在一个失败的检验中,AjaxLoginProcessingFilter#unsuccessfulAuthentication
方法被调用。
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static Logger logger = LoggerFactory.getLogger(AjaxLoginProcessingFilter.class);
private final AuthenticationSuccessHandler successH
private final AuthenticationFailureHandler failureH
private final ObjectMapper objectM
public AjaxLoginProcessingFilter(String defaultProcessUrl, AuthenticationSuccessHandler successHandler,
AuthenticationFailureHandler failureHandler, ObjectMapper mapper) {
super(defaultProcessUrl);
this.successHandler = successH
this.failureHandler = failureH
this.objectMapper =
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (!HttpMethod.POST.name().equals(request.getMethod()) || !WebUtil.isAjax(request)) {
if(logger.isDebugEnabled()) {
logger.debug("Authentication method not supported. Request method: " + request.getMethod());
throw new AuthMethodNotSupportedException("Authentication method not supported");
LoginRequest loginRequest = objectMapper.readValue(request.getReader(), LoginRequest.class);
if (StringUtils.isBlank(loginRequest.getUsername()) || StringUtils.isBlank(loginRequest.getPassword())) {
throw new AuthenticationServiceException("Username or Password not provided");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
return this.getAuthenticationManager().authenticate(token);
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
successHandler.onAuthenticationSuccess(request, response, authResult);
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
AjaxAuthenticationProvider
AjaxAuthenticationProvider类的责任是:
1. 对用户凭证与 数据库、LDAP或其他系统用户数据,进行验证。
2. 如果用户名和密码不匹配数据库中的记录,身份验证异常将会被抛出。
3. 创建用户上下文,你需要一些你需要的用户数据来填充(例如 用户名 和用户密码)
4. 在成功验证委托创建JWT令牌的是在* AjaxAwareAuthenticationSuccessHandler* 中实现。
@Component
public class AjaxAuthenticationProvider implements AuthenticationProvider {
private final BCryptPasswordE
private final DatabaseUserService userS
@Autowired
public AjaxAuthenticationProvider(final DatabaseUserService userService, final BCryptPasswordEncoder encoder) {
this.userService = userS
this.encoder =
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.notNull(authentication, "No authentication data provided");
String username = (String) authentication.getPrincipal();
String password = (String) authentication.getCredentials();
User user = userService.getByUsername(username).orElseThrow(() -& new UsernameNotFoundException("User not found: " + username));
if (!encoder.matches(password, user.getPassword())) {
throw new BadCredentialsException("Authentication Failed. Username or Password not valid.");
if (user.getRoles() == null) throw new InsufficientAuthenticationException("User has no roles assigned");
List&GrantedAuthority& authorities = user.getRoles().stream()
.map(authority -& new SimpleGrantedAuthority(authority.getRole().authority()))
.collect(Collectors.toList());
UserContext userContext = UserContext.create(user.getUsername(), authorities);
return new UsernamePasswordAuthenticationToken(userContext, null, userContext.getAuthorities());
public boolean supports(Class&?& authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
AjaxAwareAuthenticationSuccessHandler
我们将实现AuthenticationSuccessHandler接口时,称为客户端已成功进行身份验证。
AjaxAwareAuthenticationSuccessHandler AuthenticationSuccessHandler接口的类是我们的自定义实现。这个类的责任是添加JSON载荷包含JWT访问和刷新令牌到HTTP响应的body。
@Component
public class AjaxAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectM
private final JwtTokenFactory tokenF
@Autowired
public AjaxAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory) {
this.mapper =
this.tokenFactory = tokenF
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
UserContext userContext = (UserContext) authentication.getPrincipal();
JwtToken accessToken = tokenFactory.createAccessJwtToken(userContext);
JwtToken refreshToken = tokenFactory.createRefreshToken(userContext);
Map&String, String& tokenMap = new HashMap&String, String&();
tokenMap.put("token", accessToken.getToken());
tokenMap.put("refreshToken", refreshToken.getToken());
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(response.getWriter(), tokenMap);
clearAuthenticationAttributes(request);
* Removes temporary authentication-related data which may have been stored
* in the session during the authentication process..
protected final void clearAuthenticationAttributes(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
session.removeAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);
让我们关注一下如何创建JWT访问令牌。在本教程中,我们使用Java JWT, Stormpath这个人创建的库。
&io.jsonwebtoken&
&${jjwt.version}&
我们已经创建了工厂类(JwtTokenFactory)分离令牌创建逻辑。
方法JwtTokenFactory # createAccessJwtToken创建签署了JWT访问令牌。
方法JwtTokenFactory # createRefreshToken创建签署了JWT刷新令牌。
@Component
public class JwtTokenFactory {
private final JwtSettings settings
@Autowired
public JwtTokenFactory(JwtSettings settings) {
this.settings = settings
public AccessJwtToken createAccessJwtToken(UserContext userContext) {
if (StringUtils.isBlank(userContext.getUsername()))
throw new IllegalArgumentException("Cannot create JWT Token without username")
if (userContext.getAuthorities() == null || userContext.getAuthorities().isEmpty())
throw new IllegalArgumentException("User doesn't have any privileges")
Claims claims = Jwts.claims().setSubject(userContext.getUsername())
claims.put("scopes", userContext.getAuthorities().stream().map(s -& s.toString()).collect(Collectors.toList()))
DateTime currentTime = new DateTime()
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuedAt(currentTime.toDate())
.setExpiration(currentTime.plusMinutes(settings.getTokenExpirationTime()).toDate())
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact()
return new AccessJwtToken(token, claims)
public JwtToken createRefreshToken(UserContext userContext) {
if (StringUtils.isBlank(userContext.getUsername())) {
throw new IllegalArgumentException("Cannot create JWT Token without username")
DateTime currentTime = new DateTime()
Claims claims = Jwts.claims().setSubject(userContext.getUsername())
claims.put("scopes", Arrays.asList(Scopes.REFRESH_TOKEN.authority()))
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setId(UUID.randomUUID().toString())
.setIssuedAt(currentTime.toDate())
.setExpiration(currentTime.plusMinutes(settings.getRefreshTokenExpTime()).toDate())
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact()
return new AccessJwtToken(token, claims)
AjaxAwareAuthenticationFailureHandler
AjaxAwareAuthenticationFailureHandle 是AjaxLoginProcessingFilter调用身份验证失败时,被调用的函数。
你可以设计基于异常类型特定的错误消息身份验证过程中发生的异常。
@Component
public class AjaxAwareAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final ObjectM
@Autowired
public AjaxAwareAuthenticationFailureHandler(ObjectMapper mapper) {
this.mapper =
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
if (e instanceof BadCredentialsException) {
mapper.writeValue(response.getWriter(), ErrorResponse.of("Invalid username or password", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
} else if (e instanceof JwtExpiredTokenException) {
mapper.writeValue(response.getWriter(), ErrorResponse.of("Token has expired", ErrorCode.JWT_TOKEN_EXPIRED, HttpStatus.UNAUTHORIZED));
} else if (e instanceof AuthMethodNotSupportedException) {
mapper.writeValue(response.getWriter(), ErrorResponse.of(e.getMessage(), ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
mapper.writeValue(response.getWriter(), ErrorResponse.of("Authentication failed", ErrorCode.AUTHENTICATION, HttpStatus.UNAUTHORIZED));
JWT Authentication
基于身份验证模式的token(身份令牌),已经成为近年来非常流行的验证模式,他相比较于session/cookie, token能提供更加重要的好处。
2. 不需要CSRF的保护。
3. 更好的和移动端进行集成。
4. 减少了授权服务器的负载。
5. 不再需要分布式会话的存储。
有一些交互操作会用这种方式需要权衡的地方:
1. 更容易受到XSS攻击
2. 访问令牌可以包含过时的授权声明(e。g当一些用户权限撤销)
3. 在claims 的数在曾长的时候,Access token 也能在一定程度上增长。
4. 文件下载API难以实现的。
5. 无状态和撤销是互斥的。
在本文中,我们将探讨如何JWT的可以用于基于令牌的身份验证。
JWT 的授权流程是非常的简单的:
1. 用户可以向授权服务器提供凭证来获取刷新和访问令牌(refresh_token 和 access_token).
2. 用户发送每一个请求去访问受保护的资源的时候都需要access token
做为参数。
3. 问令牌是签名,并包含用户身份(例如,用户id)和授权声明。
这是非常重要的,注意到授权声明将包含Access token. 为什么他是如此之重要?很好,让我们来先说说授权声明吧(例如:在数据库中用户权限)被改变了,在Access token 还有效的这个时间周期里,这些更改不会生效,直到新的访问令牌。在更多的情况下,这并不是一个很大的问题,因为Access token 拥有很短的生命周期。 否则就是不透明的标记模式。
在我们探索实现细节之前,让我们先看看一个被保护的资源的API的简单请求。
Signed request to protected API resource
当发送 access token时,下面的一些样式将会被使用,Bearer。 在我们的例子中,header 的名字(),我们将会使用 X-Authorization.
Raw HTTP request:
GET /api/me HTTP/1.1
Host: localhost:9966
X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w
Cache-Control: no-cache
curl -X GET -H "X-Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJzdmxhZGFAZ21haWwuY29tIiwic2NvcGVzIjpbIlJPTEVfQURNSU4iLCJST0xFX1BSRU1JVU1fTUVNQkVSIl0sImlzcyI6Imh0dHA6Ly9zdmxhZGEuY29tIiwiaWF0IjoxNDcyMzkwMDY1LCJleHAiOjE0NzIzOTA5NjV9.Y9BR7q3f1npsSEYubz-u8tQ8dDOdBcVPFN7AIfWwO37KyhRugVzEbWVPO1obQlHNJWA0Nx1KrEqHqMEjuNWo5w" -H "Cache-Control: no-cache" "http://localhost:9966/api/me"
让我们来看一下下面的细节,下面的一些组件,我们需要实现JWT 的身份验证:
1. JwtTokenAuthenticationProcessingFilter
2. JwtAuthenticationProvider
3. SkipPathRequestMatcher
4. JwtHeaderTokenExtractor
5. BloomFilterTokenVerifier
6. WebSecurityConfig
JwtTokenAuthenticationProcessingFilter
JwtTokenAuthenticationProcessingFilter
过滤器 被应用到了每一个API(/api/**) 异常的刷新令牌端点(/api/auth/token)以及 login 点(/api/auth/login)。
这个过滤器拥有一下的一些职责:
1. 检查访问令牌在X-Authorization头。如果发现访问令牌的头,委托认证JwtAuthenticationProvider否则抛出身份验证异常
2. 调用成功或失败策略基于由JwtAuthenticationProvider执行身份验证过程的结果
确保chain.doFilter(request, response) 被调用,成功的验证了身份。你想在下一个处理器中,优先处理这些请求, 因为最后一个过滤器 FilterSecurityInterceptor#doFilter
会响应的实际调用方法是在Controller 中的处理访问API 资源的方法。
public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private final AuthenticationFailureHandler failureH
private final TokenExtractor tokenE
@Autowired
public JwtTokenAuthenticationProcessingFilter(AuthenticationFailureHandler failureHandler,
TokenExtractor tokenExtractor, RequestMatcher matcher) {
super(matcher);
this.failureHandler = failureH
this.tokenExtractor = tokenE
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
String tokenPayload = request.getHeader(WebSecurityConfig.JWT_TOKEN_HEADER_PARAM);
RawAccessJwtToken token = new RawAccessJwtToken(tokenExtractor.extract(tokenPayload));
return getAuthenticationManager().authenticate(new JwtAuthenticationToken(token));
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
Authentication authResult) throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException failed) throws IOException, ServletException {
SecurityContextHolder.clearContext();
failureHandler.onAuthenticationFailure(request, response, failed);
JwtHeaderTokenExtractor 是一个非常简单的类,通常用来扩展来处理身份检验的处理。
你可以扩展TokenExtractor
接口 和 提供你常用的一些实现。例如从URL中提取标记。
@Component
public class JwtHeaderTokenExtractor implements TokenExtractor {
public static String HEADER_PREFIX = "Bearer ";
public String extract(String header) {
if (StringUtils.isBlank(header)) {
throw new AuthenticationServiceException("Authorization header cannot be blank!");
if (header.length() & HEADER_PREFIX.length()) {
throw new AuthenticationServiceException("Invalid authorization header size.");
return header.substring(HEADER_PREFIX.length(), header.length());
JwtAuthenticationProvider
JwtAuthenticationProvider 拥有一下的一些职责:
1. 验证 access token 的签名
2. 从访问令牌中提取身份和授权声明和使用它们来创建UserContext
3. 如果访问令牌是畸形的,过期的或者只是如果令牌不签署与适当的签名密钥身份验证就会抛出异常
@Component
public class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtSettings jwtS
@Autowired
public JwtAuthenticationProvider(JwtSettings jwtSettings) {
this.jwtSettings = jwtS
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
RawAccessJwtToken rawAccessToken = (RawAccessJwtToken) authentication.getCredentials();
Jws&Claims& jwsClaims = rawAccessToken.parseClaims(jwtSettings.getTokenSigningKey());
String subject = jwsClaims.getBody().getSubject();
List&String& scopes = jwsClaims.getBody().get("scopes", List.class);
List&GrantedAuthority& authorities = scopes.stream()
.map(authority -& new SimpleGrantedAuthority(authority))
.collect(Collectors.toList());
UserContext context = UserContext.create(subject, authorities);
return new JwtAuthenticationToken(context, context.getAuthorities());
public boolean supports(Class&?& authentication) {
return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
SkipPathRequestMatcher
JwtTokenAuthenticationProcessingFilter 过滤器被配置为跳过这个点:/api/auth/login
和 /api/auth/token
. 这三个通过 SkipPathRequestMatcher 实现 RequestMatcher 接口来实现。
public class SkipPathRequestMatcher implements RequestMatcher {
private OrRequestM
private RequestMatcher processingM
public SkipPathRequestMatcher(List&String& pathsToSkip, String processingPath) {
Assert.notNull(pathsToSkip);
List&RequestMatcher& m = pathsToSkip.stream().map(path -& new AntPathRequestMatcher(path)).collect(Collectors.toList());
matchers = new OrRequestMatcher(m);
processingMatcher = new AntPathRequestMatcher(processingPath);
public boolean matches(HttpServletRequest request) {
if (matchers.matches(request)) {
return false;
return processingMatcher.matches(request) ? true : false;
WebSecurityConfig
WebSecurityConfig 类 继承 WebSecurityConfigurerAdapter 去提供常用的security configuration .
下面的Beans 类被配置和实例化了:
1. AjaxLoginProcessingFilter
2. JwtTokenAuthenticationProcessingFilter
3. AuthenticationManager
4. BCryptPasswordEncoder
同时,在 WebSecurityConfig#configure(HttpSecurity http) 方法中,我们将配置样式去定义 被保护/非被保护的API节点。请注意,我们已经不能用CSRF保护了,因为我们并没有使用Cookies.
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
public static final String FORM_BASED_LOGIN_ENTRY_POINT = "/api/auth/login";
public static final String TOKEN_BASED_AUTH_ENTRY_POINT = "/api/**";
public static final String TOKEN_REFRESH_ENTRY_POINT = "/api/auth/token";
@Autowired private RestAuthenticationEntryPoint authenticationEntryP
@Autowired private AuthenticationSuccessHandler successH
@Autowired private AuthenticationFailureHandler failureH
@Autowired private AjaxAuthenticationProvider ajaxAuthenticationP
@Autowired private JwtAuthenticationProvider jwtAuthenticationP
@Autowired private TokenExtractor tokenE
@Autowired private AuthenticationManager authenticationM
@Autowired private ObjectMapper objectM
protected AjaxLoginProcessingFilter buildAjaxLoginProcessingFilter() throws Exception {
AjaxLoginProcessingFilter filter = new AjaxLoginProcessingFilter(FORM_BASED_LOGIN_ENTRY_POINT, successHandler, failureHandler, objectMapper);
filter.setAuthenticationManager(this.authenticationManager);
protected JwtTokenAuthenticationProcessingFilter buildJwtTokenAuthenticationProcessingFilter() throws Exception {
List&String& pathsToSkip = Arrays.asList(TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT);
SkipPathRequestMatcher matcher = new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT);
JwtTokenAuthenticationProcessingFilter filter
= new JwtTokenAuthenticationProcessingFilter(failureHandler, tokenExtractor, matcher);
filter.setAuthenticationManager(this.authenticationManager);
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
protected void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(ajaxAuthenticationProvider);
auth.authenticationProvider(jwtAuthenticationProvider);
protected BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
protected void configure(HttpSecurity http) throws Exception {
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(this.authenticationEntryPoint)
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.authorizeRequests()
.antMatchers(FORM_BASED_LOGIN_ENTRY_POINT).permitAll()
.antMatchers(TOKEN_REFRESH_ENTRY_POINT).permitAll()
.antMatchers("/console").permitAll()
.authorizeRequests()
.antMatchers(TOKEN_BASED_AUTH_ENTRY_POINT).authenticated()
.addFilterBefore(buildAjaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(buildJwtTokenAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
BloomFilterTokenVerifier
这是虚拟类。你应该实现自己的TokenVerifier检查撤销令牌。
@Component
public class BloomFilterTokenVerifier implements TokenVerifier {
public boolean verify(String jti) {
return true;
我在网络上听到有人窃窃私语,失去JWT令牌就像失去你的房子的钥匙。所以要小心。
看过本文的人也看了:
我要留言技术领域:
取消收藏确定要取消收藏吗?
删除图谱提示你保存在该图谱下的知识内容也会被删除,建议你先将内容移到其他图谱中。你确定要删除知识图谱及其内容吗?
删除节点提示无法删除该知识节点,因该节点下仍保存有相关知识内容!
删除节点提示你确定要删除该知识节点吗?}

我要回帖

更多关于 初步如何面试应聘者 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信