关于jwt:浅谈一下前后端鉴权方式-^^

浅谈一下前后端鉴权形式 ^.^尽管自己当初从事前端开发,然而之前始终是 PHP 全栈,所以对前后端鉴权机制也有肯定的理解,就找些材料简略记录一下吧。(瞎掰扯~)常见鉴权机制HTTP 是无状态的协定(对于事务处理没有记忆能力,每次客户端和服务端会话实现时,服务端不会保留任何会话信息。):每个申请都是齐全独立的,服务端无奈确认以后访问者的身份信息,无奈分辨上一次的申请发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(晓得是谁在拜访我),就必须被动的去保护一个状态,这个状态用于告知服务端前后两个申请是否来自同一浏览器,由此产生了很多种鉴权形式。HTTP Basic AuthenticationSession-CookieTokenOAuth另外咱们要留神辨别 Authentication 与 Authrization,一个是认证一个是受权。Authentication 是为了验证你是不是自己,而 Authrization 是为了验证你有没有做某件事情的权限。咱们别离举三个例子来阐明三种状况让大家对认证和受权的关系有更好的了解。只认证不受权 只是登录利用,并不进行其余操作,这时候不须要受权只进行认证。既认证又受权 咱们应用第三方利用登录的时候,既输出了第三方利用的账号密码来认证,又受权了本利用读取第三方登录利用曾经注册了的个人信息数据等。不认证只受权 咱们点开小程序时,须要获取个人信息,这种时候相当于只受权数据给小程序,并未进行认证,毕竟在利用外部应用小程序,很少有须要再登录认证这种操作。各鉴权机制流程与原理 一旦波及认证受权,必须要思考的一个问题就是状态治理。所谓的状态治理就是说咱们在进行登录之后的一段时间里,不心愿每次拜访它都须要从新登录。所以开发者必须要思考怎么样放弃用户的登录状态以及设置生效工夫。而这个过程须要前后端通力合作来实现。 上面就来简略谈一下几种常见的认证和受权形式的流程与原理,自己瞎掰扯,欢送大佬指导。HTTP Basic Authentication 这种受权形式是浏览器恪守 HTTP 协定实现的根本受权形式,HTTP 协定进行通信的过程中定义了根本认证容许 HTTP 服务器对客户端进行用户身份证的办法。 根本流程发送申请:客户端向服务器申请数据,申请的内容可能是一个网页或者是一个 ajax 异步申请,此时假如客户端尚未被验证(服务器验证并判断是否返回 401),则客户端提供如下申请至服务器。Get /index.html HTTP/1.0 Host: www.google.com服务器返回 401:服务器向客户端发送验证申请代码 401,WWW-Authenticate: Basic realm="google.com" 这句话是要害,如果没有客户端不会弹出用户名和明码输出界面,服务器返回的数据大抵如下。HTTP/1.0 401 Unauthorised Server: SokEvo/1.0 WWW-Authenticate: Basic realm="google.com"Content-Type: text/html Content-Length: xxx客户端弹出窗口:当合乎 http1.0 或 1.1 标准的客户端收到 401 返回值时,将自动弹出一个登录窗口,要求用户输出用户名和明码。 这个时候申请时属于 pending 状态,当用户输出用户名明码的时候客户端会再次发送申请头带 Authorization 的申请。用户输出用户名和明码:输出明码后,点击提交会将用户名及明码以 Base64 加密形式加密,并将密文放入前一条申请信息中,则客户端发送的第一条申请信息则变成如下内容。Get /index.html HTTP/1.0 Host: www.google.comAuthorization: Basic xxxxxxx(base64 密文)// 加密过程是浏览器默认的行为,不须要咱们人为加密,咱们只须要输出用户名明码即可。服务端解密:服务器收到上述申请信息后,将 Authorization 字段后的用户信息取出并解密,将解密后的用户名及明码与用户数据库进行比拟验证,如用户名及明码正确,服务器则依据申请,将所申请资源发送给客户端。 ...

September 28, 2023 · 3 min · jiezi

关于jwt:gozero-jwt-鉴权快速实战

后面咱们分享了 go-zero 的疾速实战以及日志组件的分析,本次咱们来实战应用 go-zero jwt 鉴权 本次文章次要是分享对于 go-zero 中 jwt 的应用形式,会以一个 demo 的形式来进行实战,对于应用 goctl 工具以及装置细节就不在赘述,有需要的话能够查看: 官网本次文章次要分为如下几个局部: Jwt 的简略介绍<!----> Go-zero 中应用 jwt 实战Jwt 的简略介绍对于 jwt 鉴权的细节和原理,感兴趣的敌人能够查看历史文章:JWT身份认证(附带源码解说) | GO主题月 那么咱们如何辨认什么时候须要应用 jwt 呢? 用于受权例如某个零碎通过例如账号密码登录之后,后盾会生成一个 jwt,这个用户在这个零碎之后的任何操作,都会去校验这个 jwt,就不须要用户操作系统内其余模块的时候,还去进行一次登录 当然,这是须要咱们做好设定,这个 jwt 针对哪一些路由能够应用,从而容许用户拜访该令牌容许的路由,服务和资源 用于信息替换因为 jwt 能够与各方进行平安的传输,外部应用了签名算法,公钥加密,私钥解密,而且 jwt 的数据各种中有标头,有效载荷,以及其余的签发工夫,过期工夫,颁发人等等,能够用来校验信息是否被篡改了 Go-zero 中应用 jwt 实战话不多说,咱们就来开始实战吧,先来给本人提一个需要: 需要同学们平时去图书馆,都是须要登录本人的账号,才能够进入零碎查问书籍余量的<!----> 那么咱们就来实现一下,用账号密码登录图书零碎,并生成一个 jwt,后续该用户进行书籍查问的时候,就能够间接应用 jwt 来进行鉴权剖析那么,根据上述需要,显然,咱们会应用到数据库,本次这里咱们依然应用 mysql 进行演示,并且会波及到用户表和图书表 咱们能够当初创立一下这两张表 user tableuser.sql CREATE TABLE `user`( `stu_id` varchar(255) NOT NULL COMMENT 'stu_id', `name` varchar(255) NOT NULL COMMENT 'name', `password` varchar(255) NOT NULL COMMENT 'password', `gender` varchar(255) NOT NULL COMMENT 'gender', PRIMARY KEY(`stu_id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stu_id学号name姓名password明码gender性别book tablebook.sql ...

September 2, 2023 · 3 min · jiezi

关于jwt:实践篇教你玩转JWT认证从一个优惠券聊起-京东云技术团队

引言最近面试过程中,无心中跟候选人聊到了JWT相干的货色,也就联想到我本人对于JWT落地过的那些我的项目。 对于JWT,能够说是分布式系统下的一个利器,我在我的很多我的项目实际中,认证零碎的第一抉择都是JWT。它的劣势会让你骑虎难下,就像你领优惠券一样。 大家回顾一下一个场景,如果你和你的女朋友想吃某江家的烤鱼了,你会怎么做呢? 传统的时代,我想场景是这样的:咱们走进一家某江家餐厅,会被服务员疏导一个桌子,而后咱们开始点餐,服务原会记录咱们点餐信息,而后在送到后厨去。这个过程中,那个餐桌就相当于session,而咱们的点餐信息回记录到这个session之中,而后送到后厨。这个是一个典型的基于session的认证过程。但咱们也发现了它的弊病,就是基于session的这种认证,对服务器强依赖,而且信息都是存储在服务器之上,灵活性和扩展性大大降低。 而互联网时代,公众点评、美团、饿了么给了咱们另一个抉择,咱们可能第一工夫会在这些平台上搜寻江边城外的优惠券,这个优惠券中可能会形容着两人实惠套餐明细。这张优惠券就是咱们的 JWT,咱们能够在任何一家有参加优惠活动的餐厅应用这张优惠券,而不用被限度在同一家餐厅。同时这张优惠券中间接记录了咱们的点餐明细,等咱们到了餐厅,只须要将优惠券二维码告知服务员,服务员就会给咱们端上咱们想要的食物。 好了,以上只是一个小例子,其实只是想阐明一下JWT相较于传统的基于session的认证框架的劣势。 JWT 的劣势在于它能够跨域、跨服务器应用,而 Session 则只能在本域名下应用。而且,JWT 不须要在服务端保留用户的信息,只须要在客户端保留即可,这加重了服务端的累赘。 这一点在分布式架构下劣势还是很显著的。 什么是JWT说了这么多,如何定义JWT呢? JWT(JSON Web Token)是一种用于在网络应用中进行身份验证的凋谢规范(RFC7519)。它能够平安地在用户和服务器之间传输信息,因为它应用数字签名来验证数据的完整性和真实性。 JWT蕴含三个局部:头部、载荷和签名。头部蕴含算法和类型信息,载荷蕴含用户的信息,签名用于验证数据的完整性和真实性。 额定说一下poload,也就是负荷局部,这块是jwt的外围模块,它外部包含一些申明(claims)。申明由三个类型组成: Registered Claims:这是预约义的申明名称,次要包含以下几种: iss:Token 发行者sub:Token 主题aud:Token的受众exp:Token 过期工夫iat:Token发行工夫jti:Token惟一标识符Public Claims:公共申明是本人定义的申明名称,以防止抵触。 Private Claims:公有申明与公共申明相似,不同之处在于它是用于在单方之间共享信息的。 当用户登录时,服务器将生成一个JWT,并将其作为响应返回给客户端。客户端将在后续的申请中发送此JWT。服务器将应用雷同的密钥验证JWT的签名,并从载荷中获取用户信息。如果签名验证通过并且用户信息无效,则服务器将容许申请持续进行。 JWT长处JWT长处如果咱们零碎的总结一下, 如下: 跨语言和平台:JWT是基于JSON规范的,因而能够在不同的编程语言和平台之间进行替换和应用。无状态:因为JWT蕴含所有必要的信息,服务器不须要在每个申请中存储任何会话数据,因而能够轻松地进行负载平衡。安全性:JWT应用数字签名来验证数据的完整性和真实性,因而能够避免数据被篡改或伪造。可扩展性:JWT能够蕴含任何用户信息,因而能够轻松地扩大到其余应用程序中。一个基于JWT认证的计划我将举一个我理论业务落地的一个例子。 我的业务场景中个别都会有一个业务网关,该网关的外围性能就是鉴权和上线文转换。用户申请会将JWT字符串存与header之中,而后到网关后进行JWT解析,解析后的上下文信息,会转变成明文K-V的形式在此存于header之中,供零碎外部各个微服务之间相互调用时提供明文上下文信息。具体时序图如下: 基于Spring security的JWT实际JWT原理很简略,当然,你能够齐全本人实现JWT的全流程,然而,理论中,咱们个别不须要这么干,因为有很多成熟和好用的轮子提供给咱们,而且封装性和安全性也远比本人匆忙的封装一个简略的JWT来的高。 如果是基于学习JWT,我是倡议大家本人手写一个demo的,然而如果重实际的角度触发,咱们齐全能够应用Spring Security提供的JWT组件,来高效疾速的实现一个稳定性和安全性都十分高的JWT认证框架。 以下是我基于我的业务理论状况,依据保密性要求,简化了的JWT实际代码。也算是抛砖引玉,心愿能够给大家在业务场景中使用JWT做一个参考。 maven依赖首先,咱们须要增加以下依赖到pom.xml文件中: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>JWT工具类封装而后,咱们能够创立一个JwtTokenUtil类来生成和验证JWT令牌: import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.springframework.beans.factory.annotation.Value;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.stereotype.Component;import java.util.Date;import java.util.HashMap;import java.util.Map;import java.util.function.Function;@Componentpublic class JwtTokenUtil { private static final long JWT_TOKEN_VALIDITY = 5 * 60 * 60; @Value("${jwt.secret}") private String secret; public String generateToken(UserDetails userDetails) { Map<String, Object> claims = newHashMap <>(); return createToken(claims, userDetails.getUsername()); } private String createToken(Map<String, Object> claims, String subject) { Date now = new Date(); Date expiration = new Date(now.getTime() + JWT_TOKEN_VALIDITY * 1000); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(now) .setExpiration(expiration) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public boolean validateToken(String token, UserDetails userDetails) { final String username = extractUsername(token); return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractExpiration(token).before(new Date()); } public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public Date extractExpiration(String token) { return extractClaim(token, Claims::getExpiration); } private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); }}在这个实现中,咱们应用了jjwt库来创立和解析JWT令牌。咱们定义了以下办法: ...

May 19, 2023 · 3 min · jiezi

关于jwt:jwt身份认证概述

JWT全称JSON Web Token 利用流程客户端应用用户名和明码申请登录,服务端收到申请验证用户名和明码正确后,后端通过JWT机制,将用户数据作为JWT的Payload,同时在后面拼接上一个JWT Header之后进行Base64编码,并进行签名,生成一个token,格局为header.payload.signature,返回给客户端客户端后续的每次申请都须要携带token,携带在HTTP Header Authorization中后端拿到客户端传递的token后,进行解密验证身份,验证其有效性,查看签名是否正确,是否过期,最初解析出JWT Token中的用户信息长处无状态,token机制在服务端不须要存储session信息,因为token本身蕴含了所有登录用户的信息,所以能够加重服务端压力分布式敌对,因为session须要固定保留在一个中央,如果保留在本机,分布式系统中认证会生效,如果采纳redis等对立保留session,零碎复杂性会减少反对跨域拜访,跨域后不会存在信息失落问题CDN敌对,能够通过内容散发网络申请服务端的所有材料挪动端敌对,当客户端是非浏览器平台时,cookie是不被反对的,此时采纳token认证形式会简略很多无需思考CSRF(Cross Site Request Forgery跨站点申请伪造),token是开发者为了防备csrf而特地设计的令牌,浏览器不会主动增加到headers里,攻击者也无法访问用户的token,所以攻击者提交的表单无奈通过服务器认证,也就无奈造成攻打 CSRF简述 在一个浏览器中关上了两个标签页,其中一个页面通过窃取另一个页面的 cookie 来发送伪造的申请,因为cookie 是随着浏览器申请主动发送到服务端的,这个是CSRF攻打胜利的外围起因session认证实质须要依赖Cookie,如果cookie被截获,用户很容易受到跨站申请伪造攻打CSRF无奈间接窃取到用户的Cookie,header,仅仅是冒用Cookie毛病不可控,因为JWT无状态,想要在JWT 有效期内废除一个JWT或者更改它的权限的话,并不会立刻失效,通常须要等到有效期过后才能够,如果要防止这个问题,须要把JWT存入redis等缓存,JWT生效的时候就删除这个JWT,每次验证的时候查问一下JWT在不在redis,然而这样就减少了老本和零碎复杂度token续签不不便,JWT自身payload参数当中携带exp参数示意过期工夫,payload批改之后签名也须要批改,所以须要从新生成一个JWTJWT令牌个别会比拟长,如果是性能极度敏感的话须要在意这一点组成JWT生成的Token由三局部组成:header.payload.signature,艰深地说,JWT的实质是一个字符串,将用户信息保留到一个Json字符串中,而后进行编码后失去一个JWT token,并且这个JWT token带有签名信息,接管后能够校验是否被篡改,所以能够用于在各方之间平安地将信息作为Json对象传输 header alg:指定signature采纳的加密算法,默认是HS256(HMAC SHA256),对称加密(加密和解密的密钥雷同)typ:固定值,通常是JWT通常值是{"alg": "HS256", "typ": "JWT"}, 通过base64Url算法进行编码之后进行拼接payload 用户id和name默认携带iat,令牌签发工夫(工夫戳)exp设置令牌过期工夫参数个别模式如下,通过base64Url算法进行编码与header进行拼接,默认状况下JWT是未加密的,因为只是采纳base64算法,拿到JWT字符串后能够转换回本来的JSON数据,任何人都能够解读其内容,因而不要构建隐衷信息字段,比方用户的明码肯定不能保留到JWT中,JWT只是适宜在网络中传输一些非敏感的信息,要传递一些敏感数据的话须要应用一些AES或者其余类型的算法,尽量加上一些salt进行加密payload { "sub": "1234567890", "name": "Helen", "admin": true}signature 设置一个secretKey,通过将前两个后果合并后进行HS256算法,signature = HS256(base64Url(header)+'.'+base64Url(payload),secretKey)secreKey肯定不能裸露,因为能够颁发token,也能够解密跨域资源共享是一种基于 HTTP头的机制,该机制次要是为了防止跨站脚本攻打而存在 理论网站开发过程当中须要从A网站拜访到另外一个网站B,就须要通过容许服务器标示除了它本人以外的其余源(域、协定或端口),使得浏览器容许这些源拜访加载本人的资源,服务器须要返回一个HTTP Header Access-Control-Allow-Origin: * 表明,该资源能够被任意外源拜访,这样浏览器才会失常加载返回的数据 然而当携带cookie进行拜访的时候就不能返回一个Header头示意容许被任意外源拜访 服务器不能将 Access-Control-Allow-Origin 的值设为通配符“*”,而应将其设置为特定的域,如:Access-Control-Allow-Origin: https://example.com,如果值被设置为*,申请会失败,如歌设置为具体的域申请胜利服务器不能将 Access-Control-Allow-Headers 的值设为通配符“*”,而应将其设置为标头名称的列表,如:Access-Control-Allow-Headers: X-PINGOTHER, Content-Type服务器不能将 Access-Control-Allow-Methods 的值设为通配符“*”,而应将其设置为特定申请办法名称的列表,如:Access-Control-Allow-Methods: POST, GET详情参考Mozilla对跨域资源共享的定义 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORSpython实现依赖我的项目pyjwt https://github.com/jpadilla/pyjwt装置 $ pip install pyjwt应用 >>> import jwt>>> encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256")>>> print(encoded)eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzb21lIjoicGF5bG9hZCJ9.4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZg>>> jwt.decode(encoded, "secret", algorithms=["HS256"]){'some': 'payload'}参考浏览掘金-JWT与Token详解 ...

April 27, 2023 · 1 min · jiezi

关于jwt:浅析使用-JWT-的正确姿势

浅析应用 JWT 的正确姿态在很长的一段时间里,我都没有正确的应用 jwt,意识到这个问题之后,把我最实在的思考和总结拿进去和大家分享下,欢送一起探讨 现状先说下我以前是怎么应用的: 登录胜利后,将 userId 放进 payload 生成 jwt(有效期 8 小时),而后把 jwt 发送到前端,前端存储下来前端每一次拜访 API 需在 header 中携带 jwt后端先解析 jwt,拿到 userId 后,数据库查问此用户的权限列表(用户-角色-权限)拿到用户的权限列表后,和以后接口所需的权限进行匹配,匹配胜利返回数据,失败返回 401jwt 规范先来查查规范是怎么样的,首先参考了jwt.io上面对应用场景的阐明: Authorization 受权Information Exchange 信息替换对于下面的信息,我集体的了解是两个方面: 容许用户拜访路由、服务和资源,在我这里就是接口所需的权限,也能够 SSO 登录,我这里目前不须要能够确定以后用户的身份,在我这里就是 userId 了优化当初的用法有以下缺点: 每次调用接口都须要进行数据库查问权限(用户-角色-权限),浪费资源登录胜利 8 小时后,即便用户始终在不停的应用零碎,但 jwt 还是会生效,须要从新登录第一点好说,把权限列表也放进 payload,解析结束间接和接口所需权限进行比照。 第二点,把有效期缩短到一个星期,一个月?然而依然会产生正在用着用着 jwt 就生效了,须要从新登录的状况,工夫设置的太长也不平安,因为 jwt 自身就是无状态的,而且权限变更了怎么办,难道要等很久才失效吗,这么看来必须要刷新 jwt 了。 对应的优化点就来了: 把权限列表放进 payload,不必每次都去数据库查问让用户无感的刷新 jwt刷新 jwt 计划这里参考了 stackoverflow 下面的探讨: jwt-refresh-token-flowJWT (JSON Web Token) automatic prolongation of expiration而后我确定了我的刷新流程: 登录胜利后颁发两个 token:accessToken 有效期 1 小时,refreshToken 有效期 1 天accessToken 生效后返回 401,前端通过 refreshToken 获取新的 accessToken 和新的 refreshTokenrefreshToken 生效后返回 403,须要从新登录也就是说,登录胜利后,在 refreshToken 有效期内,都能够持续操作,并且顺延有效期,再也不会呈现用着用着忽然须要从新登录的状况了。这俩有效期能够自行调整,我这里思考的是 accessToken 最好不能太长,不然调整权限后失效期太短。 ...

August 4, 2022 · 2 min · jiezi

关于jwt:实战模拟│JWT-登录认证

Token 认证流程作为目前最风行的跨域认证解决方案,JWT(JSON Web Token) 深受开发者的青睐,次要流程如下:客户端发送账号和明码申请登录服务端收到申请,验证账号密码是否通过验证胜利后,服务端会生成惟一的 token,并将其返回给客户端客户端承受到 token,将其存储在 cookie 或者 localStroge 中之后每一次客户端向服务端发送申请,都会通过 cookie 或者header 携带该 token服务端验证 token 的有效性,通过才返回响应的数据 Token 认证长处反对跨域拜访:Cookie 是不容许跨域拜访的,这一点对 Token 机制是不存在的,前提是传输的用户认证信息通过 HTTP 头传输无状态: Token 机制在服务端不须要存储 session 信息,因为 Token 本身蕴含了所有登录用户的信息,只须要在客户端的 cookie 或本地介质存储状态信息适用性更广: 只有是反对 http 协定的客户端,就能够应用 token 认证。无需思考CSRF: 因为不再依赖 cookie,所以采纳 token 认证形式不会产生 CSRF,所以也就无需思考 CSRF 的进攻 JWT 构造一个 JWT 实际上就是一个字符串,它由三局部组成:头部、载荷与签名。两头用点 . 分隔成三个局部。留神 JWT 外部是没有换行的。 头部 / headerheader 由两局部组成: token 的类型 JWT 和算法名称:HMAC、SHA256、RSA{ "alg": "HS256", "typ": "JWT"} 载荷 / PayloadPayload 局部也是一个 JSON 对象,用来寄存理论须要传递的数据。JWT 指定七个默认字段供选择。除了默认字段之外,你齐全能够增加本人想要的任何字段,个别用户登录胜利后,就将用户信息寄存在这里iss:发行人exp:到期工夫sub:主题aud:用户nbf:在此之前不可用iat:公布工夫jti:JWT ID用于标识该JWT{ "iss": "xxxxxxx", "sub": "xxxxxxx", "aud": "xxxxxxx", "user": [ 'username': '极客飞兔', 'gender': 1, 'nickname': '飞兔小哥' ] } 签名 / Signature签名局部是对下面的 头部、载荷 两局部数据进行的数据签名为了保证数据不被篡改,则须要指定一个密钥,而这个密钥个别只有你晓得,并且寄存在服务端生成签名的代码个别如下:// 其中secret 是密钥String signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) JWT 根本应用客户端收到服务器返回的 JWT,能够贮存在 Cookie 外面, 也能够贮存在 localStorage而后 客户端每次与服务器通信,都要带上这个 JWT把 JWT 保留在 Cookie 外面发送申请,这样不能跨域更好的做法是放在 HTTP 申请的头信息 Authorization 字段外面fetch('license/login', { headers: { 'Authorization': 'X-TOKEN' + token }}) 实战:应用 JWT 登录认证这里应用 ThinkPHP6 整合 JWT 登录认证进行实战模仿 装置 JWT 扩大composer require firebase/php-jwt 封装生成 JWT 和解密办法<?php/** * Desc: JWT认证 * Author: autofelix * Time: 2022/07/04 */namespace app\services;use app\Helper;use Firebase\JWT\JWT;use Firebase\JWT\Key;class JwtService{ protected $salt; public function __construct() { //从配置信息这种或取惟一字符串,你能够轻易写比方md5('token') $this->salt = config('jwt.salt') || "autofelix"; } // jwt生成 public function generateToken($user) { $data = array( "iss" => 'autofelix', //签发者 能够为空 "aud" => 'autofelix', //面象的用户,能够为空 "iat" => Helper::getTimestamp(), //签发工夫 "nbf" => Helper::getTimestamp(), //立马失效 "exp" => Helper::getTimestamp() + 7200, //token 过期工夫 两小时 "user" => [ // 记录用户信息 'id' => $user->id, 'username' => $user->username, 'truename' => $user->truename, 'phone' => $user->phone, 'email' => $user->email, 'role_id' => $user->role_id ] ); $jwt = JWT::encode($data, md5($this->salt), 'HS256'); return $jwt; } // jwt解密 public function chekToken($token) { JWT::$leeway = 60; //以后工夫减去60,把工夫留点余地 $decoded = JWT::decode($token, new Key(md5($this->salt), 'HS256')); return $decoded; }} 用户登录后,生成 JWT 标识<?phpdeclare (strict_types=1);namespace app\controller;use think\Request;use app\ResponseCode;use app\Helper;use app\model\User as UserModel;use app\services\JwtService;class License{ public function login(Request $request) { $data = $request->only(['username', 'password', 'code']); // ....进行验证的相干逻辑... $user = UserModel::where('username', $data['username'])->find(); // 验证通过生成 JWT, 返回给前端保留 $token = (new JwtService())->generateToken($user); return json([ 'code' => ResponseCode::SUCCESS, 'message' => '登录胜利', 'data' => [ 'token' => $token ] ]); }} 中间件验证用户是否登录在 middleware.php 注册中间件<?php// 全局中间件定义文件return [ // ...其余中间件 // JWT验证 \app\middleware\Auth::class];注册中间件后,在权限验证中间件中欠缺验证逻辑<?phpdeclare (strict_types=1);namespace app\middleware;use app\ResponseCode;use app\services\JwtService;class Auth{ private $router_white_list = ['login']; public function handle($request, \Closure $next) { if (!in_array($request->pathinfo(), $this->router_white_list)) { $token = $request->header('token'); try { // jwt 验证 $jwt = (new JwtService())->chekToken($token); } catch (\Throwable $e) { return json([ 'code' => ResponseCode::ERROR, 'msg' => 'Token验证失败' ]); } $request->user = $jwt->user; } return $next($request); }}

July 4, 2022 · 2 min · jiezi

关于jwt:jwt生成token和token解析基础

1.jwt构造jwt生成到客户端(浏览器)的token蕴含"."离开的三个局部: header(Base64Url编码过的)payload(Base64Url编码过的)signature形如:xxxxx.yyyyy.zzzzz 1.1 例子:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiYW5keSIsImV4cCI6MTY1NTg5NzEwMCwiYWdlIjozMH0.32hfc-oBxGg2Lgk3QR48HCbadsbOfCUxexw9aiQ_FQk拆为3局部: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.(header)eyJuYW1lIjoiYW5keSIsImV4cCI6MTY1NTg5NzEwMCwiYWdlIjozMH0.(payload)32hfc-oBxGg2Lgk3QR48HCbadsbOfCUxexw9aiQ_FQk(signature)2.header+payload+signature介绍2.1 header下面的header局部:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9base64Url解码后: { "typ": "JWT", "alg": "HS256"}通常阐明token的类型、生成token所应用的的算法 The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.2.2 Payload下面的Payload局部:eyJuYW1lIjoiYW5keSIsImV4cCI6MTY1NTg5NzEwMCwiYWdlIjozMH0base64Url解码后: { "name": "andy", "exp": 1655897100, "age": 30}通常是要客户端申请时带货的内容(比方用户名,比方是否是管理员等,server端生成的时候能够定义内容,模式如map) The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data. There are three types of claims: registered, public, and private claims.2.3 Signature下面的Signature局部:32hfc-oBxGg2Lgk3QR48HCbadsbOfCUxexw9aiQ_FQk它是用来验签的, 验证是否被客户端批改过,它的生成逻辑如下:就是应用header局部的base64Url、payload局部的base64Url局部、小圆点、以及你的私钥明码,应用指定的算法生成的;因为有明码, 所以是平安的,这也是明码要爱护好的起因。 ...

June 22, 2022 · 1 min · jiezi

关于jwt:一文带你搞懂-JWT-常见概念-优缺陷

一文带你搞懂 JWT 常见概念 & 优缺点JWT 的劣势相比于 Session 认证的形式来说,使用 JWT 进行身份认证次要有上面 4 个劣势。 无状态JWT 自身蕴含了身份考据所需要的所有信息,因此,咱们的服务器不需要存储 Session 信息。这显然减少了零碎的可用性和伸缩性,大大加重了服务端的压力。不过,也正是因为 JWT 的无状态,也导致了它最大的缺点:不可控!就比方说,咱们想要在 JWT 有效期内废除一个 JWT 或者更改它的权限的话,并不会立即失效,通常需要等到有效期过后才可能。再比方说,当用户 Logout 的话,JWT 也还无效。除非,咱们在后端减少额定的处理逻辑比如将生效的 JWT 存储起来,后端先考据 JWT 是否无效再进行处理。具体的解决办法,咱们会在前面的内容中粗疏介绍到,这里只是简略提一下。 无效避免了 CSRF 攻打CSRF(Cross Site Request Forgery) 一般被翻译为 跨站请求伪造,属于网络攻击领域范畴。相比于 SQL 脚本注入、XSS 等安全攻击方式,CSRF 的知名度并没有它们高。然而,它的确是咱们开发零碎时必须要考虑的安全隐患。就连业内技术标杆 Google 的产品 Gmail 也曾在 2007 年的时候爆出过 CSRF 漏洞,这给 Gmail 的用户造成了很大的损失。 那么究竟什么是跨站请求伪造呢? 简略来说就是用你的身份去做一些不好的事件(发送一些对你不敌对的请求比如恶意转账)。举个简略的例子:小壮登录了某网上银行,他来到了网上银行的帖子区,看到一个帖子上面有一个链接写着“迷信理财,年盈利率过万”,小壮好奇的点开了这个链接,后果发现自己的账户少了 10000 元。这是这么回事呢?原来黑客在链接中藏了一个请求,这个请求间接利用小壮的身份给银行发送了一个转账请求,也就是通过你的 Cookie 向银行收回请求。 CSRF 攻打需要依赖 Cookie ,Session 认证中 Cookie 中的 SessionID 是由阅读器发送到服务端的,只需收回请求,Cookie 就会被携带。借助这个个性,即使黑客无奈获取你的 SessionID,只需让你正点攻打链接,就可能达到攻打成果。另外,并不是必须点击链接才可能达到攻打成果,很多时候,只需你打开了某个页面,CSRF 攻打就会发生。 ...

June 21, 2022 · 2 min · jiezi

关于jwt:虾皮二面什么是-JWT-如何基于-JWT-进行身份验证

分享一下群友面试虾皮遇到的对于 JWT 的面试真题。 相干面试题如下: 什么是 JWT?为什么要用 JWT?JWT 由哪些局部组成?如何基于 JWT 进行身份验证?JWT 如何避免 Token 被篡改?如何增强 JWT 的安全性?如何让 Token 生效?......什么是 JWT?JWT (JSON Web Token) 是目前最风行的跨域认证解决方案,是一种基于 Token 的认证受权机制。从 JWT 的全称能够看出,JWT 自身也是 Token,一种规范化之后的 JSON 构造的 Token。 Token 本身蕴含了身份验证所须要的所有信息,因而,咱们的服务器不须要存储 Session 信息。这显然减少了零碎的可用性和伸缩性,大大加重了服务端的压力。 能够看出,JWT 更合乎设计 RESTful API 时的「Stateless(无状态)」准则 。 并且, 应用 Token 认证能够无效防止 CSRF 攻打,因为 Token 个别是存在在 localStorage 中,应用 JWT 进行身份验证的过程中是不会波及到 Cookie 的。 我在 JWT 优缺点剖析[1]这篇文章中有具体介绍到应用 JWT 做身份认证的劣势和劣势。 上面是 RFC 7519[2] 对 JWT 做的较为正式的定义。 JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted. ——JSON Web Token (JWT)[3]JWT 由哪些局部组成?JWT 实质上就是一组字串,通过(.)切分成三个为 Base64 编码的局部: ...

May 30, 2022 · 2 min · jiezi

关于jwt:如何使用jwt-完成注销退出登录功能

原文 神奇的 JSON Web Tokens(JWT)JSON Web Tokens (JWT) 是一种无状态解决用户身份验证的办法。 什么意思? JWT帮忙建设认证机制而不将身份验证状态存储在任何存储中,无论是会话内存还是数据库,因而, 当检查用户的身份验证状态时,不须要拜访会话内存或执行数据库查问。相同, 依据你抉择的用户payload生成token 并在客户端的申请中应用它来标识服务器上的用户 因而,基本上,每当创立token时,就能够永远应用它,或者直到它过期为止。 JWT生成器能够在生成的时候有一个指定过期工夫的选项。 不过你想让曾经生成的token生效该怎么办呢?当用户登记或者更改明码的时候你该做些什么呢 登记通常在应用JWT做身份验证时客户端将token贮存在某个中央,并将附加到每个须要身份验证的申请上,所以登记的第一件事就是删除贮存在客户端上的token(例如 浏览器本地贮存)在这种状况下客户端没有了token申请须要认证的接口天然会失去未认证的响应。不过这就够了吗?这个是防小人不防君子的做法实际上在登记之前通过一些伎俩将token拿到手在登记后仍然能够应用!不信的的话能够本人尝试下。让咱们从后盾登记token,你可能会说桥豆麻袋这对于jwt来说并不是那么简略,你并不能像删除cookie和session那样来删除token。 实际上,JWT的目标与session不同,不可能强制删除或生效曾经生成的token。 Token 过期是的 Token 能够设置过期。在生成token时能够指定过期工夫,你能够在无效的paylaod中加上exp字段就像这样: { "sub": "1234567890", "name": "John Doe", "iat": 1516234022, "exp": 1516239022}exp 字段为工夫戳,这里的iat字段代表公布工夫,此Token设置为在公布后5秒过期⏰。如果你不想领有永远无效的Token你应该给你的JWT设置一个正当的到期工夫,工夫的长短取决于你的利用,咱们将在这里应用时长为一天Token,并在登录操作中生成它们,对于NodeJS应用程序,代码应该如下所示: const jwt = require('jsonwebtoken');const payload = { "sub": "1234567890", "name": "John Doe", "iat": 1516234022}const token = jwt.sign(payload, 'your-secret', {expiresIn: '1d'})当这个token过期时验证器会返回一个error,而且后端一旦收到须要受权的申请,就会以未受权的响应状态进行响应。通常,你将从客户端删除令牌并将用户重定向到登录页面。因而,在这个例子中,所有用户在应用你的应用程序1天后将主动登记。 很酷,但我还是想登记!如前所述,你不能在Token创立后手动使其过期,你实际上不能像应用session那样在服务器端应用JWT登记或者,除非,你能够...应用JWT应该是无状态的,这意味着你应该将所需的所有存储在payload中,并跳过对每个申请执行DB查问,然而如果你打算有一个严格的登记性能,无奈期待Token主动过期,即便你曾经从客户端革除了Token,而后你可能须要违反无状态规定并执行一些查问。 有一种实现可能是,存储一个所谓的“黑名单”所有Token是无效的,还没有到期你能够筛选一个领有TTL性能的数据库,TTL被用于为Token记录Token过期之前残余的工夫量。Redis 是一个很好的抉择,这将容许在内存中快速访问列表,而后,在某个中间件中,运行在每个受权申请上,你应该查看提供的令牌是否在黑名单中️,如果在的话就抛出一个未认证异样,如果没有让它通过,JWT验证将解决它并确定它是否已过期或依然无效。 论断仿佛,在应用JSON Web令牌时创立洁净的登记流程并不那么简略,你应该让Token放弃活动状态,直到它本人过期为止;或者,如果你心愿在用户登记时限度Token的应用,则抉择贮存一个Token黑名单。总而言之,只需遵循以下4个要点: 设置令牌的正当过期工夫登记时从客户端删除存储的Token领有不再流动Token的数据库,这些Token仍有一些生存工夫针对每个申请依据黑名单查问受权状况

January 3, 2022 · 1 min · jiezi

关于jwt:KubeCube-用户管理与身份认证

前言KubeCube (https://kubecube.io) 是由网易数帆近期开源的一个轻量化的企业级容器平台,为企业提供 kubernetes 资源可视化治理以及对立的多集群多租户治理性能。KubeCube 社区将通过系列技术文章解读 KubeCube 的设计特点和技术实现,帮忙开发者和用户更快地了解和上手 KubeCube。本文是第三篇,重点介绍 KubeCube 中用户治理与身份认证的实现计划。 用户治理 所有 Kubernetes 集群都有两类用户:由 Kubernetes 治理的服务账号和普通用户。Kubernetes 假设普通用户是由一个与集群无关的服务通过以下形式之一进行治理的:* 负责散发私钥的管理员* 相似 Keystone 或者 Google Accounts 这类用户数据库* 蕴含用户名和明码列表的文件有鉴于此,Kubernetes 并不蕴含用来代表普通用户账号的对象。 普通用户的信息无奈通过 API 调用增加到集群中。依据 Kubernetes 官网所述,Kubernetes 自身并不间接提供用户治理的个性,不反对普通用户对象,更不存储普通用户的任何信息。如果须要创立一个用户,须要为该用户创立私钥和证书,通过证书进行身份认证。并且,因为不存储用户信息,集群管理员无奈集中管理用户,对其余用户无感知。因而,KubeCube 首先从新定义了用户这一概念,即提供了 User 这一资源类型,存储用户信息,进行用户治理,同时不便后续的身份认证和权限校验等。 apiVersion: user.kubecube.io/v1kind: Usermetadata: name: 登录账号,用户惟一标识,用户自定义,不可反复,不可批改spec: password: 明码,必填,零碎会将明码进行md5加盐加密后保留 displayName: 用户名 email: 邮箱 phone: 电话 language: 语言:en/ch loginType: 用户登录形式:normal/ldap/github/... state: 用户状态:normal/forbiddenstatus: lastLoginTime: 上次登录工夫 lastLoginIp: 上次登录IP用户能够由管理员在前端页面手动创立,也能够在应用内部认证第一次登录时零碎主动创立。因而,用户在注册形式上能够分为零碎一般注册用户和第三方受权登录用户。但对于这两种创立形式,都是对应在管控集群创立相应的 User cr。而后 Warden 的资源同步管理器会将该 cr 从管控集群同步到计算集群,以便于后续多集群对立认证。 这样,在用户治理页面,只须要查问管控集群内的 User 资源,即可实现用户的集中管理。并且,能够轻松地增加用户、查问用户以及对用户元信息的批改。 身份认证在 KubeCube 中,反对本地认证和内部认证。本地认证是指,在 KubeCube 中创立普通用户,用户再应用其创立时注册的用户名明码进行登录和认证。而内部认证,是指无需创立用户,通过第三方的认证平台认证用户身份,从而拜访 KubeCube。上面将别离介绍这两种认证形式的实现。 ...

December 17, 2021 · 3 min · jiezi

关于jwt:JWT认证与golang实现JWT的demo

JWT=JSON Web Token,是目前较为风行的分布式认证计划。 认证计划个别罕用传统的Cookie/Session认证计划,认证过程: 用户向服务器发送username+password;服务器验证当前,存储该用户的登录信息,如userId/role/loginTime等;服务器向用户返回 session_id,写入用户的cookie;用户随后的每一次申请,都通过Cookie,将session_id传回服务器;服务器收到session_id,找到后期保留的数据,由此失去用户的身份(userId/role等);在分布式系统中,常遇到跨域认证的问题,比方A网站和B网站 是同一家公司的关联服务器。现要求,用户只有在其中一个网站登录,再拜访另一个网站就会主动登录,对该问题,罕用的计划有: 传统的Cookie/Session计划:将session集中长久化并做cache,每来一个申请,都要用申请中的sessionId进行认证;JWT计划:服务端不存储session数据,认证信息存储在客户端,服务端仅要session的颁发和校验;JWT(JSON Web Token)是目前较为风行的跨域认证解决方案。 JWT的认证过程JWT的认证过程是,客户端将用户名和明码传入服务端,服务端通过认证后,将生成一个JSON对象,发回给用户,JSON对象大略的格局: { "姓名": "张三", "角色": "管理员", "到期工夫": "2018年7月1日0点0分"}当前客户端再与服务端通信的时候,都要带上这个JSON对象,服务端校验JSON对象的内容认证用户。 这样服务端不必保留任何session信息,服务端变成无状态的,扩展性较好。 JWT的数据结构: Header:形容JWT的元数据,如签名算法;Payload:寄存理论传递的数据,除了官网定义的签发人、过期工夫等四段,还能够寄存公有字段,如userId/userName等信息;Signature:对前两局部的签名,避免数据篡改; JWT的长处:服务端便于扩大,因为服务端不存储认证信息,无状态的,十分利于扩大。 JWT的毛病:JWT一旦签发,在到期之前始终无效,除非服务端部署额定的逻辑。 golang-jwt的demohttp-server应用github.com/gin-gonic/gin。jwt应用github.com/dgrijalva/jwt-go。 该demo中,client通过POST /login进行登录,而后获取JWT token,而后通过在header中带上token,POST /order进行商品下单。 生成jwt tokenJWT token在用户/login的时候,由服务端调配: type AuthClaim struct { UserId uint64 `json:"userId"` jwt.StandardClaims}var secret = []byte("It is my secret")const TokenExpireDuration = 2 * time.Hour// 生成JWT tokenfunc GenToken(userId uint64) (string, error) { c := AuthClaim{ UserId: userId, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(TokenExpireDuration).Unix(), Issuer: "TEST001", }, } //应用指定的签名办法创立签名对象 token := jwt.NewWithClaims(jwt.SigningMethodHS256, c) // 应用指定的secret签名并取得残缺的编码后的字符串token return token.SignedString(secret)}校验jwt tokenlogin胜利后,用户再申请其它的request时,带上该JWT token,交由服务端的middleware认证,认证OK后,才会失去解决: ...

November 6, 2021 · 1 min · jiezi

关于jwt:JWT登录鉴权避免在用户操作的过程中JWT到期跳转登录

以后我的项目JWT登录鉴权办法及问题以后登陆鉴权办法用户登录时,返回一个JWT,由前端存储在本地。尔后用户每次向须要权限的API发出请求时带上该JWT用于验证身份。后端收到JWT后,验证JWT是否正确且未过期: 如果正确且未过期,则返回资源。如果不正确,则返回不正确的状态码;如果已过期,则返回已过期的状态码,要求从新登录。问题应用JWT实现登录鉴权时,因为JWT有固定的过期工夫,可能在用户正在进行操作的过程中恰好过期,而后忽然跳转到登录界面,造成较差的用户体验。 指标应用JWT实现登录鉴权,当且仅当用户在一段时间内不对系统进行任何操作时才要求用户从新登录,并且尽可能保障JWT的安全性。 解决办法及优缺点剖析解决办法一:只应用一个JWT token(不举荐)具体实现用户登录时,返回一个JWT,由前端存储在本地。尔后用户每次向须要权限的API发出请求时带上该JWT用于验证身份。后端收到JWT后,验证该JWT是否正确: 如果不正确,则间接返回错误代码;如果正确,则验证该JWT是否过期: 如果没有过期,则间接返回资源;如果过期,则计算过期工夫: 如果过期工夫没有超过某阈值,则返回资源和新的JWT;如果过期工夫超过某阈值,则返回错误代码。要求从新登录。长处实现了当且仅当用户在一段时间内不对系统进行任何操作时才要求用户从新登录。当JWT过期时如果用户还在操作系统,会向后端发送刚刚过期的JWT,此时JWT的过期工夫没有超过阈值,间接返回资源和新的JWT,实现用户无感书最新。然而如果当JWT过期之后的一段时间(阈值内)用户都没有操作系统,当用户再次操作系统时,会向后端发送过期工夫超过阈值的JWT,后端会返回错误代码,实现从新登录。 毛病安全性较低。如果频繁发送申请,能够应用一个JWT实现永恒登录。一旦JWT被窃取,攻击者能够应用失去的JWT永恒伪造用户获取信息。 解决方案二:应用两个JWT token(举荐)具体实现用户登录时,返回两个JWT(access token和refresh token),一个用于在申请资源验证身份的access token,一个用于在access token过期时更新access token的refresh token。其中refresh token的有效期较长(如7天),而access token的有效期较短(如1小时)。由前端存储在本地。尔后用户每次向须要权限的API发出请求时带上access token用于验证身份。后端收到access token后:验证该access token是否正确: 如果不正确,则间接返回错误代码;如果正确,则验证该access token是否过期: 如果没过期,则间接返回资源;如果过期,则返回过期的错误代码。前端收到过期错误代码后应用refresh token向更新接口申请新的access token,更新接口判断refresh token是否过期: 如果没有过期,则返回新的access token和新的refresh token,将之前所有由同一refresh token生成的refresh token都置为有效(保护一个有效列表)。前端应用新的access token从新向须要权限的API发出请求,获取资源。如果过期,则返回错误代码,要求从新登录。如果收到已被置为有效的refresh token,则将以后所有与有效refresh token为同一用户的refresh token和access token都置为有效(具体来说是将该用户的拜访置为有效),直到用户从新登录。应用这种办法,既能防止在用户操作的过程中呈现忽然跳转登录(次要解决了两次操作之间的不统一景象,不会呈现一次申请还能失常获取资源,下一次申请资源就忽然跳转到登录页面的景象),又能保障较高的安全性。 解释Q:为什么不应用永恒无效的refresh token?A:为了实现更高的安全性。永恒无效的refresh token一旦被窃取,攻击者能够应用该token永恒假冒用户获取个人信息。而每次应用refresh token就更换一个新的refresh token的话,即便refresh token被窃取,也会在肯定工夫内生效,攻击者应用的工夫无限。 Q:为什么refresh token获取新的access token时须要同时更新refresh token?A:因为如果不更新refresh token的话,就无奈防止用户两次操作之间的不一致性。可能用户在refresh token过期前一分钟还能失常操作,一分钟后就忽然跳转到登录页面,造成不敌对的用户体验。 Q:为什么要在应用refresh token取得新的access token和refresh token后将用于申请新token的refresh token置为有效?A:为了防止之前的refresh token被窃取后导致的重放攻打(应用之前的refresh token申请新的access token和refresh token)。 Q:为什么更新接口在收到已置为有效的refresh token后会将所有应用第一个refresh token失去的refresh token都禁止?A:因为不晓得被窃取的是哪个refresh token。有可能攻击者应用第一个refresh token,并应用该token在更新接口获取了新的refresh token,此时用户可能应用旧的refresh token(已被置为有效)申请更新接口。也有可能攻击者窃取到第一个refresh token后,用户首先应用该token在更新接口获取了新的refresh token,此时攻击者可能应用旧的refresh token(已置为有效)申请更新接口。详见:auth0-refresh-token-rotation ...

October 2, 2021 · 1 min · jiezi

关于jwt:智汀家庭云开发指南Golang用户模块

对智汀家庭云即smart-assistant(以下简称SA)的用户,角色,权限的阐明。 1.用户(1)SA的管理员 当某用户应用智汀App为某个家庭/公司增加SA之后,该用户就是SA的管理员,创建者,领有SA所有的权限,包含邀请其余用户退出该家庭,为成员调配角色等。 (2)smart-assistant-token smart-assistant-token 是家庭成员应用SA性能的用户凭证。每个用户退出一个绑定了SA的家庭时,SA会给该用户下发凭证。一个SA的用户凭证 只容许在该SA下应用。smart-assistant-token只能通过增加SA或者退出其余增加了SA的家庭获取。 smart-assistant-token的生成应用了securecookie.CodecsFromPairs(),securecookie.EncodeMulti()办法。 (3)设置账号密码 智汀App默认应用smart-assistant-token进行用户鉴权。如果您领有智汀专业版,您能够通过设置账号密码后,应用账号密码登录智汀专业版以 体验SA更多的性能。 (4)邀请其他人退出您的家庭/公司 领有生成邀请码权限的用户能够通过生成邀请二维码,供别人扫描以退出您的家庭。生成二维码时须要您抉择相干的角色信息,角色信息示意扫码的用户以什么 角色退出该家庭。每次生成的二维码有效期为十分钟,您能够邀请任何您信赖的人退出您的SA。 每个用户能够屡次扫描二维码退出您的SA,用户在该家庭的角 色以最初一次扫描的二维码为主。二维码的无效信息通过jwt生成,想理解jwt的详细信息能够浏览https://jwt.io/introduction。 (5)将用户踢出您的家庭/公司 您能够应用SA的删除成员性能,将用户踢出您的家庭/公司。留神: SA创建者不容许被删除。 (6)注意事项 每一个角色都领有不同的权限,多个角色间接之间的权限是取并集的。如果您不想您的SA数据受到损失,在生成您的二维码时,审慎抉择角色信息。SA创建者不容许被删除2.角色(1)默认角色 当您增加了SA后,SA会默认创立管理员和成员两个角色,同时将管理员的角色赋予您。不同的角色有不同的权限管制项,用户角色领有的权限越高,能够使 用SA的性能也就越多。 (2)角色的创立、批改、删除 角色的创立、批改、删除只能通过智汀专业版进行操作。角色的创立包含角色名称,角色所领有的权限。角色的批改包含批改角色名称、减少或缩小该角色领有的权限。角色的删除意味着您会删除该角色以及该角色领有的权限,同时领有该角色的用户也会失去该角色。这可能会导致某些用户无奈应用SA的性能。(3)为用户设置角色 SA的创建者和扫码退出SA的用户都领有本人的角色。如果用户有批改成员角色的权限,也能够通过智汀专业版或智汀App设置某一个用户的角色,即减少或删除 这意味着该用户的权限会变高或变低,取决于用户自身领有的角色。 留神: SA创建者的角色不可更改,创建者有且只有一个管理员角色。如果您是SA创建者,请防止您扫描该SA的二维码,这会导致您失去管理员的角色。 (4)多角色 SA容许一个用户领有多个不同的角色。这意味着您在生成邀请二维码或者设置用户角色时能够抉择多个角色。一个SA用户总的权限由该用户的所有角色领有的 权限决定。 (5)注意事项 管理员角色的权限是不可批改的SA创建者的角色不可更改,创建者有且只有一个管理员角色。如果您是SA创建者,请防止您扫描该SA的二维码,这会导致您失去管理员的角色。3.权限(1)阐明 权限是SA用于判断用户是否有对设施、家庭/公司、房间/区域、场景、角色等性能操作的根据。用户具备的权限由其被调配的角色取得。每一个性能都有不同 的权限,但相似批改删除等权限是所有性能都具备的。 与角色相干的权限查看角色列表,容许用户查看该SA领有的角色新增角色,容许用户创立新的角色,并给该角色赋予权限编辑角色,容许用户编辑角色名称和更改角色领有的权限删除角色,容许用户删除角色与场景相干的权限新增场景,容许用户创立场景删除场景,容许用户删除场景批改和删除场景,容许用户删除场景或者批改场景的设置管制场景,容许用户在场景的执行工作中抉择管制场景与房间/区域相干的权限增加房间/区域调整程序批改房间名称查看房间详情删除房间与家庭/公司相干的权限生成邀请二维码,容许用户邀请其他人进入该家庭批改名称批改成员角色,新增或缩小某一成员领有的角色删除成员,只管您领有删除成员的权限,然而您依然不能删除SA创建者这一成员与设施相干的权限增加设施删除设施,SA一旦被增加,任何人都不会领有删除SA的权限。批改设施,批改设施的权限包含批改设施的地位,名称等。管制设施,不同的设施有不同的操作权限。eg:灯有开关,色温,色差灯管制权限,开关只有开关权限。(2)注意事项 场景的批改和管制不仅仅取决于用户是否领有批改和管制场景的权限,还包含该用户是否有对场景中的设施操作项的管制权限。 eg:如果您领有管制场景A的权限,然而您没有场景A外面设施B的开关管制权限,则您同样没有管制该场景A的权限。批改场景也是如此。一旦您在编辑角色页面抉择了批改、删除、管制设施这些权限项,SA会默认将该家庭下的所有设施的所有权限项都赋予这个角色。 eg: 您抉择了删除设施管制项,则该角色领有删除该SA所有设施的权限。咱们建议您通过编辑角色的高级设置选项来抉择您要赋予该角色的设施权限。

September 30, 2021 · 1 min · jiezi

关于jwt:JWT-Token在线编码生成

JWT Token在线编码生成JWT Token在线编码生成 JWT 的三个局部顺次如下Header(头部)Payload(负载)Signature(签名)Header头部{ "alg": "HS256", "typ": "JWT"}Payload(负载)iss (issuer):签发人exp (expiration time):过期工夫sub (subject):主题aud (audience):受众nbf (Not Before):失效工夫iat (Issued At):签发工夫jti (JWT ID):编号Signature(签名)生成规定,secret是秘钥,要严格窃密。HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) https://tooltt.com/jwt-encode/

May 11, 2021 · 1 min · jiezi

关于jwt:在线解析解码jwt-token工具

在线解析解码jwt token工具在线解析解码jwt token工具 JSON Web Token(缩写 JWT)是目前最风行的跨域认证解决方案。本工具提供在线解码的性能 https://tooltt.com/jwt-decode/

May 10, 2021 · 1 min · jiezi

关于jwt:JSON-Web-Tokens-是如何工作的

在用户权限校验的过程中,一个用户如果应用受权信息胜利登录后,一个 JSON Web Token 将会返回给用户端。 因为返回的令牌蕴含有受权信息,应用程序应小心保留这些受权信息,以防止不必要的平安问题。你的应用程序在不须要受权信息的时候,应用程序不应该保留受权胜利后返回的令牌。 应用程序也不应该将这些敏感信息保留在浏览器中,因为这样会更加容易导致信息透露,请参考链接:https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html#local-storage 中的内容。 在任何时候,如果用户心愿拜访一个受爱护的资源或者路由的时候,用户应该在拜访申请中蕴含 JWT 令牌。通常这个令牌是存储在 HTTP 申请的头部信息,个别会应用 Authorization 字段,应用 Bearer 模式。 Http 头部发送给后盾所蕴含的内容看起来如下所示: Authorization: Bearer <token> 在某些状况下,能够应用无状态的受权机制。服务器上受爱护的路由将会查看随着拜访提交的 JWT 令牌。如果令牌是无效的,用户将会被容许拜访特定的资源。 如果 JWT 令牌中蕴含有必要的信息,服务器的服务端将不须要再次对数据库进行查问以放慢访问速度。当然,不是所有的时候都能够这样进行解决。 当令牌随着头部中的 Authorization 信息一起发送,那么咱们不须要应用 cookies,因而跨域拜访(Cross-Origin Resource Sharing (CORS))也不应该成为一个问题。 上面的示例图展现了JWT 是如何被取得的,同时也展现了 JWT 是如何被应用来拜访服务器 API 的。 应用程序或者客户端,通过对受权服务器的拜访来取得受权。这个可能有不同的受权模式。例如,通常咱们能够应用 OpenID Connect 提供的规范的受权地址来进行受权,请参考链接:http://openid.net/connect/。通常来说一个规范的受权地址为 /oauth/authorize,并且应用上面相似的规范受权流程,请参考链接:http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth 中的内容。当受权实现后,受权服务器将会返回拜访令牌(access token)给利用。利用应用取得的令牌来拜访收到爱护的资源(例如 API)等。须要留神的是,通过应用了签名的令牌,只管用户可能没有方法对应用的令牌进行批改,然而令牌中蕴含的所有信息将会裸露给用户或者其余的利用。因而,你不应该在你的令牌中存储密钥或者任何的敏感信息。 https://www.ossez.com/t/json-web-tokens/532

October 2, 2020 · 1 min · jiezi

关于jwt:JSON-Web-Token-的结构是什么

JSON Web Tokens 由应用 (.) 离开的 3 个局部组成的,这 3 个局部别离是: 头部(Header)负载(Payload)签名(Signature)正是因为下面的组织模式,因而一个 JWT 通常看起如上面的表现形式。 xxxxx.yyyyy.zzzzz 让咱们针对下面的模式来具体的剖析下。 头部(Header)在头部的数据中 _通常_ 蕴含有 2 局部的内容:token 的类型,这里应用的是字符 JWT,和应用的的签名加密算法,例如 SHA256 或者 RSA。 例如上面的格局: {"alg": "HS256","typ": "JWT"} 而后,将下面的 JSON 数据格式应用 Base64Url 算法进行哈希,这样你就失去了 JWT 的第一局部。 负载(Payload)JWT 的第二局部为负载,在负载中是由一些 claims 组成的。 Claims 是一些实体(通常指用户)和其余的一一些信息。 有上面 3 种类型的 claims _registered_, _public_ 和 _private_ 。 Registered claims:这些 claims 是事后定义的,这些配置的内容不是必须的然而是举荐应用的,因而提供了一系列约定俗成应用的。比方:iss(issuer), exp(expiration time), sub(subject),aud(audience)和其余的一些更多的配置。 请留神,这些约定俗称的配置只有 3 个字符,以便于压缩数据量。 Public claims:这些数据能够由应用 JWT 的用户自在去定义,然而为了防止抵触,你须要参考在 IANA JSON Web Token Registry 中对它们进行定义,或者将这些内容定义为 URI,并且须要防止可能呈现的抵触。 Private claims:这些内容是自定义的内容,这部分的内容被用于在数据传输短间进行转换的数据。这些数据是没有在 registered 和 public 两头没有定义的内容。 ...

October 2, 2020 · 1 min · jiezi

关于jwt:什么是-JSON-Web-TokenJWT

无关本文档的疾速链接,请参考页面提醒。 链接名称 URL 内容阐明 GitHub MD 源代文件 https://github.com/cwiki-us-docs/cwikius-docs/blob/master/jwt/README.md 将本页面中的内容转换为 MD 文件的手册,并存于 Github 下面 Docsify 转换后的手册 https://cwiki-us-docs.github.io/cwikius-docs/#/jwt/README 将 MD 文件应用 Docsify 转换后的手册链接地址 问题探讨和社区 https://www.ossez.com/tag/jwt 请拜访应用 JWT 标签 CWIKI.US 页面链接 https://www.cwiki.us/display/CWIKIUSDOCS/JWT+-+JSON+Web+Token Confluence 平台的原始翻译文件更新地址 什么是 JSON Web Token(JWT)?JSON Web Token (JWT) 作为一个凋谢的规范 (RFC 7519) 定义了一种简洁自蕴含的办法用于通信单方之间以 JSON 对象的模式平安的传递信息。因为有数字签名,所以这些通信的信息可能被校验和信赖。 JWT 能够应用秘钥(secret)进行签名 (应用 HMAC 算法) 或应用 RSA 或 ECDSA 算法的公钥/私钥对(public/private key)。 只管 JWT 能够在通信的单方之间通过提供秘钥(secret)来进行签名,咱们将会更多关注 已签名(signed)的 token。 通过签名的令牌能够验证其中数据的 _完整性(integrity)_ ,而加密的令牌能够针对其余方 _暗藏(hide)_ 申明。 当令牌(token)应用 公钥/私钥对(public/private key)进行签名的时候,只有持有私钥进行签名的一方是进行签名的。 要害术语的中英文对照token - 令牌secret - 秘钥signature - 签名claims - 要求或者数据

October 1, 2020 · 1 min · jiezi

关于jwt:关于JWT-Token-自动续期的解决方案

前言在前后端拆散的开发模式下,前端用户登录胜利后后端服务会给用户颁发一个jwt token。前端(如vue)在接管到jwt token后会将token存储到LocalStorage中。 后续每次申请都会将此token放在申请头中传递到后端服务,后端服务会有一个过滤器对token进行拦挡校验,校验token是否过期,如果token过期则会让前端跳转到登录页面从新登录。 因为jwt token中个别会蕴含用户的根底信息,为了保障token的安全性,个别会将token的过期工夫设置的比拟短。 然而这样又会导致前端用户须要频繁登录(token过期),甚至有的表单比较复杂,前端用户在填写表单时须要思考较长时间,等真正提交表单时后端校验发现token过期生效了不得不跳转到登录页面。 如果真产生了这种状况前端用户必定是要骂人的,用户体验十分不敌对。本篇内容就是在前端用户无感知的状况下实现token的主动续期,防止频繁登录、表单填写内容失落状况的产生。 实现原理jwt token主动续期的实现原理如下: 登录胜利后将用户生成的 jwt token 作为key、value存储到cache缓存外面 (这时候key、value值一样),将缓存有效期设置为 token无效工夫的2倍。当该用户再次申请时,通过后端的一个 jwt Filter 校验前端token是否是无效token,如果token有效表明是非法申请,间接抛出异样即可;依据规定取出cache token,判断cache token是否存在,此时次要分以下几种状况: cache token 不存在这种状况表明该用户账户闲暇超时,返回用户信息已生效,请从新登录。cache token 存在,则须要应用jwt工具类验证该cache token 是否过期超时,不过期无需解决。过期则示意该用户始终在操作只是token生效了,后端程序会给token对应的key映射的value值从新生成jwt token并笼罩value值,该缓存生命周期从新计算。实现逻辑的外围原理:前端申请Header中设置的token放弃不变,校验有效性以缓存中的token为准。 代码实现(伪码)登录胜利后给用户签发token,并设置token的有效期...SysUser sysUser = userService.getUser(username,password);if(null !== sysUser){ String token = JwtUtil.sign(sysUser.getUsername(), sysUser.getPassword());}...public static String sign(String username, String secret) { //设置token有效期为30分钟 Date date = new Date(System.currentTimeMillis() + 30 * 60 * 1000); //应用HS256生成token,密钥则是用户的明码 Algorithm algorithm = Algorithm.HMAC256(secret); // 附带username信息 return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);}将token存入redis,并设定过期工夫,将redis的过期工夫设置成token过期工夫的两倍Sting tokenKey = "sys:user:token" + token;redisUtil.set(tokenKey, token);redisUtil.expire(tokenKey, 30 * 60 * 2);过滤器校验token,校验token有效性public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { //从header中获取token String token = httpServletRequest.getHeader("token") if(null == token){ throw new RuntimeException("illegal request,token is necessary!") } //解析token获取用户名 String username = JwtUtil.getUsername(token); //依据用户名获取用户实体,在理论开发中从redis取 User user = userService.findByUser(username); if(null == user){ throw new RuntimeException("illegal request,token is Invalid!") } //校验token是否生效,主动续期 if(!refreshToken(token,username,user.getPassword())){ throw new RuntimeException("illegal request,token is expired!") } ...}实现token的主动续期public boolean refreshToken(String token, String userName, String passWord) { Sting tokenKey = "sys:user:token" + token ; String cacheToken = String.valueOf(redisUtil.get(tokenKey)); if (StringUtils.isNotEmpty(cacheToken)) { // 校验token有效性,留神须要校验的是缓存中的token if (!JwtUtil.verify(cacheToken, userName, passWord)) { String newToken = JwtUtil.sign(userName, passWord); // 设置超时工夫 redisUtil.set(tokenKey, newToken) ; redisUtil.expire(tokenKey, 30 * 60 * 2); } return true; } return false;}...public static boolean verify(String token, String username, String secret) { try { // 依据明码生成JWT效验器 Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).withClaim("username", username).build(); // 效验TOKEN DecodedJWT jwt = verifier.verify(token); return true; } catch (Exception exception) { return false; }}本文中jwt的相干操作是基于 com.auth0.java-jwt 实现,大家能够通过浏览原文获取 JWTUtil 工具类。 ...

August 4, 2020 · 2 min · jiezi

springboot整合token

写在前面在前后端交互过程中,为了保证信息安全,我们往往需要加点用户验证。本文介绍了用springboot简单整合token。springboot版本2.2.0。另外主要用到了jjwt,redis。阅读本文,你大概需要花费7-10分钟时间整合token1. 导入相关依赖pom.xml文件中 <!-- jwt 加密解密工具类--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>2.TokenUtil.java实现生成/解析tokenpackage com.dbc.usermanager.util;import com.dbc.usermanager.service.RedisService;import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.springframework.beans.factory.annotation.Autowired;import javax.crypto.spec.SecretKeySpec;import javax.xml.bind.DatatypeConverter;import java.security.Key;import java.util.Date;public class TokenUtil { /** * 签名秘钥,可以换成 秘钥 注入 */ public static final String SECRET = "DaTiBao";//注意:本参数需要长一点,不然后面剪切的时候很可能长度为0,就会报错 /** * 签发地 */ public static final String issuer = "dtb.com"; /** * 过期时间 */ public static final long ttlMillis = 3600*1000*60; /** * 生成token * * @param id 一般传入userName * @return */ public static String createJwtToken(String id,String subject) { return createJwtToken(id, issuer, subject, ttlMillis); } public static String createJwtToken(String id) { return createJwtToken(id, issuer, "", ttlMillis); } /** * 生成Token * * @param id 编号 * @param issuer 该JWT的签发者,是否使用是可选的 * @param subject 该JWT所面向的用户,是否使用是可选的; * @param ttlMillis 签发时间 (有效时间,过期会报错) * @return token String */ public static String createJwtToken(String id, String issuer, String subject, long ttlMillis) { // 签名算法 ,将对token进行签名 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成签发时间 long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); // 通过秘钥签名JWT byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(SECRET); String str=signatureAlgorithm.getJcaName(); Key signingKey = new SecretKeySpec(apiKeySecretBytes, str); // 让我们设置JWT声明 JwtBuilder builder = Jwts.builder().setId(id) .setIssuedAt(now) .setSubject(subject) .setIssuer(issuer) .signWith(signatureAlgorithm, signingKey); // if it has been specified, let's add the expiration if (ttlMillis >= 0) { //过期时间 long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } // 构建JWT并将其序列化为一个紧凑的url安全字符串 return builder.compact(); } /** * Token解析方法 * @param jwt Token * @return */ public static Claims parseJWT(String jwt) { // 如果这行代码不是签名的JWS(如预期),那么它将抛出异常 Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(SECRET)) .parseClaimsJws(jwt).getBody(); return claims; } public static void main(String[] args) { String token = TokenUtil.createJwtToken("2","ltz"); System.out.println(TokenUtil.createJwtToken("2","ltz")); Claims claims = TokenUtil.parseJWT(token); System.out.println(claims); }}3.新增登录验证的注解@LoginRequiredpackage com.dbc.usermanager.util;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;//加入此注解,就需要token@Target({ElementType.METHOD, ElementType.TYPE})// 表明此注解可用在方法名上@Retention(RetentionPolicy.RUNTIME)// 运行时有效public @interface LoginRequired { boolean required() default true;}4.测试 @PostMapping(value = "test") @ApiOperation(value="生成token") public ResultJson test(@RequestBody JSONObject requestJson){ String token= TokenUtil.createJwtToken("1","dtb"); redisService.set(token,"1"); return new ResultJson(0,"测试成功",null); } @GetMapping(value = "getToken") @LoginRequired @ApiOperation("") public ResultJson getToken(String token){ if(redisService.exists(token)){ System.out.println(redisService.get(token)); } return new ResultJson(0,"测试成功",null); }最后实体类User.java等相关文件就不贴出来了,大家可以用自己写好的实体类去编写。很多步骤与思想都在代码中体现,代码中也加了很多注释,你可以根据自己的需求进行增删查改。

November 5, 2019 · 2 min · jiezi

SpringBoot2X使用JWTJSONWebToken

一、跨域认证遇到的问题由于多终端的出现,很多的站点通过 web api restful 的形式对外提供服务,采用了前后端分离模式进行开发,因而在身份验证的方式上可能与传统的基于 cookie 的 Session Id 的做法有所不同,除了面临跨域提交 cookie 的问题外,更重要的是,有些终端可能根本不支持 cookie。 JWT(JSON Web Token) 是一种身份验证及授权方案,简单的说就是调用端调用 api 时,附带上一个由 api 端颁发的 token,以此来验证调用者的授权信息。 一般流程是下面这样: 用户向服务器发送用户名和密码。服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。服务器向用户返回一个 session_id,写入用户的 Cookie。用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。这种模式的问题在于扩展性不好。单机没有问题,如果是服务器集群、跨域的服务导向架构或者用户禁用了 cookie ,就不行了。 二、解决方案 1. 单机和分布式应用下登录校验,session 共享 单机和多节点 tomcat 应用登录检验 ①、单机 tomcat 应用登录,sesssion 保存在浏览器和应用服务器会话之间,用户登录成功后,服务端会保证一个 session,也会给客户端一个 sessionId,客户端会把 sessionId 保存在 cookie 中,用户每次请求都会携带这个 sessionId。②、多节点 tomcat 应用登录,开启 session 数据共享后,每台服务器都能够读取 session。缺点是每个 session 都是占用内存和资源的,每个服务器节点都需要同步用户的数据,即一个数据需要存储多份到每个服务器,当用户量到达百万、千万级别的时,占用资源就严重,用户体验特别不好!! 分布式应用中 session 共享 ①、真实的应用不可能单节点部署,所以就有个多节点登录 session 共享的问题需要解决。tomcat 支持 session 共享,但是有广播风暴;用户量大的时候,占用资源就严重,不推荐②、Reids 集群,存储登陆的 token,向外提供服务接口,Redis 可设置过期时间(服务端使用 UUID生成随机 64 位或者 128 位 token ,放入 Redis 中,然后返回给客户端并存储)。 ...

November 5, 2019 · 2 min · jiezi

springbootplus集成SpringBootShiroJWT权限管理

SpringBoot+Shiro+JWT权限管理ShiroApache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。三个核心组件:Subject, SecurityManager 和 Realms. Subject代表了当前用户的安全操作,即“当前操作用户”。SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。ShiroBasicArchitecture ShiroArchitecture JWTJSON Web Token(JWT)是目前最流行的跨域身份验证解决方案JSON Web令牌是一种开放的行业标准 RFC 7519方法,用于在双方之间安全地表示声明。JWT 数据结构eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJodHRwczovL3NwcmluZ2Jvb3QucGx1cyIsIm5hbWUiOiJzcHJpbmctYm9vdC1wbHVzIiwiaWF0IjoxNTE2MjM5MDIyfQ.1Cm7Ej8oIy1P5pkpu8-Q0B7bTU254I1og-ZukEe84II JWT有三部分组成:Header:头部,Payload:负载,Signature:签名SpringBoot+Shiro+JWTpom.xml Shiro依赖<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.4.1</version></dependency>pom.xml JWT依赖<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.8.3</version></dependency>ShiroConfig.java配置@Slf4j@Configurationpublic class ShiroConfig { /** * JWT过滤器名称 */ private static final String JWT_FILTER_NAME = "jwtFilter"; /** * Shiro过滤器名称 */ private static final String SHIRO_FILTER_NAME = "shiroFilter"; @Bean public CredentialsMatcher credentialsMatcher() { return new JwtCredentialsMatcher(); } /** * JWT数据源验证 * * @return */ @Bean public JwtRealm jwtRealm(LoginRedisService loginRedisService) { JwtRealm jwtRealm = new JwtRealm(loginRedisService); jwtRealm.setCachingEnabled(false); jwtRealm.setCredentialsMatcher(credentialsMatcher()); return jwtRealm; } /** * 禁用session * * @return */ @Bean public DefaultSessionManager sessionManager() { DefaultSessionManager manager = new DefaultSessionManager(); manager.setSessionValidationSchedulerEnabled(false); return manager; } @Bean public SessionStorageEvaluator sessionStorageEvaluator() { DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); sessionStorageEvaluator.setSessionStorageEnabled(false); return sessionStorageEvaluator; } @Bean public DefaultSubjectDAO subjectDAO() { DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO(); defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator()); return defaultSubjectDAO; } /** * 安全管理器配置 * * @return */ @Bean public DefaultWebSecurityManager securityManager(LoginRedisService loginRedisService) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(jwtRealm(loginRedisService)); securityManager.setSubjectDAO(subjectDAO()); securityManager.setSessionManager(sessionManager()); SecurityUtils.setSecurityManager(securityManager); return securityManager; } /** * ShiroFilterFactoryBean配置 * * @param securityManager * @param loginRedisService * @param shiroProperties * @param jwtProperties * @return */ @Bean(SHIRO_FILTER_NAME) public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager, LoginService loginService, LoginRedisService loginRedisService, ShiroProperties shiroProperties, JwtProperties jwtProperties) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String, Filter> filterMap = new HashedMap(); filterMap.put(JWT_FILTER_NAME, new JwtFilter(loginService, loginRedisService, jwtProperties)); shiroFilterFactoryBean.setFilters(filterMap); Map<String, String> filterChainMap = shiroFilterChainDefinition(shiroProperties).getFilterChainMap(); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap); return shiroFilterFactoryBean; } /** * Shiro路径权限配置 * * @return */ @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition(ShiroProperties shiroProperties) { DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); // 获取ini格式配置 String definitions = shiroProperties.getFilterChainDefinitions(); if (StringUtils.isNotBlank(definitions)) { Map<String, String> section = IniUtil.parseIni(definitions); log.debug("definitions:{}", JSON.toJSONString(section)); for (Map.Entry<String, String> entry : section.entrySet()) { chainDefinition.addPathDefinition(entry.getKey(), entry.getValue()); } } // 获取自定义权限路径配置集合 List<ShiroPermissionConfig> permissionConfigs = shiroProperties.getPermissionConfig(); log.debug("permissionConfigs:{}", JSON.toJSONString(permissionConfigs)); if (CollectionUtils.isNotEmpty(permissionConfigs)) { for (ShiroPermissionConfig permissionConfig : permissionConfigs) { String url = permissionConfig.getUrl(); String[] urls = permissionConfig.getUrls(); String permission = permissionConfig.getPermission(); if (StringUtils.isBlank(url) && ArrayUtils.isEmpty(urls)) { throw new ShiroConfigException("shiro permission config 路径配置不能为空"); } if (StringUtils.isBlank(permission)) { throw new ShiroConfigException("shiro permission config permission不能为空"); } if (StringUtils.isNotBlank(url)) { chainDefinition.addPathDefinition(url, permission); } if (ArrayUtils.isNotEmpty(urls)) { for (String string : urls) { chainDefinition.addPathDefinition(string, permission); } } } } // 最后一个设置为JWTFilter chainDefinition.addPathDefinition("/**", JWT_FILTER_NAME); Map<String, String> filterChainMap = chainDefinition.getFilterChainMap(); log.debug("filterChainMap:{}", JSON.toJSONString(filterChainMap)); return chainDefinition; } /** * ShiroFilter配置 * * @return */ @Bean public FilterRegistrationBean delegatingFilterProxy() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); DelegatingFilterProxy proxy = new DelegatingFilterProxy(); proxy.setTargetFilterLifecycle(true); proxy.setTargetBeanName(SHIRO_FILTER_NAME); filterRegistrationBean.setFilter(proxy); filterRegistrationBean.setAsyncSupported(true); filterRegistrationBean.setEnabled(true); filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC); return filterRegistrationBean; } @Bean public Authenticator authenticator(LoginRedisService loginRedisService) { ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); authenticator.setRealms(Arrays.asList(jwtRealm(loginRedisService))); authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); return authenticator; } /** * Enabling Shiro Annotations * * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * depends-on lifecycleBeanPostProcessor * * @return */ @Bean public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); return defaultAdvisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; }}JWT过滤器配置@Slf4jpublic class JwtFilter extends AuthenticatingFilter { private LoginService loginService; private LoginRedisService loginRedisService; private JwtProperties jwtProperties; public JwtFilter(LoginService loginService, LoginRedisService loginRedisService, JwtProperties jwtProperties) { this.loginService = loginService; this.loginRedisService = loginRedisService; this.jwtProperties = jwtProperties; } /** * 将JWT Token包装成AuthenticationToken * * @param servletRequest * @param servletResponse * @return * @throws Exception */ @Override protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { String token = JwtTokenUtil.getToken(); if (StringUtils.isBlank(token)) { throw new AuthenticationException("token不能为空"); } if (JwtUtil.isExpired(token)) { throw new AuthenticationException("JWT Token已过期,token:" + token); } // 如果开启redis二次校验,或者设置为单个用户token登陆,则先在redis中判断token是否存在 if (jwtProperties.isRedisCheck() || jwtProperties.isSingleLogin()) { boolean redisExpired = loginRedisService.exists(token); if (!redisExpired) { throw new AuthenticationException("Redis Token不存在,token:" + token); } } String username = JwtUtil.getUsername(token); String salt; if (jwtProperties.isSaltCheck()){ salt = loginRedisService.getSalt(username); }else{ salt = jwtProperties.getSecret(); } return JwtToken.build(token, username, salt, jwtProperties.getExpireSecond()); } /** * 访问失败处理 * * @param request * @param response * @return * @throws Exception */ @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest httpServletRequest = WebUtils.toHttp(request); HttpServletResponse httpServletResponse = WebUtils.toHttp(response); // 返回401 httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 设置响应码为401或者直接输出消息 String url = httpServletRequest.getRequestURI(); log.error("onAccessDenied url:{}", url); ApiResult apiResult = ApiResult.fail(ApiCode.UNAUTHORIZED); HttpServletResponseUtil.printJSON(httpServletResponse, apiResult); return false; } /** * 判断是否允许访问 * * @param request * @param response * @param mappedValue * @return */ @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { String url = WebUtils.toHttp(request).getRequestURI(); log.debug("isAccessAllowed url:{}", url); if (this.isLoginRequest(request, response)) { return true; } boolean allowed = false; try { allowed = executeLogin(request, response); } catch (IllegalStateException e) { //not found any token log.error("Token不能为空", e); } catch (Exception e) { log.error("访问错误", e); } return allowed || super.isPermissive(mappedValue); } /** * 登陆成功处理 * * @param token * @param subject * @param request * @param response * @return * @throws Exception */ @Override protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { String url = WebUtils.toHttp(request).getRequestURI(); log.debug("鉴权成功,token:{},url:{}", token, url); // 刷新token JwtToken jwtToken = (JwtToken) token; HttpServletResponse httpServletResponse = WebUtils.toHttp(response); loginService.refreshToken(jwtToken, httpServletResponse); return true; } /** * 登陆失败处理 * * @param token * @param e * @param request * @param response * @return */ @Override protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { log.error("登陆失败,token:" + token + ",error:" + e.getMessage(), e); return false; }}JWT Realm配置@Slf4jpublic class JwtRealm extends AuthorizingRealm { private LoginRedisService loginRedisService; public JwtRealm(LoginRedisService loginRedisService) { this.loginRedisService = loginRedisService; } @Override public boolean supports(AuthenticationToken token) { return token != null && token instanceof JwtToken; } /** * 授权认证,设置角色/权限信息 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { log.debug("doGetAuthorizationInfo principalCollection..."); // 设置角色/权限信息 String token = principalCollection.toString(); // 获取username String username = JwtUtil.getUsername(token); // 获取登陆用户角色权限信息 LoginSysUserRedisVo loginSysUserRedisVo = loginRedisService.getLoginSysUserRedisVo(username); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 设置角色 authorizationInfo.setRoles(loginSysUserRedisVo.getRoles()); // 设置权限 authorizationInfo.setStringPermissions(loginSysUserRedisVo.getPermissions()); return authorizationInfo; } /** * 登陆认证 * * @param authenticationToken * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { log.debug("doGetAuthenticationInfo authenticationToken..."); // 校验token JwtToken jwtToken = (JwtToken) authenticationToken; if (jwtToken == null) { throw new AuthenticationException("jwtToken不能为空"); } String salt = jwtToken.getSalt(); if (StringUtils.isBlank(salt)) { throw new AuthenticationException("salt不能为空"); } return new SimpleAuthenticationInfo( jwtToken, salt, getName() ); }}更多配置:https://github.com/geekidea/spring-boot-plusapplication.yml配置############################## spring-boot-plus start ##############################spring-boot-plus: ######################## Spring Shiro start ######################## shiro: # shiro ini 多行字符串配置 filter-chain-definitions: | /=anon /static/**=anon /templates/**=anon # 权限配置 permission-config: # 排除登陆登出相关 - urls: /login,/logout permission: anon # 排除静态资源 - urls: /static/**,/templates/** permission: anon # 排除Swagger - urls: /docs,/swagger-ui.html, /webjars/springfox-swagger-ui/**,/swagger-resources/**,/v2/api-docs permission: anon # 排除SpringBootAdmin - urls: /,/favicon.ico,/actuator/**,/instances/**,/assets/**,/sba-settings.js,/applications/** permission: anon # 测试 - url: /sysUser/getPageList permission: anon ######################## Spring Shiro end ########################## ############################ JWT start ############################# jwt: token-name: token secret: 666666 issuer: spring-boot-plus audience: web # 默认过期时间1小时,单位:秒 expire-second: 3600 # 是否刷新token refresh-token: true # 刷新token的时间间隔,默认10分钟,单位:秒 refresh-token-countdown: 600 # redis校验jwt token是否存在,可选 redis-check: true # true: 同一个账号只能是最后一次登陆token有效,false:同一个账号可多次登陆 single-login: false # 盐值校验,如果不加自定义盐值,则使用secret校验 salt-check: true ############################ JWT end ############################################################## spring-boot-plus end ###############################Redis存储信息使用Redis缓存JWTToken和盐值:方便鉴权,token后台过期控制等Redis二次校验和盐值校验是可选的127.0.0.1:6379> keys *1) "login:user:token:admin:0f2c5d670f9f5b00201c78293304b5b5"2) "login:salt:admin"3) "login:user:admin"4) "login:token:0f2c5d670f9f5b00201c78293304b5b5"Redis存储的JwtToken信息127.0.0.1:6379> get login:token:0f2c5d670f9f5b00201c78293304b5b5{ "@class": "io.geekidea.springbootplus.shiro.vo.JwtTokenRedisVo", "host": "127.0.0.1", "username": "admin", "salt": "f80b2eed0110a7ea5a94c35cbea1fe003d9bb450803473428b74862cceb697f8", "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJ3ZWIiLCJpc3MiOiJzcHJpbmctYm9vdC1wbHVzIiwiZXhwIjoxNTcwMzU3ODY1LCJpYXQiOjE1NzAzNTQyNjUsImp0aSI6IjE2MWQ1MDQxZmUwZjRmYTBhOThjYmQ0ZjRlNDI1ZGQ3IiwidXNlcm5hbWUiOiJhZG1pbiJ9.0ExWSiniq7ThMXfqCOi9pCdonY8D1azeu78_vLNa2v0", "createDate": [ "java.util.Date", 1570354265000 ], "expireSecond": 3600, "expireDate": [ "java.util.Date", 1570357865000 ]}ReferenceShirohttps://shiro.apache.org/spring.htmlhttps://shiro.apache.org/spring-boot.htmlJWThttps://jwt.io/https://github.com/auth0/java-jwtspring-boot-plushttps://github.com/geekidea/spring-boot-plushttps://springboot.plus/guide/shiro-jwt.html

October 8, 2019 · 6 min · jiezi

JWS-解析

参考文档 rfc7515 JWS 也就是 Json Web Signature,是构造 JWT 的基础结构(JWT 其实涵盖了 JWS 和 JWE 两类,其中 JWT 的载荷还可以是嵌套的 JWT),包括三部分 JOSE Header、JWS Payload、JWS Signature。 这里的 Signature 可以有两种生成方式,一种是标准的签名,使用非对称加密,因为私钥的保密性,能够确认签名的主体,同时能保护完整性;另一种是消息认证码 MAC(Message Authentication Code),使用对称秘钥,该秘钥需要在签发、验证的多个主体间共享,因此无法确认签发的主体,只能起到保护完整性的作用。 JWS 最终有两种序列化的表现形式,一种是 JWS Compact Serialization,为一串字符;另一种是 JWS JSON Serialization,是一个标准的 Json 对象,允许为同样的内容生成多个签名/消息认证码。 JWS Compact Serialization,各部分以 '.' 分隔。 BASE64URL(UTF8(JWS Protected Header)) || ’.’ ||BASE64URL(JWS Payload) || ’.’ ||BASE64URL(JWS Signature)JWS Json Serialization 还可以分为两种子格式:通用、扁平。 通用格式,最外层为 payload、signatures。signatures 中可以包含多个 json 对象,内层的 json 对象由 protected、header、signature 组成。不同的 protected header 生成不同的 Signature。 ...

October 7, 2019 · 2 min · jiezi

JSON-Web-Token-使用详解

JWT是什么?JSON Web Token(缩写 JWT)是目前最流行的<font color='red'>跨域</font>认证解决方案。它是有三部分组成,示例如下,具体的讲解如下(jwt是不会有空行的,下面只是为了显示,便使用了换行看着比较方便)。 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjMfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c它是由一个"."号隔开、三部分组成。第一部分是header信息, { "alg": "HS256",// 加密的算法 "typ": "JWT"// 加密的方式,填写JWT}第二部分是Payload,有固定的六个部分和自定义数据组成,自定义数据看自己的情况需要来定义,是可以省去的。 'iss' => 'https://www.qqdeveloper.com',// 签发人'exp' => time() + 86400,// 过期时间(这里的有效期时间为1天)'sub' => '主题内容',// 主题'aud' => '受众内容',// 受众'nbf' => $time,// 生效时间'iat' => $time,// 签发时间'jti' => 123,// 编号第三部分是Signature(是对前两部分加密得来的)。由于前两部分是公开透明的数据,因此防止数据的篡改和泄露,我们需要加密处理。首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。 第一部分的加密方式(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)最终生成的就是上面很长的一段字符串了。 为什么会使用JWT这就需要从我们传统的认证模式来说了,传统的认证模式是基于session和cookie来实现用户的认证和鉴权。具体的流程模式如下图。<center>(图一)Session与Cookie认证与鉴权</center>1.客户端向服务端发送一个http请求。 2.服务端在收到客户端的请求时,生成一个唯一的sessionid,<font color='red'>这里需要将该生成的session存储在服务端</font>,这个sessionid存储具体的session内容,默认的是文件存储,当然我们可以修改具体的存储方式,例如数据库存储。3.客户端在接受到这个sessionid时,存在cookie里面,每次请求时携带该sessionid。4.服务端在接收到客户端的请求之后,根据客户端发送的sessionid来进行认证与授权。这里也推荐一下自己之前分享的一篇有关session于cookie的知识点。session与cookie详解<center>(图二)传统的token授权</center>1.客户端向服务端发送一个http请求。 2.服务端在收到客户端的请求之后,生成一个唯一token,<font color='red'>这里需要将该生成的token存储在服务端</font>,至于怎么存,可以和上面session与cookie的方式一致。也可以存在缓存数据库中,如redis,memcached。3.服务端将该token返回给客户端,客户端存在本地,可以存请求头header中,也可以存在cookie中,同时也可以存在localstorage中。4.向服务端发送请求时,携带该token,服务端进行认证或者授权。<center>(图三)JWT认证模式</center>1.客户端向服务端发送一个http请求。 2.服务端根据jwt的生成规则,生成一个token,并返回给客户端,<font color='red'>这里服务端是不需要存储的</font>。3.客户端在接受到该token时,存在客户端。4.客户端向服务端发送请求时,服务端对请求的token进行解析,如果发现解析出来的数据和生成的数据是一致的代表是一个合法的token,则进行相应的操作。 基于session和cookie的认证和鉴权模式有什么好与不好的地方呢?总结如下几点:通过上面几张图,我们也大致可以看得出来,基于session都是需要服务端存储的,而JWT是不需要服务端来存储的。针对以上几点,总结如下:一、缺点1.容易遇到跨域问题。不同域名下是无法通过session直接来做到认证和鉴权的。2.分布式部署的系统,需要使用共享session机制3.容易出现csrf问题。 二、优点1.方便灵活,服务器端直接创建一个sessionid,下发给客户端,客户端请求携带sessionid即可。2.session存储在服务端,更加安全。3.便于服务端清除session,让用户重新授权一次。 JWT与session有什么区别呢?JWT是基于客户端存储的一种认证方式,然而session是基于服务端存储的一种认证方式。JWT虽然不用服务端存储了,也可以避免跨域、csrf等情况。但也存在如下几个不太好的地方。1.无法清除认证token。由于JWT生成的token都是存储在客户端的,不能有服务端去主动清除,只有直到失效时间到了才能清除。除非服务端的逻辑做了改变。2.存储在客户端,相对服务端,安全性更低一些。当JWT生成的token被破解,我们不便于清除该token。 如何使用JWT这里推荐使用GitHub上面人家封装好的包,这里我使用的是firebase/php-jwt,在项目中直接使用即可安装成功。 composer require firebase/php-jwt接下来创建一个控制器,我这里使用的ThinkPHP5.1的框架 use think\Controller;use Firebase\JWT\JWT;class Test extends Controller{ private $key = 'jwtKey'; // 生成JWT public function createJwt() { $time = time(); $key = $this->key; $token = [ 'iss' => 'https://www.qqdeveloper.com',// 签发人 'exp' => $time + 86400,// 过期时间(这里的有效期时间为1天) 'sub' => '主题内容',// 主题 'aud' => '受众内容',// 受众 'nbf' => $time,// 生效时间 'iat' => $time,// 签发时间 'jti' => 123,// 编号 // 额外自定义的数据 'data' => [ 'userName' => '编程浪子走四方' ]]; // 调用生成加密方法('Payloadn内容','加密的键',['加密算法'],['加密的可以'],['JWT的header头']) $jwt = JWT::encode($token, $key); return json(['data' => $jwt]); } // 解析JWT public function analysisJwt() { try { $key = $this->key; $jwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOlwvXC9leGFtcGxlLm9yZyIsImV4cCI6MTU2ODA5NjE4MCwic3ViIjoiXHU0ZTNiXHU5ODk4XHU1MTg1XHU1YmI5IiwiYXVkIjoiXHU1M2Q3XHU0ZjE3XHU1MTg1XHU1YmI5IiwibmJmIjoxNTY4MDA5NzgwLCJpYXQiOjE1NjgwMDk3ODAsImp0aSI6MTIzLCJkYXRhIjp7InVzZXJOYW1lIjoiXHU3ZjE2XHU3YTBiXHU2ZDZhXHU1YjUwXHU4ZDcwXHU1NmRiXHU2NWI5In19.kHb_9Np0zjE25YE9czUEGvmFPYtqMJT9tuZzJTuMZl0'; // 调用解密方法('JWT内容','解密的键,和加密时的加密键一直','加密算法') $decoded = JWT::decode($jwt, $key, array('HS256')); return json(['message' => $decoded]); } catch (\Exception $exception) { return json(['message' => $exception->getMessage()]); } }}通过访问第一个方法,可以生成下图一段字符串我们将上图中的字符串复制到第二图中的$jwt变量,访问第二个方法即可解析出具体的数据。 ...

September 9, 2019 · 1 min · jiezi

lumen框架下jwt配置多guards使用

JWT的配置文件config/jwt.php翻译ttl:token有效期(分钟)refresh_ttl:刷新token时间(分钟)algo:token签名算法user:指向User模型的命名空间路径identifier:用于从token的sub中获取用户require_claims:必须出现在token的payload中的选项,否则会抛出TokenInvalidException异常blacklist_enabled:如果该选项被设置为false,那么我们将不能废止token,即使我们刷新了token,前一个token仍然有效providers:完成各种任务的具体实现,如果需要的话你可以重写他们User —— providers.user:基于sub获取用户的实现JWT —— providers.jwt:加密/解密tokenAuthentication —— providers.auth:通过证书/ID获取认证用户+Storage —— providers.storage:存储token直到它们失效载荷(Payload)我们先将用户认证的操作描述成一个JSON对象。其中添加了一些其他的信息,帮助今后收到这个JWT的服务器理解这个JWT 这里面的前6个字段都是由JWT的标准所定义的。sub: 该JWT所面向的用户 (这部分就是我们使用到的。)iss: 该JWT的签发者iat(issued at): 在什么时候签发的tokenexp(expires): token什么时候过期nbf(not before):token在此时间之前不能被接收处理jti:JWT ID为web token提供唯一标识路由定义// 登陆$app->post('login', 'AuthController@login');// 使用supplier守卫$app->group(['prefix' => 'auth', 'middleware' => 'auth:supplier'], function ($app) { // 退出登陆 $app->get('logout', 'AuthController@logout'); // 刷新token $app->get('refresh', 'AuthController@refresh'); // 获取授权详情 $app->get('detail', 'AuthController@detail');});中间件定义namespace App\Http\Middleware;use Closure;use Illuminate\Contracts\Auth\Factory as Auth;class Authenticate{ /** * The authentication guard factory instance. * * @var \Illuminate\Contracts\Auth\Factory */ protected $auth; /** * Create a new middleware instance. * * @param \Illuminate\Contracts\Auth\Factory $auth * @return void */ public function __construct(Auth $auth) { $this->auth = $auth; } /** * @desc 在进入控制器之前,判断并处理请求体 * @date 2019/6/27 18:06 * @param $request * @param Closure $next * @param null $guard */ public function handle($request, Closure $next, $guard = null) { // 判断当前用户是否是游客(未登录) if ($this->auth->guard($guard)->guest()) { $response['code'] = '4001'; $response['errorMsg'] = '无效令牌,需要重新获取'; return response()->json($response); } // 判断当前用户是否登录 if ($this->auth->guard($guard)->check()) { $user = $this->auth->guard($guard)->user(); // 获取登陆信息 \Illuminate\Support\Facades\Auth::setUser($user); // 设置Auth获取的用户信息 } return $next($request); }}BaseController定义公用方法/** * @desc 初始化供应商认证器 * @date 2019/6/27 15:08 */protected function supplier(){ // 辅助函数返回守卫信息 //return auth('supplier'); // Auth授权方式返回守卫信息 return Auth::guard('supplier');}Token:创建$credentials = $request->only('username', 'password');$validate = $this->supplier()->validate($credentials); // 验证账密是否正确$token = $this->supplier()->attempt($credentials); // 根据账户密码创建token# 更具用户Model创建token$user = AdminModel::find(1000000);$token = $this->supplier()->fromUser($user);Token:获取$this->supplier()->getToken()获取token过期时间默认单位分钟,所以乘以60得到秒单位 ...

June 28, 2019 · 2 min · jiezi

Yii-使用JWT

了解JWT可以参考:了解JWT 一:下载JWT拓展 在JWT官网中我们可以看到很多php版本的JWT,选择一个JWT进行下载 这里我选择的是lcobucci/jwt,使用composer进行下载 lcobucci/jwt的composer地址:https://packagist.org/package... composer require lcobucci/jwt "^3.3.0" #这里我下载的是3.3.0版本二:lcobucci/jwt使用 lcobucci/jwt使用方法可以参考下载下来的README.md文件(vendor/lcobucci/jwt/README.md) 1:生成JWT $request = Yii::$app->getRequest();$signer = new Sha256();//使用Sha256加密,常用加密方式有Sha256,Sha384,Sha512$time = time();$tokenBuilder = (new Builder()) ->issuedBy($request->getHostInfo()) // 设置发行人 ->permittedFor(isset($_SERVER['HTTP_ORIGIN']) ? $_SERVER['HTTP_ORIGIN'] : '') // 设置接收 ->identifiedBy(Yii::$app->security->generateRandomString(10), true) // 设置id ->issuedAt($time) // 设置生成token的时间 ->canOnlyBeUsedAfter($time) // 设置token使用时间(实时使用) ->expiresAt($time + 3600); //设置token过期时间//定义自己所需字段$user = ['user_name' => '测试', 'user_no' => '001'];$tokenBuilder->withClaim('user', $user);$tokenBuilder->withClaim('ceshi', '测试字段');//使用Sha256加密生成token对象,该对象的字符串形式为一个JWT字符串$token = $tokenBuilder->getToken($signer, new Key('jwt_secret'));echo (string) $token;2:对JWT进行校验 在正常的开发环境下,我们将生成的JWT字符串传到前端,当前端调用其他接口时,将我们所给的JWT传递到后台,我们后台需要对前端传来的JWT字符串进行校验 下面的$token为我们获取到的前端传递的JWT $token = (new Parser())->parse($token);//数据校验$data = new ValidationData(); // 使用当前时间来校验数据if (!$token->validate($data)) { //数据校验失败 return '数据校验失败';}//token校验$signer = new Sha256();//生成JWT时使用的加密方式if (!$token->verify($signer, new Key('jwt_secret'))) { //token校验失败 return 'token校验失败';}echo '校验成功';3:获取JWT的相关信息 ...

June 24, 2019 · 2 min · jiezi

了解-JWT

JWT官网:https://jwt.io/ 一:JWT简介 JWT全名JSON WEB TOKEN,是一个JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串 二:JWT作用 由于http协议是无状态的,所以客户端每次访问都是新的请求。这样每次请求都需要验证身份,传统方式是用session+cookie来记录/传输用户信息,而JWT就是更安全方便的方式。它的特点就是简洁,紧凑和自包含,而且不占空间,传输速度快,而且有利于多端分离,接口的交互等等,JWT是一种Token规范,主要面向的还是登录、验证和授权方向,当然也可以用只来传递信息。一般都是存在header里 三:JWT结构 JWT一共分为三个部分:Header(头部) . Payload(负载) . Signature(签名) ; 1:Header(头部) Header 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子: { "alg": "HS256", "typ": "JWT"}字段 全称 描述alg algorithm 是签名的算法;一般是 HS256typ type 固定值为 JWT二:PayLoad(负载)Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用: { "iss": "d8b832c0c8caf0d99e9406ed", "sub": "1", "aud": "baijunyao", "iat": "1557066830", "nbf": "1557066840", "exp": "1557066850", "jti": "9e9668d8b8306ed8caf0d94"}字段 全称 描述iss issuer 发布者sub subject 面向的用户aud audience 受众iat issued at 签发时间的时间戳nbf not before 生效时间的时间戳exp expiration time 过期时间的时间戳jti jwt id 每个 JWT 自己的唯一 id除了官方定义的这些字段,我们也可以自己定义一些自己需要的字段 ...

June 24, 2019 · 1 min · jiezi

JWTJson-Web-Token-科普

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案,本文介绍它的原理和用法。 一、跨域认证的问题互联网服务离不开用户认证。一般流程是下面这样: 用户向服务器发送用户名和密码。服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。服务器向用户返回一个 session_id,写入用户的 Cookie。用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。 举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现? 一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。 另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。 二、JWT 的原理JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样: { "姓名": "张三", "角色": "管理员", "到期时间": "2018年7月1日0点0分"}以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。 三、JWT 的数据结构实际的 JWT 大概就像下面这样: 它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。 JWT 的三个部分依次如下: Header(头部)Payload(负载)Signature(签名)写成一行,就是下面的样子:Header.Payload.Signature 下面依次介绍这三个部分: 3.1 HeaderHeader 部分是一个 JSON 对象,描述 JWT 的元数据,通常是下面的样子: { "alg": "HS256", "typ": "JWT"}上面代码中,alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256);typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。 ...

June 22, 2019 · 1 min · jiezi

express-jwt-持久化登录vue前端篇

jwt 持久化验证前端篇,node 配置详情请移步这里 我用的是vue3,下面是 src 的目录 用到的依赖 验证思路Home 页写登录,然后在 About 页获取到登录名。登录成功缓存 token,进入About页时,通过判断是否有 token 来判断是否登录/登录超时登录页在登录页输入用户名和密码,将其提交到vuex // src/views/Home.vue<template> <div class="home"> <input type="text" v-model="user" placeholder="账号"> <input type="text" v-model="password" placeholder="密码"> <button @click="login">点击</button> </div></template><script>import {mapActions} from 'vuex'export default { data(){ return{ user:'', password:'' } }, name: 'home', methods:{ ...mapActions(["toLogin"]), login(){ // 请求之后能拿到用户名,nickname,把用户名存在state // 传入多个参数 改成对象 // action moutation只能拿第一个参数哦,所以要改成对象 this.toLogin({user:this.user,password:this.password}) } }}</script>后台 jwt后台的 jwt 验证,我们把过期时间设置成60s // src/app.jslet express = require('express')let cors = require('cors')let bodyParser = require('body-parser')let jwt = require("jsonwebtoken")let app = express()app.use(cors())app.use(bodyParser.json())app.use(bodyParser.urlencoded({extended:false}))// 模拟一个登陆的接口app.post('/login',function(req,res){ // 登录成功获取用户名 let username = req.body.user res.json({ // 进行加密的方法 // sing 参数一:加密的对象 参数二:加密的规则 参数三:对象 token:jwt.sign({username:username},'abcd',{ // 过期时间 expiresIn:"60s" }), username, code:200 })})// 登录持久化验证接口 访问这个接口的时候 一定要访问token(前端页面每切换一次,就访问一下这个接口,问一下我有没有登录/登陆过期)// 先访问登录接口,得到token,在访问这个,看是否成功app.post('/validate',function(req,res){ let token = req.headers.authorization; // 验证token合法性 对token进行解码 jwt.verify(token,'abcd',function(err,decode){ if(err){ res.json({ msg:'当前用户未登录' }) }else { // 证明用户已经登录 res.json({ token:jwt.sign({username:decode.username},'abcd',{ // 过期时间 expiresIn:"60s" }), username:decode.username, msg:'已登录' }) } })})app.listen(8000,function(){ console.log('OK')})后台接口// src/api/login.jsimport axios from 'axios'axios.defaults.baseURL = 'http://localhost:8000'// axios 请求拦截axios.interceptors.request.use(function(response){ // 在 headers 中设置authorization 属性放token,token是存在缓存中的 response.headers.authorization = localStorage["token"] return response}, function (error) { return Promise.reject(error); })// axios 响应拦截器axios.interceptors.response.use(function (response) { return response.data; }, function (error) { return Promise.reject(error); });// 登录的接口export let loginApi = (user,password) => { return axios.post('/login',{user,password})}// 验证是否登录的接口export let valiApi = () => { return axios.post('/validate')}vuex// src/store.jsimport Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)import {loginApi,valiApi} from './api/login'export default new Vuex.Store({ state: { username:"" }, mutations: { setusername(state,payload){ // 改变state里的 username state.username = payload } }, actions: { async toLogin({commit},{user,password}){ let res = await loginApi(user,password) console.log(res) let {username,token} = res // 提交到 mutations commit("setusername",username) // token 具有时效性 登录成功 把token存在本地存储 localStorage["token"] = token }, async valiApi({commit}){ const { username, token } = await valiApi(); commit('setusername', username); localStorage["token"] = token return username !== undefined; } }})验证持久化登录页 从 vuex 中拿到用户名,打开页面就请求是否登录的 api ,从而实现持久化登录验证 ...

June 14, 2019 · 2 min · jiezi

带你弄懂JWT原理

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。 让我们来假想一下一个场景。在A用户关注了B用户的时候,系统发邮件给B用户,并且附有一个链接“点此关注A用户”。链接的地址可以是这样的 https://your.app.com/make-friend/?from_user=B&target_user=A上面的URL主要通过URL来描述这个当然这样做有一个弊端,那就是要求用户B用户是一定要先登录的。可不可以简化这个流程,让B用户不用登录就可以完成这个操作。JWT就允许我们做到这点。 JWT的组成一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。 { "iss": "John Wu JWT", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com, "sub": "kevin@example.com", "from_user": "B", "target_user": "A" } 这里面的前五个字段都是由JWT的标准所定义的。 iss: 该JWT的签发者sub: 该JWT所面向的用户aud: 接收该JWT的一方exp(expires): 什么时候过期,这里是一个Unix时间戳iat(issued at): 在什么时候签发的这些定义都可以在标准中找到。 将上面的JSON对象进行[base64编码]可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。 eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9注意:base64是一种编码,它是可以被翻译回原来的样子来的。它并不是一种加密过程。头部(Header)JWT还需要一个头部,头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。 {"typ": "JWT", "alg": "HS256" } 在这里,我们说明了这是一个JWT,并且我们所用的签名算法(后面会提到)是HS256算法。 对它也要进行Base64编码,之后的字符串就成了JWT的Header(头部)。 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9签名(签名)将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0最后,我们将上面拼接完的字符串($input)用HS256算法($algo)进行加密。在加密的时候,我们还需要提供一个密钥($secret)。比方我们使用字符串mySecret作为密钥的话,那么就可以通过下面的方法得到我们加密后的内容作为JWT的签名: hash_hmac($algo, $input, $secret);// output: rSWamyAYwuHCo7IFAgd1oRpùSP7nzL7BF5t7ItqpKViM最后将这一部分签名也拼接在被签名的字符串后面,我们就得到了完整的JWT: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcm9tX3VzZXIiOiJCIiwidGFyZ2V0X3VzZXIiOiJBIn0.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM于是,我们就可以将邮件中的URL改成 https://your.app.com/make-fri...这样服务端验证完JWT的签名没问题后就可以完成B用户添加A用户为好友的操作而不在需要用户再进行登录啦。 看到这里你应该会有两个疑问 签名的目的是什么?Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?签名的目的 最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小。 所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。 服务器应用在接收到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。 如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。 关于两个签名的推荐用PHP中内建函数hash_equals来帮我们完成 hash_equals ( string $known_string , string $user_string ) : bool比较两个字符串,无论它们是否相等,本函数的时间消耗是恒定的。 ...

June 7, 2019 · 1 min · jiezi

express-jwt-postMan验证-实现持久化登录

原理第一次登陆时会返回一个经过加密的token,下一次访问接口(携带登录返回你的token)的时候,会对token进行解密,如果解密正在进行,说明你已经登录,再把过期时间延长下载npm init -y // 一键初始化npm install express -s // 下载expressnpm install cors // 跨域中间件npm install body-parser // body-parser中间件 解析带请求体的数据(post,put)npm install jsonwebtoken // 持久化登录 jwt json web token基本配置// 引入expresslet express = require('express')let cors = require('cors')let bodyParser = require('body-parser')let jwt = require("jsonwebtoken")let banner = require("./banner")// 拿到服务器let app = express()app.use(cors())app.use(bodyParser.json())app.use(bodyParser.urlencoded({extended:false}))// listen 后面跟着的是端口app.listen(8000,function(){ console.log('OK')})模拟一个登陆的接口app.post('/login',function(req,res){ let {username} = req.body console.log(username) res.json({ // 进行加密的方法 // sing 参数一:加密的对象 参数二:加密的规则 参数三:对象 token:jwt.sign({username:username},'abcd',{ // 过期时间 expiresIn:"1h" }), username, code:200 })})postMan模拟 发送POST请求 ...

June 4, 2019 · 1 min · jiezi

利用-jwt-配合-postman-实现-持久化登录

jwt 实现持久化登录原理第一次登录时会返回一个经过加密的token下一次访问接口时(携带登录返回给你的token)会对token进行解密 如果解密正确 证明你已经登录 再把过期时间延长// 首先 npm init -y 一键初始化// 引入 express 下载 npm install expresslet express = require('express');let app = express();// 用于跨域 下载 npm install corslet cors = require('cors');// 解析带请求体(post,put)的数据 下载 npm install body-parserlet bodyParser = require('body-parser');// 引入 jwt 全称 json web token 下载 npm install jsonwebtokenlet jwt = require('jsonwebtoken');// 解析 json 格式app.use(bodyParser.json())// 解析 form 格式app.use(bodyParser.urlencoded({extended:true}))app.use(cors())// 进行登录持久化验证的接口// 访问这个接口时 一定要携带 token 前端页面每切换一次就访问一下这个接口 问一下我有没有登录 或者登录过期app.post('/validate',function(req,res){ let token = req.headers.authorization; // 验证token的合法性 jwt.verify(token,'sxq',function(err,decode){ if(err){ res.json({ msg:'当前用户未登录' }) }else{ // 证明用户已经登录 只要用户操作就会有过期时间 res.json({ username:decode.user, token:jwt.sign({username:decode.user},'sxq',{ // 过期时间 expiresIn:'1h' }) }) } })})// 持久化登录的原理// 第一次登录时会返回一个经过加密的token// 下一次访问接口时(携带登录返回给你的token)会对token进行解密 如果解密正确 证明你已经登录 再把过期时间延长// 模拟一个登录接口 username passwordapp.post('/login',function(req,res){ let {username} = req.body res.json({ // sign 参数 加密的对象 加密的规则 token:jwt.sign({username},'sxq',{ // 过期时间 expiresIn:'1h' }), username })})// 接口app.listen(3000)

June 4, 2019 · 1 min · jiezi

koajwt实现token验证与刷新

JWTJSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 本文只讲Koa2 + jwt的使用,不了解JWT的话请到这里)进行了解。 koa环境要使用koa2+jwt需要先有个koa的空环境,搭环境比较麻烦,我直接使用koa起手式,这是我使用koa+typescript搭建的空环境,如果你也经常用koa写写小demo,可以点个star,方便~ 安装koa-jwtkoa-jwt主要作用是控制哪些路由需要jwt验证,哪些接口不需要验证: import * as koaJwt from 'koa-jwt';//路由权限控制 除了path里的路径不需要验证token 其他都要app.use( koaJwt({ secret: secret.sign }).unless({ path: [/^\/login/, /^\/register/] }));上面代码中,除了登录、注册接口不需要jwt验证,其他请求都需要。 使用jsonwebtoken生成、验证token执行npm install jsonwebtoken安装jsonwebtoken相关代码: import * as jwt from 'jsonwebtoken';const secret = 'my_app_secret';const payload = {user_name:'Jack', id:3, email: '1234@gmail.com'};const token = jwt.sign(payload, secret, { expiresIn: '1h' });上面代码中通过jwt.sign来生成一个token,参数意义: payload:载体,一般把用户信息作为载体来生成tokensecret:秘钥,可以是字符串也可以是文件expiresIn:过期时间 1h表示一小时在登录中返回tokenimport * as crypto from 'crypto';import * as jwt from 'jsonwebtoken';async login(ctx){ //从数据库中查找对应用户 const user = await userRespository.findOne({ where: { name: user.name } }); //密码加密 const psdMd5 = crypto .createHash('md5') .update(user.password) .digest('hex'); //比较密码的md5值是否一致 若一致则生成token并返回给前端 if (user.password === psdMd5) { //生成token token = jwt.sign(user, secret, { expiresIn: '1h' }); //响应到前端 ctx.body = { token } }}前端拦截器前端通过登录拿到返回过来的token,可以将它存在localStorage里,然后再以后的请求中把token放在请求头的Authorization里带给服务端。这里以axios请求为例,在发送请求时,通过请求拦截器把token塞到header里: ...

May 30, 2019 · 2 min · jiezi

jwt

const jwt = require('jsonwebtoken')// 定义签名const key = 'token'const Token = { encrypt: function (data, time = 60) { // string加密数据; time过期时间,默认3天后过期,单位秒 return jwt.sign(data.toJSON(), key, {expiresIn: time}) }, decrypt: function (token) { try { let data = jwt.verify(token, key) console.log('data', data) return { token: true, data } } catch (e) { return { token: false, data: e } } }}module.exports = Token

May 21, 2019 · 1 min · jiezi

基于Spring-Security-Oauth2的SSO单点登录JWT权限控制实践

概 述在前文《基于Spring Security和 JWT的权限系统设计》之中已经讨论过基于 Spring Security和 JWT的权限系统用法和实践,本文则进一步实践一下基于 Spring Security Oauth2实现的多系统单点登录(SSO)和 JWT权限控制功能,毕竟这个需求也还是蛮普遍的。 代码已开源,放在文尾,需要自取理论知识在此之前需要学习和了解一些前置知识包括: Spring Security:基于 Spring实现的 Web系统的认证和权限模块OAuth2:一个关于授权(authorization)的开放网络标准单点登录 (SSO):在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统JWT:在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息要完成的目标目标1:设计并实现一个第三方授权中心服务(Server),用于完成用户登录,认证和权限处理目标2:可以在授权中心下挂载任意多个客户端应用(Client)目标3:当用户访问客户端应用的安全页面时,会重定向到授权中心进行身份验证,认证完成后方可访问客户端应用的服务,且多个客户端应用只需要登录一次即可(谓之 “单点登录 SSO”)基于此目标驱动,本文设计三个独立服务,分别是: 一个授权服务中心(codesheep-server)客户端应用1(codesheep-client1)客户端应用2(codesheep-client2)多模块(Multi-Module)项目搭建三个应用通过一个多模块的 Maven项目进行组织,其中项目父 pom中需要加入相关依赖如下: <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.0.8.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>io.spring.platform</groupId> <artifactId>platform-bom</artifactId> <version>Cairo-RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency></dependencies>项目结构如下: 授权认证中心搭建授权认证中心本质就是一个 Spring Boot应用,因此需要完成几个大步骤: pom中添加依赖<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency></dependencies>项目 yml配置文件:server: port: 8085 servlet: context-path: /uac即让授权中心服务启动在本地的 8085端口之上 创建一个带指定权限的模拟用户@Componentpublic class SheepUserDetailsService implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { if( !"codesheep".equals(s) ) throw new UsernameNotFoundException("用户" + s + "不存在" ); return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_MEDIUM")); }}这里创建了一个用户名为codesheep,密码 123456的模拟用户,并且赋予了 普通权限(ROLE_NORMAL)和 中等权限(ROLE_MEDIUM) ...

May 7, 2019 · 2 min · jiezi

认识JWT

1.JSON Web Token是什么JSON Web Token (JWT)是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 2.什么时候你应该用JSON Web Tokens下列场景中使用JSON Web Token是很有用的: Authorization (授权) : 这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的JWT的一个特性,因为它的开销很小,并且可以轻松地跨域使用。Information Exchange (信息交换) : 对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。3.JSON Web Token的结构是什么样的 JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是: HeaderPayloadSignature因此,一个典型的JWT看起来是这个样子的: xxxxx.yyyyy.zzzzz接下来,具体看一下每一部分: Header header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HMAC SHA256或者RSA等等)。 例如: 然后,用Base64对这个JSON编码就得到JWT的第一部分 Payload JWT的第二部分是payload,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered, public 和 private。 Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。Public claims : 可以随意定义。Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。下面是一个例子: 对payload进行Base64编码就得到JWT的第二部分 注意,不要在JWT的payload或header中放置敏感信息,除非它们是加密的。 Signature 为了得到签名部分,你必须有编码过的header、编码过的payload、一个秘钥,签名算法是header中指定的那个,然对它们签名即可。 例如: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。 ...

May 3, 2019 · 1 min · jiezi

JWT-refreshtoken-实践

Json web token (JWT), 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。详细介绍可以查看这篇文章 理解JWT(JSON Web Token)认证及实践JWT 特点优点体积小,因而传输速度快传输方式多样,可以通过URL/POST参数/HTTP头部等方式传输严格的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持为你的应用而定制化。支持跨域验证,可以应用于单点登录。存在的问题JWT 自身(在 payload 中)就包含了所有与用户相关的验证消息,所以通常情况下不需要保存。这种设计存在几个问题: Token不能撤销--客户端重置密码后之前的JWT依然可以使用(JWT 并没有过期或者失效不支持refresh token,JWT过期后需要执行登录授权的完整流程无法知道用户签发了几个JWT针对第一个问题,可能的解决方法有: 保存JWT到数据库(或Redis),这样可以针对每个JWT单独校验在重置密码等需要作废之前全部JWT时,把操作时间点记录到数据库(或Redis),校验JWT时同时判断此JWT创建之后有没有过重置密码等类似操作,如果有校验不通过当然,这种解决方法都会多一次数据库请求,JWT自身可校验的优势会有所减少,同时也会影响认证效率。 这篇文章主要介绍解决第二个问题(不支持refresh token)的思路。 refresh tokenrefresh token是OAuth2 认证中的一个概念,和OAuth2 的access token 一起生成,表示更新令牌,过期所需时间比access toen 要长,可以用来获取下一次的access token。 如果JWT 需要添加 refresh token支持,refresh token需要满足的条件有一下几项: 和JWT一起生成返回给客户端有实效时间,有效时间比JWT要长只能用来换取下一次JWT,不能用于访问认证不能重复使用(可选)refresh token 获取流程 refresh token 使用流程 代码示例import jwtimport time# 使用 sanic 作为restful api 框架 def create_token(account_id, username): payload = { "iss": "gusibi.mobi", "iat": int(time.time()), "exp": int(time.time()) + 86400 * 7, "aud": "www.gusibi.mobi", "sub": account_id, "username": username, "scopes": ['open'] } token = jwt.encode(payload, 'secret', algorithm='HS256') payload['grant_type'] = "refresh" refresh_token = jwt.encode(payload, 'secret', algorithm='HS256') return True, { 'access_token': token, 'account_id': account_id, "refresh_token": refresh_token }# 验证refresh token 出否有效def verify_refresh_token(token): payload = jwt.decode(token, 'secret', audience='www.gusibi.com', algorithms=['HS256']) # 校验token 是否有效,以及是否是refresh token,验证通过后生成新的token 以及 refresh_token if payload and payload.get('grant_type') == 'refresh': # 如果需要标记此token 已经使用,需要借助redis 或者数据库(推荐redis) return True, payload return False, None# 验证token 是否有效def verify_bearer_token(token): # 如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数 payload = jwt.decode(token, 'secret', audience='www.gusibi.com', algorithms=['HS256']) # 校验token 是否有效,以及不能是refresh token if payload and not payload.get('grant_type') == 'refresh': return True, payload return False, None参考链接理解JWT(JSON Web Token)认证及实践理解OAuth 2.0[1]References[1] 理解OAuth 2.0: http://www.ruanyifeng.com/blo... ...

April 29, 2019 · 1 min · jiezi

jwt前后端整合方案

一、jwt是什么JWT全称, JSON Web Token,是一个以JSON为基准的标准规范。 举例:服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样 { "姓名": "brook", "角色": "前端攻城狮", "帅气指数": "5颗星"}以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。 我们先看看jwt的真实面目 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwibmFtZSI6ImFkbWluIiwidXNlcm5hbWUiOiJhZG1pbiIsInBvc2l0aW9uIjoiIiwicGhvbmUiOm51bGwsImVtYWlsIjpudWxsLCJyb2xlIjpbImFkbWluIl0sImF2YXRhciI6Imh0dHA6Ly9pbWcuZG9uZ3FpdWRpLmNvbS91cGxvYWRzL2F2YXRhci8yMDE1LzA3LzI1L1FNMzg3bmg3QXNfdGh1bWJfMTQzNzc5MDY3MjMxOC5qcGciLCJpbnRyb2R1Y3Rpb24iOiIiLCJjcmVhdGVfdGltZSI6IjIwMTctMTEtMDJUMTg6MTU6NDguMDAwWiIsInVwZGF0ZV90aW1lIjoiMjAxNy0xMS0yNlQwNjozMzoxNy4wMDBaIiwiaWF0IjoxNTM5MjQ0NjQ1fQ.cRg7ZAQ-1ZBiJUPDx6naQupUMK2BLHmIusMQZrnqVpG它是一个很长的字符串,中间用点(.)分隔成三个部分。三个部分依次为 Header(头部) Payload(负载) Signature(签名)即header.payload.sign。 Header 部分是一个 JSON 对象,描述 JWT 的元数据。 Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用。 Signature 部分是对前两部分的签名,防止数据篡改。 关于这三部分的详解,可以具体参考阮一峰老师的文章:http://www.ruanyifeng.com/blo... 在使用jwt的时候建议放在 HTTP 请求的头信息Authorization字段里面,如下 Authorization: Bearer <token>二、jwt的好处前后端分离:使用JWT作为接口鉴权不需要前端代码发布到后端指定目录下,可以完全跨域,前端项目可以单独部署减轻服务端内存负担:比起使用session来保存cookie,JWT自身包含了所有信息,通过解密即可验证(当然啦,这个通过吧session存在redis来避免)安全性:防止CSRF攻击移动端:对于无法使用cookie的一些移动端,JWT能够正常使用部署:服务器不需要保存session数据,无状态,容易扩展PS:为什么不写为什么使用jwt呢,因为它其实还是存在不少缺点的,需要根据使用业务场景确定,不是所有的场景都适合使用jwt,甚至网上有些帖子都是在评论jwt比较鸡肋的。具体可以看分析https://juejin.im/entry/59748... 三、jwt怎么使用直接上图,流程如下 我们以Eggjs项目为例,使用koa-jwt这个库https://github.com/koajs/jwt 后端(以Eggjs项目为例)1、在config.default.js 中以中间件的方式使用koa-jwt config.middleware = ['compress', 'errorHandler','jwt']; // 加上配置 config.jwt = { match: '/api', secret: 'abiao', unless: ['/api/user/login'], };match指egg路由匹配到相应前缀,则会使用当前的中间件。可以使用正则表达式去匹配,推荐api前缀定为api。secret指jwt的加密密钥。unless指指定的路由不经过此中间件,一般为login接口PS:egg中使用插件有全局模式和中间件模式。全局模式应该使用egg的插件,中间件模式可以使用第三方koa的插件 ...

April 28, 2019 · 2 min · jiezi

JWT 在 Gin 中的使用

介绍JSON Web Token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该 Token 被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT 的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 Token 也可直接被用于认证,也可被加密。使用安装go get github.com/appleboy/gin-jwt引入import “github.com/appleboy/gin-jwt"我目前使用的版本是 v2.5.0.<!– more –>创建中间件设计 API 对象type API struct { App *apps.App // 业务对象 Router *gin.Engine // 路由 JWT *jwt.GinJWTMiddleware // jwt 对象}中间件对象:api.JWT = &jwt.GinJWTMiddleware{ Realm: “gin jwt”, Key: []byte(“secret key”), Timeout: time.Hour, MaxRefresh: time.Hour, PayloadFunc: func(data interface{}) jwt.MapClaims {}, Authenticator: func(c *gin.Context) (interface{}, error) {}, Authorizator: func(data interface{}, c *gin.Context) bool {}, Unauthorized: func(c *gin.Context, code int, message string) {}, TokenLookup: “header: Authorization, query: token, cookie: jwt”, // TokenLookup: “query:token”, // TokenLookup: “cookie:token”, TokenHeadName: “Bearer”, TimeFunc: time.Now, }Realm JWT标识Key 服务端密钥Timeout token 过期时间MaxRefresh token 更新时间PayloadFunc 添加额外业务相关的信息Authenticator 在登录接口中使用的验证方法,并返回验证成功后的用户对象。Authorizator 登录后其他接口验证传入的 token 方法Unauthorized 验证失败后设置错误信息TokenLookup 设置 token 获取位置,一般默认在头部的 Authorization 中,或者 query的 token 字段,cookie 中的 jwt 字段。TokenHeadName Header中 token 的头部字段,默认常用名称 Bearer。TimeFunc 设置时间函数注册阶段在注册时如果要直接返回 token,那么可以调用 TokenGenerator 来生成 token。token, expire, err := c.JWT.TokenGenerator(strconv.Itoa(user.ID), *user)TokenGenerator 的具体实现func (mw *GinJWTMiddleware) TokenGenerator(userID string, data interface{}) (string, time.Time, error) { // 根据签名算法创建 token 对象 token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm)) // 获取 claims claims := token.Claims.(jwt.MapClaims) // 设置业务中需要的额外信息 if mw.PayloadFunc != nil { for key, value := range mw.PayloadFunc(data) { claims[key] = value } } // 过期时间 expire := mw.TimeFunc().UTC().Add(mw.Timeout) claims[“id”] = userID claims[“exp”] = expire.Unix() claims[“orig_iat”] = mw.TimeFunc().Unix() // 生成 token tokenString, err := mw.signedString(token) if err != nil { return “”, time.Time{}, err } return tokenString, expire, nil}登录阶段登录时会调用 Authenticator 注册的方法。func (api *API) LoginAuthenticator(ctx *gin.Context) (interface{}, error) { var params model.UserParams if err := ctx.Bind(&params); err != nil { return “”, jwt.ErrMissingLoginValues } // 根据用户名获取用户 user, err := api.App.GetUserByName(params.Username) if err != nil { return nil, err } // 验证密码 if user.AuthPassword(params.Password) { return *user, nil } return nil, jwt.ErrFailedAuthentication}验证 Token其他接口在设置了中间件 Router.Use(api.JWT.MiddlewareFunc()) 后,通过调用 Authorizator 方法来验证。func (api *API) LoginedAuthorizator(data interface{}, c *gin.Context) bool { if id, ok := data.(string); ok { return api.App.IsExistUser(id) } return false}在业务 Hander 中可以通过方法 jwt.ExtractClaims(ctx) 来获取 payload 的信息。深入gin-jwt 依赖的 jwt 库叫做 jwt-go。下面来介绍一下这个库。核心的 Token 结构:// A JWT Token. Different fields will be used depending on whether you’re// creating or parsing/verifying a token.type Token struct { Raw string // The raw token. Populated when you Parse a token Method SigningMethod // The signing method used or to be used Header map[string]interface{} // The first segment of the token Claims Claims // The second segment of the token Signature string // The third segment of the token. Populated when you Parse a token Valid bool // Is the token valid? Populated when you Parse/Verify a token}这个Token结构体是用来生成 jwt 的 token。其中 Method 是用来表示签名使用的算法。Header 是头部jwt的信息,还有 Claims 记录额外的信息。然后是生成签名的方法,key 是服务端的密钥。func (t *Token) SignedString(key interface{}) (string, error) { var sig, sstr string var err error // 将 Header 和 Claims 转换成字符串然后 base64 之后拼接在一起。 if sstr, err = t.SigningString(); err != nil { return “”, err } // 使用签名算法加密 if sig, err = t.Method.Sign(sstr, key); err != nil { return “”, err } return strings.Join([]string{sstr, sig}, “.”), nil}解密 token 的对象叫做 Parsertype Parser struct {}// 主要解析方法func (p *Parser) ParseWithClaims(tokenString string, claims Claims, keyFunc Keyfunc) (*Token, error) {}Parser 除了验证 Token 外,还包括解码 Header 和 Claims 的内容。资源https://jwt.io/introductionhttps://github.com/appleboy/g…https://github.com/dgrijalva/… ...

April 19, 2019 · 3 min · jiezi

SpringBoot JWT Token 跨域 Preflight response is not successful

一、Springboot实现token校验SpringBoot实现token校验,可以通过Filter或者HandlerInterceptor,两种方式都可以,Filter在最外层,请求首先会通过Filter,filter允许请求才会通过Intercept。下面以HandlerInterceptor实现为例1.实现HandlerInterceptor,拦截请求校验tokenpublic class AuthenticationInterceptor implements HandlerInterceptor { private static final String URI_PASS_TOKEN = “/user/login”; @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception { log.info(“authentication interceptor preHandle path:{} uri:{}",httpServletRequest.getServletPath(),httpServletRequest.getRequestURI());// if (“OPTIONS”.equalsIgnoreCase(httpServletRequest.getMethod())) {// return true;// } if (httpServletRequest.getRequestURI().endsWith(URI_PASS_TOKEN)) { return true; } //从http header里面获取token String token = httpServletRequest.getHeader(“token”); if (StringUtils.isEmpty(token)) { throw new AuthenticationException(CODE_AUTHENTICATION_FAILED,“token is empty”); } Algorithm algorithm = Algorithm.HMAC256(JwtConstant.TOKEN_CREATE_SECRET); JWTVerifier verifier = JWT.require(algorithm).build(); try { verifier.verify(token); }catch (Exception ex){ throw new AuthenticationException(CODE_AUTHENTICATION_FAILED,ex.getMessage()); } return true; } @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { }}2.Configuration配置,实现自动注入@Configurationpublic class InterceptorConfig extends WebMvcConfigurerAdapter { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor()) .addPathPatterns(”/**"); } @Bean public AuthenticationInterceptor authenticationInterceptor() { return new AuthenticationInterceptor(); }}二、前端调用 跨域 Preflight response is not successful通过单元测试、PostMan测试都可以调同,但是vue前端怎么都无法调用,错误如下:参考https://segmentfault.com/a/11…发现是浏览器发出的OPTIONS预检请求被HandlerInterceptor拦截了,因此在HandlerInterceptor添加如下代码: if (“OPTIONS”.equalsIgnoreCase(httpServletRequest.getMethod())) { return true; }对于options的请求不进行token检测即可 ...

April 16, 2019 · 1 min · jiezi

干货|一个案例学会Spring Security 中使用 JWT

在前后端分离的项目中,登录策略也有不少,不过 JWT 算是目前比较流行的一种解决方案了,本文就和大家来分享一下如何将 Spring Security 和 JWT 结合在一起使用,进而实现前后端分离时的登录解决方案。1 无状态登录1.1 什么是有状态?有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如Tomcat中的Session。例如登录:用户登录后,我们把用户的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session,然后下次请求,用户携带cookie值来(这一步有浏览器自动完成),我们就能识别到对应session,从而找到用户的信息。这种方式目前来看最方便,但是也有一些缺陷,如下:服务端保存大量数据,增加服务端压力服务端保存用户状态,不支持集群化部署1.2 什么是无状态微服务集群中的每个服务,对外提供的都使用RESTful风格的接口。而RESTful风格的一个最重要的规范就是:服务的无状态性,即:服务端不保存任何客户端请求者信息客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份那么这种无状态性有哪些好处呢?客户端请求不依赖服务端的信息,多次请求不需要必须访问到同一台服务器服务端的集群和状态对客户端透明服务端可以任意的迁移和伸缩(可以方便的进行集群化部署)减小服务端存储压力1.3.如何实现无状态无状态登录的流程:首先客户端发送账户名/密码到服务端进行认证认证通过后,服务端将用户信息加密并且编码成一个token,返回给客户端以后客户端每次发送请求,都需要携带认证的token服务端对客户端发送来的token进行解密,判断是否有效,并且获取用户登录信息1.4 JWT1.4.1 简介JWT,全称是Json Web Token, 是一种JSON风格的轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权: JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的Java 实现是GitHub 上的开源项目 jjwt,地址如下:https://github.com/jwtk/jjwt1.4.2 JWT数据格式JWT包含三部分数据:Header:头部,通常头部有两部分信息:声明类型,这里是JWT加密算法,自定义我们会对头部进行Base64Url编码(可解码),得到第一部分数据。Payload:载荷,就是有效数据,在官方文档中(RFC7519),这里给了7个示例信息:iss (issuer):表示签发人exp (expiration time):表示token过期时间sub (subject):主题aud (audience):受众nbf (Not Before):生效时间iat (Issued At):签发时间jti (JWT ID):编号这部分也会采用Base64Url编码,得到第二部分数据。Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥secret(密钥保存在服务端,不能泄露给客户端),通过Header中配置的加密算法生成。用于验证整个数据完整和可靠性。生成的数据格式如下图: 注意,这里的数据通过 . 隔开成了三部分,分别对应前面提到的三部分,另外,这里数据是不换行的,图片换行只是为了展示方便而已。1.4.3 JWT交互流程流程图:步骤翻译:应用程序或客户端向授权服务器请求授权获取到授权后,授权服务器会向应用程序返回访问令牌应用程序使用访问令牌来访问受保护资源(如API)因为JWT签发的token中已经包含了用户的身份信息,并且每次请求都会携带,这样服务的就无需保存用户信息,甚至无需去数据库查询,这样就完全符合了RESTful的无状态规范。1.5 JWT 存在的问题说了这么多,JWT 也不是天衣无缝,由客户端维护登录状态带来的一些问题在这里依然存在,举例如下:续签问题,这是被很多人诟病的问题之一,传统的cookie+session的方案天然的支持续签,但是jwt由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入redis,虽然可以解决问题,但是jwt也变得不伦不类了。注销问题,由于服务端不再保存用户信息,所以一般可以通过修改secret来实现注销,服务端secret修改后,已经颁发的未过期的token就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。密码重置,密码重置后,原本的token依然可以访问系统,这时候也需要强制修改secret。基于第2点和第3点,一般建议不同用户取不同secret。2 实战说了这么久,接下来我们就来看看这个东西到底要怎么用?2.1 环境搭建首先我们来创建一个Spring Boot项目,创建时需要添加Spring Security依赖,创建完成后,添加 jjwt 依赖,完整的pom.xml文件如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version></dependency>然后在项目中创建一个简单的 User 对象实现 UserDetails 接口,如下:public class User implements UserDetails { private String username; private String password; private List<GrantedAuthority> authorities; public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } //省略getter/setter}这个就是我们的用户对象,先放着备用,再创建一个HelloController,内容如下:@RestControllerpublic class HelloController { @GetMapping("/hello") public String hello() { return “hello jwt !”; } @GetMapping("/admin") public String admin() { return “hello admin !”; }}HelloController 很简单,这里有两个接口,设计是 /hello 接口可以被具有 user 角色的用户访问,而 /admin 接口则可以被具有 admin 角色的用户访问。2.2 JWT 过滤器配置接下来提供两个和 JWT 相关的过滤器配置:一个是用户登录的过滤器,在用户的登录的过滤器中校验用户是否登录成功,如果登录成功,则生成一个token返回给客户端,登录失败则给前端一个登录失败的提示。第二个过滤器则是当其他请求发送来,校验token的过滤器,如果校验成功,就让请求继续执行。这两个过滤器,我们分别来看,先看第一个:public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter { protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) { super(new AntPathRequestMatcher(defaultFilterProcessesUrl)); setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException { User user = new ObjectMapper().readValue(req.getInputStream(), User.class); return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword())); } @Override protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException { Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities(); StringBuffer as = new StringBuffer(); for (GrantedAuthority authority : authorities) { as.append(authority.getAuthority()) .append(","); } String jwt = Jwts.builder() .claim(“authorities”, as)//配置用户角色 .setSubject(authResult.getName()) .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000)) .signWith(SignatureAlgorithm.HS512,“sang@123”) .compact(); resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString(jwt)); out.flush(); out.close(); } protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException { resp.setContentType(“application/json;charset=utf-8”); PrintWriter out = resp.getWriter(); out.write(“登录失败!”); out.flush(); out.close(); }}关于这个类,我说如下几点:自定义 JwtLoginFilter 继承自 AbstractAuthenticationProcessingFilter,并实现其中的三个默认方法。attemptAuthentication方法中,我们从登录参数中提取出用户名密码,然后调用AuthenticationManager.authenticate()方法去进行自动校验。第二步如果校验成功,就会来到successfulAuthentication回调中,在successfulAuthentication方法中,将用户角色遍历然后用一个 , 连接起来,然后再利用Jwts去生成token,按照代码的顺序,生成过程一共配置了四个参数,分别是用户角色、主题、过期时间以及加密算法和密钥,然后将生成的token写出到客户端。第二步如果校验失败就会来到unsuccessfulAuthentication方法中,在这个方法中返回一个错误提示给客户端即可。再来看第二个token校验的过滤器:public class JwtFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) servletRequest; String jwtToken = req.getHeader(“authorization”); System.out.println(jwtToken); Claims claims = Jwts.parser().setSigningKey(“sang@123”).parseClaimsJws(jwtToken.replace(“Bearer”,"")) .getBody(); String username = claims.getSubject();//获取当前登录用户名 List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get(“authorities”)); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); }}关于这个过滤器,我说如下几点:首先从请求头中提取出 authorization 字段,这个字段对应的value就是用户的token。将提取出来的token字符串转换为一个Claims对象,再从Claims对象中提取出当前用户名和用户角色,创建一个UsernamePasswordAuthenticationToken放到当前的Context中,然后执行过滤链使请求继续执行下去。如此之后,两个和JWT相关的过滤器就算配置好了。2.3 Spring Security 配置接下来我们来配置 Spring Security,如下:@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder passwordEncoder() { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser(“admin”) .password(“123”).roles(“admin”) .and() .withUser(“sang”) .password(“456”) .roles(“user”); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/hello").hasRole(“user”) .antMatchers("/admin").hasRole(“admin”) .antMatchers(HttpMethod.POST, “/login”).permitAll() .anyRequest().authenticated() .and() .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class) .addFilterBefore(new JwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }}简单起见,这里我并未对密码进行加密,因此配置了NoOpPasswordEncoder的实例。简单起见,这里并未连接数据库,我直接在内存中配置了两个用户,两个用户具备不同的角色。配置路径规则时, /hello 接口必须要具备 user 角色才能访问, /admin 接口必须要具备 admin 角色才能访问,POST 请求并且是 /login 接口则可以直接通过,其他接口必须认证后才能访问。最后配置上两个自定义的过滤器并且关闭掉csrf保护。2.4 测试做完这些之后,我们的环境就算完全搭建起来了,接下来启动项目然后在 POSTMAN 中进行测试,如下: 登录成功后返回的字符串就是经过 base64url 转码的token,一共有三部分,通过一个 . 隔开,我们可以对第一个 . 之前的字符串进行解码,即Header,如下: 再对两个 . 之间的字符解码,即 payload: 可以看到,我们设置信息,由于base64并不是加密方案,只是一种编码方案,因此,不建议将敏感的用户信息放到token中。 接下来再去访问 /hello 接口,注意认证方式选择 Bearer Token,Token值为刚刚获取到的值,如下: 可以看到,访问成功。总结这就是 JWT 结合 Spring Security 的一个简单用法,讲真,如果实例允许,类似的需求我还是推荐使用 OAuth2 中的 password 模式。 不知道大伙有没有看懂呢?如果没看懂,松哥还有一个关于这个知识点的视频教程,如下: 如何获取这个视频教程呢?很简单,将本文转发到一个超过100人的微信群中(QQ群不算,松哥是群主的微信群也不算,群要为Java方向),或者多个微信群中,只要累计人数达到100人即可,然后加松哥微信,截图发给松哥即可获取资料。 ...

April 8, 2019 · 2 min · jiezi

Spring Boot Security OAuth2 实现支持JWT令牌的授权服务器

标题文字 #### 概要之前的两篇文章,讲述了Spring Security 结合 OAuth2 、JWT 的使用,这一节要求对 OAuth2、JWT 有了解,若不清楚,先移步到下面两篇提前了解下。Spring Boot Security 整合 OAuth2 设计安全API接口服务Spring Boot Security 整合 JWT 实现 无状态的分布式API接口这一篇我们来实现 支持 JWT令牌 的授权服务器。优点使用 OAuth2 是向认证服务器申请令牌,客户端拿这令牌访问资源服务服务器,资源服务器校验了令牌无误后,如果资源的访问用到用户的相关信息,那么资源服务器还需要根据令牌关联查询用户的信息。使用 JWT 是客户端通过用户名、密码 请求服务器获取 JWT,服务器判断用户名和密码无误之后,可以将用户信息和权限信息经过加密成 JWT 的形式返回给客户端。在之后的请求中,客户端携带 JWT 请求需要访问的资源,如果资源的访问用到用户的相关信息,那么就直接从JWT中获取到。所以,如果我们在使用 OAuth2 时结合JWT ,就能节省集中式令牌校验开销,实现无状态授权认证。快速上手项目说明工程名端口作用jwt-authserver8080授权服务器jwt-resourceserver8081资源服务器授权服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>WebSecurityConfig@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http. authorizeRequests().antMatchers("/").permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser(“user”).password(“123456”).roles(“USER”); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; }}为了方便,使用内存模式,在内存中创建一个用户 user 密码 123456。OAuth2AuthorizationServer/ * 授权服务器 /@Configuration@EnableAuthorizationServerpublic class OAuth2AuthorizationServer extends AuthorizationServerConfigurerAdapter { /* * 注入AuthenticationManager ,密码模式用到 / @Autowired private AuthenticationManager authenticationManager; /* * 对Jwt签名时,增加一个密钥 * JwtAccessTokenConverter:对Jwt来进行编码以及解码的类 / @Bean public JwtAccessTokenConverter accessTokenConverter() { JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); converter.setSigningKey(“test-secret”); return converter; } /* * 设置token 由Jwt产生,不使用默认的透明令牌 / @Bean public JwtTokenStore jwtTokenStore() { return new JwtTokenStore(accessTokenConverter()); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints .authenticationManager(authenticationManager) .tokenStore(jwtTokenStore()) .accessTokenConverter(accessTokenConverter()); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(“clientapp”) .secret(“123”) .scopes(“read”) //设置支持[密码模式、授权码模式、token刷新] .authorizedGrantTypes( “password”, “authorization_code”, “refresh_token”); }}资源服务器pom.xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId></dependency><dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.10.RELEASE</version></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>HelloController@RestController("/api")public class HelloController { @PostMapping("/api/hi") public String say(String name) { return “hi , " + name; }}OAuth2ResourceServer/* * 资源服务器 */@Configuration@EnableResourceServerpublic class OAuth2ResourceServer extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated().and() .requestMatchers().antMatchers("/api/**”); }}application.ymlserver: port: 8081security: oauth2: resource: jwt: key-value: test-secret参数说明:security.oauth2.resource.jwt.key-value:设置签名key 保持和授权服务器一致。security.oauth2.resource.jwt:项目启动过程中,检查到配置文件中有security.oauth2.resource.jwt 的配置,就会生成 jwtTokenStore 的 bean,对令牌的校验就会使用 jwtTokenStore 。验证请求令牌curl -X POST –user ‘clientapp:123’ -d ‘grant_type=password&username=user&password=123456’ http://localhost:8080/oauth/token返回JWT令牌{ “access_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU”, “token_type”: “bearer”, “refresh_token”: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZCJdLCJhdGkiOiI4YzRhYzI5Ni0wNDBhLTRjZTMtODkxMC0xYmY2NmRhNDA5OTciLCJleHAiOjE1NTY5Nzk5MDgsImF1dGhvcml0aWVzIjpbIlJPTEVfVVNFUiJdLCJqdGkiOiI0ZjA5M2ZjYS04NmM0LTQxZWUtODcxZS1kZTY2ZjFhOTI0NTAiLCJjbGllbnRfaWQiOiJjbGllbnRhcHAifQ.vvAE2LcqggBv8pxuqU6RKPX65bl7Zl9dfcoIbIQBLf4”, “expires_in”: 43199, “scope”: “read”, “jti”: “8c4ac296-040a-4ce3-8910-1bf66da40997”}携带JWT令牌请求资源curl -X POST -H “authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTQ0MzExMDgsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiOGM0YWMyOTYtMDQwYS00Y2UzLTg5MTAtMWJmNjZkYTQwOTk3IiwiY2xpZW50X2lkIjoiY2xpZW50YXBwIiwic2NvcGUiOlsicmVhZCJdfQ.YAaSRN0iftmlR6Khz9UxNNEpHHn8zhZwlQrCUCPUmsU” -d ’name=zhangsan’ http://localhost:8081/api/hi返回hi , zhangsan源码https://github.com/gf-huanchu… ...

April 5, 2019 · 2 min · jiezi

kong 配置 jwt 认证

我们只演示在 services 上配置jwt认证。service 开启 jwt 插件#创建一个需jwt验证的服务 成功响应返回baiducurl -x POST localhost:8001/services-d “name=service.jwt”-d “url=http://www.baidu.com”#查看插件列表curl -x GET localhost:8001/services/service.jwt/plugins#开启jwt插件curl -x POST localhost:8001/services/service.jwt/plugins-d “name=jwt”#查看jwt插件curl -x GET localhost:8001/services/service.jwt/plugins/jwt#删除jwt插件curl -x DELETE localhost:8001/services/service.jwt/plugins/{jwt.id}创建 route为service.jwt服务绑定routecurl -x POST localhost:8001/services/service.jwt/routes-d “name=route.jwt”-d “paths[]=/api/v1"创建一个 consumercurl -x POST localhost:8001/consumers -d “username=consumer.jwt”{ “custom_id”: null, “created_at”: 1553681695, “username”: “consumer.jwt”, “id”: “2e34d380-ec48-4a0d-926f-6dd8696a7eca”}创建 consumer 的 jwt 凭证可以指定算法algorithm,iss签发者key,密钥secret,也可以省略,会自动生成。curl -x POST localhost:8001/consumers/consumer.jwt/jwt -d “algorithm=HS256” -d “key=big_cat” -d “secret=uFLMFeKPPL525ppKrqmUiT2rlvkpLc9u”//response{ “rsa_public_key”: null, “created_at”: 1553681782, “consumer”: { “id”: “2e34d380-ec48-4a0d-926f-6dd8696a7eca” }, “id”: “61ee520c-3387-42f0-8e5f-02e0dc34d3d4”, “algorithm”: “HS256”, “secret”: “uFLMFeKPPL525ppKrqmUiT2rlvkpLc9u”, “key”: “7Xc3L8TdFpU6kgPEeR4iqMAstqLewJSS”}查看 consumer jwt 凭证curl -x GET localhost:8001/consumers/comsumer.jwt/jwt// 这里我们创建了 2个 jwt 凭证{ “next”: null, “data”: [ { “rsa_public_key”: null, “created_at”: 1553682659, “consumer”: { “id”: “2e34d380-ec48-4a0d-926f-6dd8696a7eca” }, “id”: “6966cec4-6d25-4642-983b-95e512eef608”, “algorithm”: “HS384”, “secret”: “WF3Ig85MgyGMZjvSCoKLOwOevZkD8jNG”, “key”: “big_cat” }, { “rsa_public_key”: null, “created_at”: 1553681990, “consumer”: { “id”: “2e34d380-ec48-4a0d-926f-6dd8696a7eca” }, “id”: “e3d34707-0f4f-4c2d-ae54-25aaed6c9211”, “algorithm”: “HS256”, “secret”: “yBcPzjWsaW0dMquiWCOGlH2ILDQfJIya”, “key”: “wP7ZxrL4OgMVViwE8GYcaYq57cVa2IHL” } ]}jwt 下发业务服务器根据kong生成的jwt凭证中的algorithm、key(iss)、secret进行token的演算和下发。请求鉴权接口需携带Authorization: Bearer jwt进行请求。测试的话可以用 https://jwt.io 生成:请求带有jwt认证的服务的路由curl -X GET localhost:8000/api/v1 -H ‘Authorization: Bearer eyJhbGciOiJIUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiaWdfY2F0Iiwic3ViIjoiMTIzNDU2Nzg5MCIsIm5hbWUiOiJKb2huIERvZSIsImlhdCI6MTUxNjIzOTAyMn0.8yO2FmP23u2sS3kq94B39uT23SU2WVNuslPTeSJaHfBLoCT4oNmFTODfHS3s6sot’//返回了baidu首页<html> <head> <script> location.replace(location.href.replace(“https://”,“http://”)); </script> </head> <body> <noscript> <meta http-equiv=“refresh” content=“0;url=http://www.baidu.com/"> </noscript> </body></html>否则// 401{ “message”: “Unauthorized”} ...

March 27, 2019 · 1 min · jiezi

JWT原理和简单应用

JWT认证登录最近在做一个审核系统,后台登录用到JWT登录认证,在此主要做个总结JWT是什么Json web token (JWT), 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。为什么使用JWT此处主要和传统的session作对比,传统的session在服务器端需要保存一些登录信息,通常是在内存中,在后端服务器是集群等分布式的情况下,其他主机没有保存这些信息,所以都需要通过一个固定的主机进行验证,如果用户量大,在认证这个点上容易形成瓶颈,是应用不易拓展。JWT原理JWT由三个部分组成,用点号分割,看起来像是这样,JWT token本身没有空格换行等,下面是为了美观处理了下eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJsYWJzX3B1cmlmaWVyLWFwaS1wYW5lbCIsImlhdCI6MTU1Mjk3NTg3OCwiZXhwIjoxNTU1NTY3ODc4LCJhdWQiOiJodHRwOi8vZmYtbGFic19wdXJpZmllci1hcGktdGVzdC5mZW5kYS5pby9wcm9kL3YxL2F1dGgvand0Iiwic3ViIjoiMTUwMTM4NTYxMTg4NDcwNCIsInNjb3BlcyI6WyJyZWdpc3RlciIsIm9wZW4iLCJsb2dpbiIsInBhbmVsIl19.m0HD1SUd30TWKuDQImwjIl9a-oWJreG7tKVzuGVh7e41.头部(Header)Header部分是一个json,描述JWT的元数据,通常是下面这样{ “alg”: “HS256”, “typ”: “JWT”}alg表示签名使用的的算法,默认是HMAC SHA256,写成HS256, tye表示这个token的类型,JWT token统一使用JWT,上面这段Header生成的token是eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ92.负载(Payload)官方规定了7个字段,解释如下iss: 签发人,可以填写生成这个token的ID等等,可选参数sub: 该JWT所面向的客户,可以存储用户的account_id等等,可选aud:该JWTtoken的接收方,可以填写生成这个token的接口URL,但是不强制,可选exp: 过期时间,时间戳,整数,可选参数iat:生成token的时间,unix时间,时间戳,可选参数nbf(Not Before): 表示该token在此时间前不可用,验证不通过的意思,可选jti: JWT ID,主要用来生成一次性token,可选的参数除了官方之外,我们还可以定义一部分自定义字段,但是考虑到BASE64是可逆的,所以不要放入敏感信息下面是一个例子;{ “iss”: “labs_purifier-api-panel”, “iat”: 1552975878, “exp”: 1555567878, “aud”: “http://ff-labs_purifier-api-test.fenda.io/prod/v1/auth/jwt", “sub”: “1501385611884704”, “scopes”: [ “register”, “open”, “login”, “panel” ]}上面这个Payload,经过BASE64加密后,生成的token是eyJpc3MiOiJsYWJzX3B1cmlmaWVyLWFwaS1wYW5lbCIsImlhdCI6MTU1Mjk3NTg3OCwiZXhwIjoxNTU1NTY3ODc4LCJhdWQiOiJodHRwOi8vZmYtbGFic19wdXJpZmllci1hcGktdGVzdC5mZW5kYS5pby9wcm9kL3YxL2F1dGgvand0Iiwic3ViIjoiMTUwMTM4NTYxMTg4NDcwNCIsInNjb3BlcyI6WyJyZWdpc3RlciIsIm9wZW4iLCJsb2dpbiIsInBhbmVsIl193.签名(Signature)Signature是对前面两部分生成的两段token的加密,使用的加密方式是Header里面指定的,此处是HS256,此时,需要一个秘钥,不可以泄露,大致过程如下:HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)JWT的使用JWT token 一般放在请求头里面,当然也可以放在cookie里面,但是放在cookie里面不可以跨域,例如:Authorization: Bearer <token>JWT在Python中的简单生成和验证jwt库生成tokendef create_token(): payload={ “iss”: “labs_purifier-api-panel”, “iat”: 1552975878, “exp”: 1555567878, “aud”: Config.AUDIENCE, “sub”: “1501385611884704”, “scopes”: [ “register”, “open”, “login”, “panel” ] } token = jwt.encode(payload, Config.SECRET_KEY, algorithm=‘HS256’) return True, {‘access_token’: token}验证tokendef verify_jwt_token(token): try: payload = jwt.decode(token, Config.SECRET_KEY, audience=Config.AUDIENCE, algorithms=[‘HS256’]) except (ExpiredSignatureError, DecodeError): return False, token if payload: return True, jwt_model需要注意的是,如果在生成的时候,加上了aud参数,验证的时候也要用上audience参数,并且值必须一样 ...

March 27, 2019 · 1 min · jiezi

如何在SpringBoot中集成JWT(JSON Web Token)鉴权

这篇博客主要是简单介绍了一下什么是JWT,以及如何在Spring Boot项目中使用JWT(JSON Web Token)。1.关于JWT1.1 什么是JWT老生常谈的开头,我们要用这样一种工具,首先得知道以下几个问题。这个工具是什么,这个工具解决了什么问题是否适用于当前我们所处得业务场景用了之后是否会带来任何其他问题怎么用才是最佳实践那什么是JWT呢?以下是我对jwt官网上对JWT介绍的翻译。JSON Web Token (JWT)是一种定义了一种紧凑并且独立的,用于在各方之间使用JSON对象安全的传输信息的一个开放标准(RFC 7519)。现在我们知道,JWT其实是一种开放标准,用于在多点之间安全地传输用JSON表示的数据。在传输的过程中,JWT以字符串的形式出现在我们的视野中。该字符串中的信息可以通过数字签名进行验证和信任。1.2 应用场景JWT在实际的开发中,有哪些应用场景呢?1.2.1 授权这应该算是JWT最常见的使用场景。在前端界面中,一旦用户登录成功,会接收到后端返回的JWT。后续的请求都会包含后端返回的JWT,作为对后端路由、服务以及资源的访问的凭证。1.2.2 信息交换利用JWT在多方之间相互传递信息具有一定的安全性,例如JWT可以用HMAC、RSA非对称加密算法以及ECDSA数字签名算法对JWT进行签名,可以确保消息的发送者是真的发送者,而且使用header和payload进行的签名计算,我们还可以验证发送的消息是否被篡改了。2.JWT的结构通俗来讲JWT由header.payload.signature三部分组成的字符串,网上有太多帖子介绍这一块了,所以在这里就简单介绍一下就好了。2.1 headerheader由使用的签名算法和令牌的类型的组成,例如令牌的类型就是JWT这种开放标准,而使用的签名算法就是HS256,也就是HmacSHA256算法。其他的加密算法还有HmacSHA512、SHA512withECDSA等等。然后将这个包含两个属性的JSON对象转化为字符串然后使用Base64编码,最终形成了JWT的header。2.2 payloadpayload说直白一些就类似你的requestBody中的数据。只不过是分了三种类型,预先申明好的、自定义的以及私有的。像iss发件人,exp过期时间都是预先注册好的申明。预先申明在载荷中的数据不是强制性的使用,但是官方建议使用。然后这串类似于requestBody的JSON经过Base64编码形成了JWT的第二部分。2.3 signature如果要生成signature,就需要使用jwt自定义配置项中的secret,也就是Hmac算法加密所需要的密钥。将之前经过Base64编码的header和payload用.相连,再使用自定义的密钥,对该消息进行签名,最终生成了签名。生成的签名用于验证消息在传输的过程中没有被更改。在使用非对称加密算法进行签名的时候,还可以用于验证JWT的发件人是否与payload中申明的发件人是同一个人。3.JWT在Spring项目中的应用场景3.1 生成JWT代码如下。public String createJwt(String userId, String projectId) throws IllegalArgumentException, UnsupportedEncodingException { Algorithm al = Algorithm.HMAC256(secret); Instant instant = LocalDateTime.now().plusHours(outHours).atZone(ZoneId.systemDefault()).toInstant(); Date expire = Date.from(instant); String token = JWT.create() .withIssuer(issuer) .withSubject(“userInfo”) .withClaim(“user_id”, userId) .withClaim(“project_id”, projectId) .withExpiresAt(expire) .sign(al); return token;}传入的两个Claim是项目里自定义的payload,al是选择的算法,而secret就是对信息签名的密钥,subject则是该token的主题,withExpiresAt标识了该token的过期时间。3.2 返回JWT在用户登录系统成功之后,将token作为返回参数,返回给前端。3.3 验证token在token返回给前端之后,后端要做的就是验证这个token是否是合法的,是否可以访问服务器的资源。主要可以通过以下几种方式去验证。3.3.1 解析token使用JWTVerifier解析token,这是验证token是否合法的第一步,例如前端传过来的token是一串没有任何意义的字符串,在这一步就可以抛出错误。示例代码如下。try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(token);} catch (JWTVerificationException e) { e.printStackTrace();}JWTVerifier可以使用用制定secret签名的算法,指定的claim来验证token的合法性。3.3.2 判断token时效性判断了token是有效的之后,再对token的时效性进行验证。try { Algorithm algorithm = Algorithm.HMAC256(secret); JWTVerifier verifier = JWT.require(algorithm).build(); DecodedJWT jwt = verifier.verify(token); if (jwt.getExpiresAt().before(new Date())) { System.out.println(“token已过期”); return null; }} catch (JWTVerificationException e) { e.printStackTrace(); return null;}如果该token过期了,则不允许访问服务器资源。具体的流程如下。3.3.3 刷新过期时间上面创建token的有效时间是可以配置的,假设是2个小时,并且用户登录进来连续工作了1小时59分钟,在进行一个很重要的操作的时候,点击确定,这个时候token过期了。如果程序没有保护策略,那么用户接近两个小时的工作就成为了无用功。遇到这样的问题,我们之前的流程设计必然面对一次重构。可能大家会有疑问,不就是在用户登录之后,每次操作对去刷新一次token的过期时间吗?那么问题来了,我们知道token是由header.payload.signature三段内容组成的,而过期时间则是属于payload,如果改变了过期的时间,那么最终生成的payload的hash则势必与上一次生成的不同。换句话说,这是一个全新的token。前端要怎么接收这个全新的token呢?可想到的解决方案无非就是每次请求,根据response header中的返回不断的刷新的token。但是这样的方式侵入了前端开发的业务层。使其每一个接口都需要去刷新token。大家可能会说,无非就是加一个拦截器嘛,对业务侵入不大啊。即使这部分逻辑是写在拦截器里的,但是前端因为token鉴权的逻辑而多出了这部分代码。而这部分代码从职能分工上来说,其实是后端的逻辑。说的直白一些,刷新token,对token的时效性进行管理,应该是由后端来做。前端不需要也不应该去关心这一部分的逻辑。3.3.4 redis大法好综上所述,刷新token的过期时间势必要放到后端,并且不能通过判断JWT中payload中的expire来判断token是否有效。所以,在用户登录成功之后并将token返回给前端的同时,需要以某一个唯一表示为key,当前的token为value,写入Redis缓存中。并且在每次用户请求成功后,刷新token的过期时间,流程如下所示。经过这样的重构之后,流程就变成了这样。在流程中多了一个刷新token的流程。只要用户登录了系统,每一次的操作都会刷新token的过期时间,就不会出现之前说的在进行某个操作时突然失效所造成数据丢失的情况。在用户登录之后的两个小时内,如果用户没有进行任何操作,那么2小时后再次请求接口就会直接被服务器拒绝访问。4.总结总的来说,JWT中是不建议放特别敏感信息的。如果没有用非对称加密算法的话,把token复制之后直接可以去jwt官网在线解析。如果请求被拦截到了,里面的所有信息等于是透明的。但是JWT可以用来当作一段时间内运行访问服务器资源的凭证。例如JWT的payload中带有userId这个字段,那么就可以对该token标识的用户的合法性进行验证。例如,该用户当前状态是否被锁定?该userId所标识的用户是否存在于我们的系统?等等。并且通过实现token的公用,可以实现用户的多端同时登录。像之前的登录之后创建token,就限定了用户只能同时在一台设备上登录。欢迎大家浏览之前的文章:个人博客,如果有说的不对的地方,还请不吝赐教。5.参考JWT官网 ...

March 18, 2019 · 1 min · jiezi

基于Spring Security和 JWT的权限系统设计

写在前面关于 Spring SecurityWeb系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求。在Java EE领域,成熟的安全框架解决方案一般有 Apache Shiro、Spring Security等两种技术选型。Apache Shiro简单易用也算是一大优势,但其功能还是远不如 Spring Security强大。Spring Security可以为 Spring 应用提供声明式的安全访问控制,起通过提供一系列可以在 Spring应用上下文中可配置的Bean,并利用 Spring IoC和 AOP等功能特性来为应用系统提供声明式的安全访问控制功能,减少了诸多重复工作。关于JWTJSON Web Token (JWT),是在网络应用间传递信息的一种基于 JSON的开放标准((RFC 7519),用于作为JSON对象在不同系统之间进行安全地信息传输。主要使用场景一般是用来在 身份提供者和服务提供者间传递被认证的用户身份信息。关于JWT的科普,可以看看阮一峰老师的《JSON Web Token 入门教程》。本文则结合 Spring Security和 JWT两大利器来打造一个简易的权限系统。本文实验环境如下:Spring Boot版本:2.0.6.RELEASEIDE:IntelliJ IDEA 2018.2.4另外本文实验代码置于文尾,需要自取。设计用户和角色本文实验为了简化考虑,准备做如下设计:设计一个最简角色表role,包括角色ID和角色名称设计一个最简用户表user,包括用户ID,用户名,密码再设计一个用户和角色一对多的关联表user_roles一个用户可以拥有多个角色创建 Spring Security和 JWT加持的 Web工程pom.xml 中引入 Spring Security和 JWT所必需的依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version></dependency>项目配置文件中加入数据库和 JPA等需要的配置server.port=9991spring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://121.196.XXX.XXX:3306/spring_security_jwt?useUnicode=true&characterEncoding=utf-8spring.datasource.username=rootspring.datasource.password=XXXXXXlogging.level.org.springframework.security=infospring.jpa.hibernate.ddl-auto=updatespring.jpa.show-sql=truespring.jackson.serialization.indent_output=true创建用户、角色实体用户实体 User:/** * @ www.codesheep.cn * 20190312 /@Entitypublic class User implements UserDetails { @Id @GeneratedValue private Long id; private String username; private String password; @ManyToMany(cascade = {CascadeType.REFRESH},fetch = FetchType.EAGER) private List<Role> roles; … // 下面为实现UserDetails而需要的重写方法! @Override public Collection<? extends GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); for (Role role : roles) { authorities.add( new SimpleGrantedAuthority( role.getName() ) ); } return authorities; } …}此处所创建的 User类继承了 Spring Security的 UserDetails接口,从而成为了一个符合 Security安全的用户,即通过继承 UserDetails,即可实现 Security中相关的安全功能。角色实体 Role:/* * @ www.codesheep.cn * 20190312 /@Entitypublic class Role { @Id @GeneratedValue private Long id; private String name; … // 省略 getter和 setter}创建JWT工具类主要用于对 JWT Token进行各项操作,比如生成Token、验证Token、刷新Token等/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -5625635588908941275L; private static final String CLAIM_KEY_USERNAME = “sub”; private static final String CLAIM_KEY_CREATED = “created”; public String generateToken(UserDetails userDetails) { … } String generateToken(Map<String, Object> claims) { … } public String refreshToken(String token) { … } public Boolean validateToken(String token, UserDetails userDetails) { … } … // 省略部分工具函数}创建Token过滤器,用于每次外部对接口请求时的Token处理/* * @ www.codesheep.cn * 20190312 /@Componentpublic class JwtTokenFilter extends OncePerRequestFilter { @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Override protected void doFilterInternal ( HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader( Const.HEADER_STRING ); if (authHeader != null && authHeader.startsWith( Const.TOKEN_PREFIX )) { final String authToken = authHeader.substring( Const.TOKEN_PREFIX.length() ); String username = jwtTokenUtil.getUsernameFromToken(authToken); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails( request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); }}Service业务编写主要包括用户登录和注册两个主要的业务public interface AuthService { User register( User userToAdd ); String login( String username, String password );}/* * @ www.codesheep.cn * 20190312 /@Servicepublic class AuthServiceImpl implements AuthService { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private UserRepository userRepository; // 登录 @Override public String login( String username, String password ) { UsernamePasswordAuthenticationToken upToken = new UsernamePasswordAuthenticationToken( username, password ); final Authentication authentication = authenticationManager.authenticate(upToken); SecurityContextHolder.getContext().setAuthentication(authentication); final UserDetails userDetails = userDetailsService.loadUserByUsername( username ); final String token = jwtTokenUtil.generateToken(userDetails); return token; } // 注册 @Override public User register( User userToAdd ) { final String username = userToAdd.getUsername(); if( userRepository.findByUsername(username)!=null ) { return null; } BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); final String rawPassword = userToAdd.getPassword(); userToAdd.setPassword( encoder.encode(rawPassword) ); return userRepository.save(userToAdd); }}Spring Security配置类编写(非常重要)这是一个高度综合的配置类,主要是通过重写 WebSecurityConfigurerAdapter 的部分 configure配置,来实现用户自定义的部分。/* * @ www.codesheep.cn * 20190312 /@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Bean public JwtTokenFilter authenticationTokenFilterBean() throws Exception { return new JwtTokenFilter(); } @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure( AuthenticationManagerBuilder auth ) throws Exception { auth.userDetailsService( userService ).passwordEncoder( new BCryptPasswordEncoder() ); } @Override protected void configure( HttpSecurity httpSecurity ) throws Exception { httpSecurity.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(HttpMethod.OPTIONS, “/”).permitAll() // OPTIONS请求全部放行 .antMatchers(HttpMethod.POST, “/authentication/”).permitAll() //登录和注册的接口放行,其他接口全部接受验证 .antMatchers(HttpMethod.POST).authenticated() .antMatchers(HttpMethod.PUT).authenticated() .antMatchers(HttpMethod.DELETE).authenticated() .antMatchers(HttpMethod.GET).authenticated(); // 使用前文自定义的 Token过滤器 httpSecurity .addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class); httpSecurity.headers().cacheControl(); }}编写测试 Controller登录和注册的 Controller:/* * @ www.codesheep.cn * 20190312 /@RestControllerpublic class JwtAuthController { @Autowired private AuthService authService; // 登录 @RequestMapping(value = “/authentication/login”, method = RequestMethod.POST) public String createToken( String username,String password ) throws AuthenticationException { return authService.login( username, password ); // 登录成功会返回JWT Token给用户 } // 注册 @RequestMapping(value = “/authentication/register”, method = RequestMethod.POST) public User register( @RequestBody User addedUser ) throws AuthenticationException { return authService.register(addedUser); }}再编写一个测试权限的 Controller:/* * @ www.codesheep.cn * 20190312 */@RestControllerpublic class TestController { // 测试普通权限 @PreAuthorize(“hasAuthority(‘ROLE_NORMAL’)”) @RequestMapping( value="/normal/test", method = RequestMethod.GET ) public String test1() { return “ROLE_NORMAL /normal/test接口调用成功!”; } // 测试管理员权限 @PreAuthorize(“hasAuthority(‘ROLE_ADMIN’)”) @RequestMapping( value = “/admin/test”, method = RequestMethod.GET ) public String test2() { return “ROLE_ADMIN /admin/test接口调用成功!”; }}这里给出两个测试接口用于测试权限相关问题,其中接口 /normal/test需要用户具备普通角色(ROLE_NORMAL)即可访问,而接口/admin/test则需要用户具备管理员角色(ROLE_ADMIN)才可以访问。接下来启动工程,实验测试看看效果—实验验证在文章开头我们即在用户表 user中插入了一条用户名为 codesheep的记录,并在用户-角色表 user_roles中给用户 codesheep分配了普通角色(ROLE_NORMAL)和管理员角色(ROLE_ADMIN)接下来进行用户登录,并获得后台向用户颁发的JWT Token接下来访问权限测试接口不带 Token直接访问需要普通角色(ROLE_NORMAL)的接口 /normal/test会直接提示访问不通:而带 Token访问需要普通角色(ROLE_NORMAL)的接口 /normal/test才会调用成功:同理由于目前用户具备管理员角色,因此访问需要管理员角色(ROLE_ADMIN)的接口 /admin/test也能成功:接下里我们从用户-角色表里将用户codesheep的管理员权限删除掉,再访问接口 /admin/test,会发现由于没有权限,访问被拒绝了:经过一系列的实验过程,也达到了我们的预期!写在最后本文涉及的东西还是蛮多的,最后我们也将本文的实验源码放在 Github上,需要的可以自取:源码下载地址由于能力有限,若有错误或者不当之处,还请大家批评指正,一起学习交流!My Personal Blog:CodeSheep 程序羊 ...

March 14, 2019 · 3 min · jiezi

Swoft 系列教程:(2)认证服务及组件

Swoft 提供了一整套认证服务组件,基本做到了配置后开箱即用。用户只需根据自身业务实现相应的登录认证逻辑,框架认证组件会调用你的登录业务进行token的签发,而后的请求中token解析、合法性验证也都由框架提供,同时框架开放了token权限认证接口给用户,我们需根据自身业务实现token对当前访问资源权限的认证。下面我们详细讲一下 jwt 的签发及验证、访问控制的流程。token 签发token 签发的基本流程为请求用户登录认证服务,认证通过则签发token。Swoft 的认证组件为我们完成了token签发工作,同时 Swoft 约定了一个Swoft\Auth\Mapping\AuthManagerInterface::login方法作为用户的认证业务的入口。使用到的组件及服务:#认证组件服务,使用此接口类名作为服务名注册到框架服务中Swoft\Auth\Mapping\AuthManagerInterface::class#框架认证组件的具体实现者 token 的签发、合法校验、解析Swoft\Auth\AuthManager#token的会话载体 存储着token的信息Swoft\Auth\Bean\AuthSession#约定用户的认证业务需实现返回Swoft\Auth\Bean\AuthResult的login方法和bool的authenticate的方法Swoft\Auth\Mapping\AccountTypeInterface#用于签发token的必要数据载体 iss/sub/iat/exp/data 传递给 Swoft\Auth\AuthManager 签发 tokenSwoft\Auth\Bean\AuthResult配置项:config/properties/app.php设定auth模式jwtreturn [ … ‘auth’ => [ ‘jwt’ => [ ‘algorithm’ => ‘HS256’, ‘secret’ => ‘big_cat’ ], ] …];config/beans/base.php为\Swoft\Auth\Mapping\AuthManagerInterface::class服务绑定具体的服务提供者return [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];App\Models\Logic\AuthLogic实现用户业务的认证,以 Swoft\Auth\Mapping\AccountTypeInterface 接口的约定实现了 login/authenticate方法。login方法返回Swoft\Auth\Bean\AuthResult对象,存储用于jwt签发的凭证:setIdentity 对应 sub,即jwt的签发对象,一般使用uid即可setExtendedData 对应 payload, 即jwt的载荷,存储一些非敏感信息即可authenticate方法签发时用不到,主要在验证请求的token合法性时用到,即检测jwt的sub是否为本平台合法用户<?phpnamespace App\Models\Logic;use Swoft\Auth\Bean\AuthResult;use Swoft\Auth\Mapping\AccountTypeInterface;class AuthLogic implements AccountTypeInterface{ /** * 用户登录认证 需返回 AuthResult 对象 * 返回 Swoft\Auth\Bean\AuthResult 对象 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param array $data * @return AuthResult / public function login(array $data): AuthResult { $account = $data[‘account’]; $password = $data[‘password’]; $user = $this->userDao->getByConditions([‘account’ => $account]); $authResult = new AuthResult(); // 用户验证成功则签发token if ($user instanceof User && $this->userDao->verifyPassword($user, $password)) { // authResult 主标识 对应 jwt 中的 sub 字段 $authResult->setIdentity($user->getId()); // authResult 附加数据 jwt 的 payload $authResult->setExtendedData([self::ID => $user->getId()]); } return $authResult; } /* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool / public function authenticate(string $identity): bool { return $this->userDao->exists($identity); }}Swoft\Auth\AuthManager::login 要求传入用户业务的认证类,及相应的认证字段,根据返回Swoft\Auth\Bean\AuthResult对象判断登录认证是否成功,成功则签发token,返回Swoft\Auth\Bean\AuthSession对象。App\Services\AuthManagerService用户认证管理服务,继承框架Swoft\Auth\AuthManager做定制扩展。比如我们这里实现一个auth方法供登录请求调用,auth 方法中则传递用户业务认证模块来验证和签发token,获取token会话数据。<?php/* * 用户认证服务 * User: big_cat * Date: 2018/12/17 0017 * Time: 16:36 /namespace App\Services;use App\Models\Logic\AuthLogic;use Swoft\Redis\Redis;use Swoft\Bean\Annotation\Bean;use Swoft\Bean\Annotation\Inject;use Swoft\Auth\AuthManager;use Swoft\Auth\Bean\AuthSession;use Swoft\Auth\Mapping\AuthManagerInterface;/* * @Bean() * @package App\Services /class AuthManagerService extends AuthManager implements AuthManagerInterface{ /* * 缓存类 * @var string / protected $cacheClass = Redis::class; /* * jwt 具有自包含的特性 能自己描述自身何时过期 但只能一次性签发 * 用户主动注销后 jwt 并不能立即失效 所以我们可以设定一个 jwt 键名的 ttl * 这里使用是否 cacheEnable 来决定是否做二次验证 * 当获取token并解析后,token 的算法层是正确的 但如果 redis 中的 jwt 键名已经过期 * 则可认为用户主动注销了 jwt,则依然认为 jwt 非法 * 所以我们需要在用户主动注销时,更新 redis 中的 jwt 键名为立即失效 * 同时对 token 刷新进行验证 保证当前用户只有一个合法 token 刷新后前 token 立即失效 * @var bool 开启缓存 / protected $cacheEnable = true; // token 有效期 7 天 protected $sessionDuration = 86400 * 7; /* * 定义登录认证方法 调用 Swoft的AuthManager@login 方法进行登录认证 签发token * @param string $account * @param string $password * @return AuthSession / public function auth(string $account, string $password): AuthSession { // AuthLogic 需实现 AccountTypeInterface 接口的 login/authenticate 方法 return $this->login(AuthLogic::class, [ ‘account’ => $account, ‘password’ => $password ]); }}App\Controllers\AuthController处理用户的登录请求<?php/* * Created by PhpStorm. * User: big_cat * Date: 2018/12/10 0010 * Time: 17:05 /namespace App\Controllers;use App\Services\AuthManagerService;use Swoft\Http\Message\Server\Request;use Swoft\Http\Server\Bean\Annotation\Controller;use Swoft\Http\Server\Bean\Annotation\RequestMapping;use Swoft\Http\Server\Bean\Annotation\RequestMethod;use Swoft\Bean\Annotation\Inject;use Swoft\Bean\Annotation\Strings;use Swoft\Bean\Annotation\ValidatorFrom;/* * 登录认证模块 * @Controller("/v1/auth") * @package App\Controllers /class AuthController{ /* * 用户登录 * @RequestMapping(route=“login”, method={RequestMethod::POST}) * @Strings(from=ValidatorFrom::POST, name=“account”, min=6, max=11, default="", template=“帐号需{min}{max}位,您提交的为{value}”) * @Strings(from=ValidatorFrom::POST, name=“password”, min=6, max=25, default="", template=“密码需{min}{max}位,您提交的为{value}”) * @param Request $request * @return array / public function login(Request $request): array { $account = $request->input(‘account’) ?? $request->json(‘account’); $password = $request->input(‘password’) ?? $request->json(‘password’); // 调用认证服务 - 登录&签发token $session = $this->authManagerService->auth($account, $password); // 获取需要的jwt信息 $data_token = [ ’token’ => $session->getToken(), ’expired_at’ => $session->getExpirationTime() ]; return [ “err” => 0, “msg” => ‘success’, “data” => $data_token ]; }}POST /v1/auth/login 的结果{ “err”: 0, “msg”: “success”, “data”: { “token”: “eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJBcHBcXE1vZGVsc1xcTG9naWNcXEF1dGhMb2dpYyIsInN1YiI6IjgwIiwiaWF0IjoxNTUxNjAyOTk4LCJleHAiOjE1NTIyMDc3OTgsImRhdGEiOnsidWlkIjo4MH19.u2g5yU9ir1-ETVehLFIIZZgtW7u9aOvH2cndMsIY98Y”, “expired_at”: 1552207798 }}这里提及一下为什么要提供在服务端缓存token的选项$cacheEnable。普通的token不像jwt具有自我描述的特性,我们为维护token的有效期只能在服务端缓存其有效期,防止过期失效的token被滥用。jwt可以自我描述过期时间,为什么也要缓存呢?因为jwt自身的描述是只读的,即我们无法让jwt提前过期失效,如果用户退出登录,则销毁token是个不错的安全开发习惯,所以只有在服务端也维护了一份jwt的过期时间,用户退出时过期此token,那么就可以自由控制jwt的过期时间。/* * @param string $token * @return bool /public function authenticateToken(string $token): bool{ … // 如果开启了服务端缓存选项 则验证token是否过期 可变向控制jwt的有效期 if ($this->cacheEnable === true) { try { $cache = $this->getCacheClient() ->get($this->getCacheKey($session->getIdentity(), $session->getExtendedData())); if (! $cache || $cache !== $token) { throw new AuthException(ErrorCode::AUTH_TOKEN_INVALID); } } catch (InvalidArgumentException $e) { $err = sprintf(‘Identity : %s ,err : %s’, $session->getIdentity(), $e->getMessage()); throw new AuthException(ErrorCode::POST_DATA_NOT_PROVIDED, $err); } } $this->setSession($session); return true;}token 解析、验证token的解析及合法性验证实现流程,注意只是验证token的合法性,即签名是否正确,签发者,签发对象是否合法,是否过期。并未对 token 的访问权限做认证。使用到的组件及服务:#调用token拦截服务尝试获取token,并调用token管理服务做解析及合法性验证Swoft\Auth\Middleware\AuthMiddleware#token拦截服务``Swoft\Auth\Mapping\AuthorizationParserInterface::class#token拦截服务提供者,根据token类型调用相应的token解析器``Swoft\Auth\Parser\AuthorizationHeaderParser#token管理服务,由token管理服务提供者提供基础服务,被token解析器调用Swoft\Auth\Mapping\AuthManagerInterface::class#token管理服务提供者,负责签发、解析、合法性验证Swoft\Auth\AuthManagerSwoft\Auth\Middleware\AuthMiddleware负责拦截请求并调用token解析及验证服务。会尝试获取请求头中的Authorization字段值,根据类型Basic/Bearer来选择相应的权限认证服务组件对token做合法性的校验并生成token会话。但并不涉及业务访问权限ACL的验证,即只保证某个token 是本平台合法签发的,不保证此token对当前资源有合法的访问权限。如果Authorization为空的话则视为普通请求。执行流程:Swoft\Auth\Middleware\AuthMiddleware调用 Swoft\Auth\Mapping\AuthorizationParserInterface::class 服务,服务具体由 Swoft\Auth\Parser\AuthorizationHeaderParser实现。服务AuthorizationHeaderParser尝试获取请求头中的Authorization字段值,如果获取到token,则根据token的类型:BasicorBearer来调用具体的解析器。Basic的解析器为`Swoft\Auth\Parser\Handler::BasicAuthHandler,Bearer的解析器为 Swoft\Auth\Parser\Handler::BearerTokenHandler,下面我们具体以Bearer模式的jwt为示例。在获取到类型为Bearer的token后,BearerTokenHandler将会调用Swoft\Auth\Mapping\AuthManagerInterface::class服务的authenticateToken方法来对token进行合法性的校验和解析,即判断此token的签名是否合法,签发者是否合法,签发对象是否合法(注意:调用了App\Models\Logic\AuthLogic::authenticate方法验证),是否过期等。token解析验证非法,则抛出异常中断请求处理。token解析验证合法,则将payload载入本次会话并继续执行。所以我们可以将此中间件注册到全局,请求携带token则解析验证,不携带token则视为普通请求。#config/beans/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ … \Swoft\Auth\Middleware\AuthMiddleware::class ], … ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ],];<?phpnamespace AppModelsLogic;use SwoftAuthBeanAuthResult;use SwoftAuthMappingAccountTypeInterface;class AuthLogic implements AccountTypeInterface{…/* * 验证签发对象是否合法 这里我们简单验证签发对象是否为本平台用户 * $identity 即 jwt 的 sub 字段 * @override Swoft\Auth\Mapping\AccountTypeInterface * @param string $identity token sub 字段 * @return bool /public function authenticate(string $identity): bool{ return $this->userDao->exists($identity);}}acl鉴权token 虽然经过了合法性验证,只能说明token是本平台签发的,还无法判断此token是否有权访问当前业务资源,所以我们还要引入Acl认证。使用到的组件及服务:#Acl认证中间件Swoft\Auth\Middleware\AclMiddleware#用户业务权限auth服务Swoft\Auth\Mapping\AuthServiceInterface::class#token会话访问组件Swoft\Auth\AuthUserServiceSwoft\Auth\Middleware\AclMiddleware中间件会调用Swoft\Auth\Mapping\AuthServiceInterface::class服务,此服务主要用于Acl认证,即验证当前请求是否携带了合法token,及token是否对当前资源有访问权限。Swoft\Auth\Mapping\AuthServiceInterface::class服务由框架的Swoft\Auth\AuthUserService组件实现获取token会话的部分功能,auth方法则交由用户层重写,所以我们需继承Swoft\Auth\AuthUserService并根据自身业务需求实现auth方法。在继承了Swoft\Auth\AuthUserService的用户业务认证组件中,我们可以尝试获取token会话的签发对象及payload数据:getUserIdentity/getUserExtendData。然后在auth方法中判断当前请求是否有token会话及是否对当前资源有访问权限,来决定返回true or false给AclMiddleware中间件。AclMiddleware中间件获取到用户业务下的auth为false(请求没有携带合法token 401 或无权访问当前资源 403),则终端请求处理。AclMiddleware中间件获取到在用户业务下的auth为true,则说明请求携带合法token,且token对当前资源有权访问,继续请求处理。config/bean/base.phpreturn [ ‘serverDispatcher’ => [ ‘middlewares’ => [ …. //系统token解析中间件 \Swoft\Auth\Middleware\AuthMiddleware::class, … ] ], // token签发及合法性验证服务 \Swoft\Auth\Mapping\AuthManagerInterface::class => [ ‘class’ => \App\Services\AuthManagerService::class ], // Acl用户资源权限认证服务 \Swoft\Auth\Mapping\AuthServiceInterface::class => [ ‘class’ => \App\Services\AclAuthService::class, ‘userLogic’ => ‘${’ . \App\Models\Logic\UserLogic::class . ‘}’ // 注入UserLogicBean ],];App\Services\AclAuthService对token做Acl鉴权。<?phpnamespace App\Services;use Swoft\Auth\AuthUserService;use Swoft\Auth\Mapping\AuthServiceInterface;use Psr\Http\Message\ServerRequestInterface;/* * Bean 因在 config/beans/base.php 中已经以参数配置的方式注册,故此处不能再使用Bean注解声明 * Class AclAuthService * @package App\Services /class AclAuthService extends AuthUserService implements AuthServiceInterface{ /* * 用户逻辑模块 * 因本模块是以参数配置的方式注入到系统服务的 * 所以其相关依赖也需要使用参数配置方式注入 无法使用Inject注解声明 * @var App\Models\Logic\UserLogic / protected $userLogic; /* * 配合 AclMiddleware 中间件 验证用户请求是否合法 * true AclMiddleware 通过 *false AclMiddleware throw AuthException * @override AuthUserService * @param string $requestHandler * @param ServerRequestInterface $request * @return bool */ public function auth(string $requestHandler, ServerRequestInterface $request): bool { // 签发对象标识 $sub = $this->getUserIdentity(); // token载荷 $payload = $this->getUserExtendData(); // 验证当前token是否有权访问业务资源 aclAuth为自己的认证逻辑 if ($this->aclAuth($sub, $payload)) { return true; } return false; }} ...

March 5, 2019 · 4 min · jiezi

InvalidClaimException: The Token can't be used before

jwt token错误linux服务器上最近使用jwt token的时候遇到了一个奇怪的问题:InvalidClaimException: The Token can’t be used before 某个时间测试库完全没问题,正式库只使用一台也没问题,但是一旦启用多台服务器就会报这个错误。经分析发现,多台正式服务器的系统时间是不一样的,有一定的差值。生成token的服务器比使用token的服务器时间快几十秒,生成token后,请求分发到时间慢的服务器上导致时间还没到token的开始时间,才报这个错误。同步服务器时间: nptdate -u ntp1.aliyun.com注意123端口必须可以使用或者通过date设置系统时间,具体的可以百度。

February 28, 2019 · 1 min · jiezi

219. 单页应用 会话管理(session、cookie、jwt)

原文链接:https://github.com/ly525/blog…关键字:http-only, cookie,sessionid, vue-router, react-router, 安全,localStorage, jwt需求描述内部管理平台,需要用户登录之后才能访问。现在将 该平台地址(www.xxx.com/home) 直接发给新来的运营同学前端需要检测该用户是否已登录,如果未登录,则将其 redirect 到登录页面因为该页面为单页应用,路由跳转不涉及后端的 302 跳转,使用前端路由跳转实现思路实现代码// 以 vue-router 为例// 登录中间验证,页面需要登录而没有登录的情况直接跳转登录router.beforeEach((to, from, next) => { const hasToken = document.cookie.includes(‘sessionid’); // 如果采用 jwt,则同样 hasToken = localStorage.jwt const pathNeedAuth = to.matched.some(record => record.meta.requiresAuth); // 用户本地没有后端返回的 cookie 数据 && 前往的页面需要权限 // if (pathNeedAuth && !hasToken ) { next({ path: ‘/login’, query: { redirect: to.fullPath }, }); } else if (hasToken && to.name === ’login’) { // 已登录 && 前往登录页面, 则不被允许,会重定向到首页 next({ path: ‘/’, }); } else { next(); }});应该在进入任何页面之前,判断:该页面是否需要权限才能访问:登录、注册页面不需要权限用户是否已经登录:本地 cookie (或者 localStorage)包含 session 相关信息 Cookie: csrftoken=YaHb…; sessionid=v40ld3x….如果 A页面需要权限 且 本地 cookie中包含了 sessionid 字段,则允许访问A页面,否则跳转到登录页面备注:sessionid 该字段由用户在登录之后,由后端框架通过 response.setCookie 写入前端 ,因此该字段需要和后端同学确认需要后端同学在 response header 中配置cookie中该字段的 http-only属性,允许前端读取 cookie。否则前端通过 document.cookie 读取到的 cookie 将不包含 sessionid 字段这个时候,可能会存在 js 读取cookie 导致不安全的情况,后端同学可以把 cookie 中的某个字段设置为 允许读取,其他 cookie 设置不允许读取,这样即使被第三方不安全脚本获取,也无法产生负面影响。如果用户已登录 && 在浏览器中输入了登录页地址,则将其重定向到首页更多说明这样做,前端就不必再向后端发起 API 做权限鉴定了(后端返回401,前端跳转到 401),减少不必要的API 请求,特别是如果在API响应时间过长的情况下,体验不太友好。用户修改 cookie,伪造 sessionid这样的话,前端就无能为力了,前端鉴权此时认为该用户合法。此时访问首页,将会调用获取数据的API。浏览器会将 用户伪造的 sessionid 带给后端,这时候就需要后端对 sessionid 进行较验了。比如校验前端带来的 sessionid 与数据库中的 sessionid 是否一致用户伪造的数据 sessionid 和 后端数据库中 sessionid 的概率 非常非常低,可以忽略不计,因为 sessionid 的位数一般在 32 位以上,因为里面包含了字母和数字,也就由 32 ^ 36 种可能结论:伪造没有意义,即使用户可以看到页面的样子,但是看不到数据采用 localStorage 而不是 sessionStorage 的原因(SessionStorage 失效场景)原因:sessionStorage 无法跨 Tab 共享用户在新打开一个 Tab,输入已经登录之后的某个页面通过target="_blank" 来打开新页面的时候,会导致会话失效在当前页面执行 Duplicate (复制 Tab),sessionStorage 失效 ...

February 20, 2019 · 1 min · jiezi

JWT验证

JWT(Json Web Token):是目前最流行的跨域身份验证解决方案。此前我们使用的身份验证方式都是基于Session:这种方式并没有什么不妥,但其实这里有三个缺点:Session一般存储在redis中,而redis数据保存在内存中,随着用户的增多,内存消耗太大。扩展性不好,用户每次验证都需要请求session服务器,增大了负载均衡能力,应用扩展受限。因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。所以,我们需要一种既能实现相同要求并且还要比session存储更有效的身份验证方式。JWT通过一种加密的方式,将加密后的数据保存返回给用户本地进行保存,我们称为token数据。其数据由三部分组成:1、header声明类型和加密的算法:{ ’typ’: ‘JWT’, #固定值 ‘alg’: ‘HS256’ #加密算法}2、payload负载这是有效信息的存放地方,其分为三部分:标准中注册的声明、公共声明、私有声明(用户信息)标准中的注册声明(有需要在使用,不强制使用):iss: jwt签发者sub: jwt所面向的用户aud: 接收jwt的一方exp: jwt的过期时间,这个过期时间必须要大于签发时间nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。公共声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.私有声明:{ “name”: “jim”, “id”: “111111”, “admin”: true}3、signature签名需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。由于base64是对称加密算法,所以可以轻松解密:因此我们在负载部分不要将私密信息放置在里面,只需要把能验证唯一的标识信息添加就可以了。有关base64,请参考:https://www.liaoxuefeng.com/w…由于目前在学习DRF,所以我介绍一下怎样在DRF项目中使用JWT进行身份验证:安装djangorestframework-jwt:pip install djangorestframework-jwt添加jwt认证类:REST_FRAMEWORK = { ‘DEFAULT_PERMISSION_CLASSES’: ( ‘rest_framework.permissions.IsAuthenticated’, ), ‘DEFAULT_AUTHENTICATION_CLASSES’: ( ‘rest_framework_jwt.authentication.JSONWebTokenAuthentication’, ‘rest_framework.authentication.SessionAuthentication’, ‘rest_framework.authentication.BasicAuthentication’, ),}添加jwt路由用于生成token:from rest_framework_jwt.views import obtain_jwt_tokenurlpatterns = [ url(r’^自己的路由/’, obtain_jwt_token),]然后我们就可以通过自己添加的路由并通过post添加username和password来获取到token了,在进行访问页面的时候我们只需要在请求头中添加一个:Authorization: JWT <your_token>,就可以得到验证了。本文参考:https://lion1ou.win/2017/01/18/

January 27, 2019 · 1 min · jiezi

Java安全验证之JWT实践

先给出JWT的官方文档什么是JWT?JSON Web Token(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,用于在各方之间作为JSON对象安全地传输信息。什么时候应该使用JWT?授权:这是使用JWT的最常见方式。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。Single Sign On是一种现在广泛使用JWT的功能,因为它的开销很小,并且能够在不同的域中轻松使用。信息交换:JSON Web令牌是在各方之间安全传输信息的好方法。因为JWT可以通过签名(使用公钥/私钥对 )核实发送人的身份。此外,由于使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。JWT令牌结构JWT令牌由Header、Payload、Signature三部分组成,每部分之间用点号分隔,通常的形式为xxxxx.yyyyy.zzzzz,下面分别对每部分做详细介绍。Header(头)Header通常由两部分组成:令牌的类型,即JWT,以及使用的签名算法,例如HMAC SHA256或RSA。{ “alg”: “HS256”, “typ”: “JWT”}这个JSON被编码为Base64Url,形成JWT的第一部分。Payload(有效载荷)JWT的第二部分是有效载荷,其中包含声明( claims)。声明包含实体(通常是用户)和其他自定义信息。声明有三种类型:registered, public和private claims 。registered claims :这是一组预定义声明,不是强制性的,但建议使用,以提供一组有用的,可互操作的声明。其中包括: iss(发行人), exp(到期时间),sub(主题), aud(受众)。请注意:声明名称只有三个字符,因为JWT需要保持简洁。public claims:这些可以由使用JWT的人随意定义。但为避免冲突,应在 IANA JSON Web Token Register中定义它们,或者将其定义为包含防冲突命名空间的URI。private claims:这些是自定义声明,用于在同意使用这些声明的各方之间共享信息,这些信息既没有注册也没有公开声明。Payload经过Base64Url编码,形成JWT的第二部分。请注意:对于JWT令牌,虽然可以防止被篡改,但任何人都可以读取。除非加密,否则不要将秘密信息放在JWT的有效载荷或头元素中。Signature(签名)Signature由Base64Url加密的Header、Payload再使用Header中指定的算法加密之后再和secret组成。如果要使用HMACSHA256算法,将按以下方式创建签名:HMACSHA256( base64UrlEncode(header) + “.” + base64UrlEncode(payload), secret)Signature用于验证消息在此过程中未被更改,并且,在使用私钥签名的令牌的情况下,它还可以验证JWT的发件人的身份。总结最后将JWT的三部分由点号分隔作为一个字符串,其可以在HTML和HTTP环境中轻松传递,并且比基于XML的标准的Token(如SAML)更加简洁。JWT工作流程在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web Token。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。每当用户想要访问受保护的路由或资源时,用户发送JWT到相应的地址,通常在Authorization标头中。请求头的的内容应如下所示:Authorization: Bearer <token>在某些情况下,这可以是无状态授权机制。服务器的受保护路由将检查Authorization标头中的有效JWT ,如果存在,则允许用户访问受保护资源。如果JWT包含必要的数据,则可以减少查询数据库以进行某些操作的需要,尽管可能并非总是如此。如果在标Authorization头中发送Token,则跨域资源共享(CORS)将不会成为问题,因为它不使用cookie。下图显示了如何获取JWT并用于访问API或资源:应用程序或客户端向授权服务器发送授权请求。授予授权后,授权服务器会向应用程序返回JWT。应用程序使用JWT来访问受保护资源(如API)。请注意:使用JWT时,Token中包含的所有信息都会向用户或其他方公开,即使他们无法更改。所以您不应该在令牌中放置秘密信息。JWT中的JAVA实现JWT的java实现非常多,详细何以查看官方文档。其中常用的有com.auth0.java-jwt和io.jsonwebtoken.jjwt这里我采用 jjwt 作为演示,因为他的Github中的Star比较多。Maven依赖<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.10.5</version></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.10.5</version> <scope>runtime</scope></dependency><dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.10.5</version> <scope>runtime</scope></dependency><!– Uncomment this next dependency if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.60</version> <scope>runtime</scope></dependency>–>创建Token //Sample method to construct a JWT public static String createJWT(String id, String issuer, String subject, long ttlMillis) { //The JWT signature algorithm we will be using to sign the token SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); //We will sign our JWT with our ApiKey secret byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary( APP_ID + APP_SECRET); Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); //Let’s set the JWT Claims JwtBuilder builder = Jwts.builder().setId(id) .setIssuedAt(now) .setSubject(subject) .setIssuer(issuer) .signWith(signatureAlgorithm, signingKey); //if it has been specified, let’s add the expiration if (ttlMillis >= 0) { long expMillis = nowMillis + ttlMillis; Date exp = new Date(expMillis); builder.setExpiration(exp); } //Builds the JWT and serializes it to a compact, URL-safe string return builder.compact(); }解析Token //Sample method to validate and read the JWT public static Claims parseJWT(String jwt) { //This line will throw an exception if it is not a signed JWS (as expected) Claims claims = Jwts.parser() .setSigningKey(DatatypeConverter.parseBase64Binary(APP_ID + APP_SECRET)) .parseClaimsJws(jwt).getBody();// System.out.println(“ID: " + claims.getId());// System.out.println(“Subject: " + claims.getSubject());// System.out.println(“Issuer: " + claims.getIssuer());// System.out.println(“Expiration: " + claims.getExpiration()); return claims; }其中APP_ID和APP_SECRET可以自定义为你想要的任何值,但是不能过于简单。你会发现我们再使用的时候没有设置Header的值,因为jjwt为了我们使用方便会根据使用的签名算法或压缩算法自动设置它们。使用@Testpublic void createJWT(){ String jwt = JWTUtil.createJWT(“1”, “111”, “admin”, JWTUtil.DAY_TTL); System.out.println(jwt);}@Testpublic void parseJWT(){ Claims claims = JWTUtil.parseJWT(“eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxIiwiaWF0IjoxNTQ4Mjk1NjQ0LCJzdWIiOiJhZG1pbiIsImlzcyI6IjExMSIsImV4cCI6MTU0ODMzODg0NH0.WRkyeG3MfVor02Ya4732fgGydXhtkkKSDwbxOIZ2i9Y”); System.out.println(claims);}如果想使用jjwt更复杂的功能或者其他的Java实现可以去他们相应的Github上学习。 ...

January 24, 2019 · 2 min · jiezi

记一次翻译站经历

做这个记录之前,刚完成使用drone作为公司前端项目的持续交付工具的实践,打算写的教程前先把官方文档扒下来做个翻译站。在实践一番后,卡在不能频密调取google翻译这块上,项目无法进行下去。最后觉得经历的过程涉及的内容挺多的所以记录一下同时分享给大家。这次经历涉及以下知识:wget抓取网站使用基于python的翻译工具使用nodejs调取命令行使用nodejs读写文件为express添加jwt基于express实现上传文件在nodejs环境下读取并编辑html文件我们一点点来说。wget抓取网站最初是寻找有什么带可视化的工具来达到目的的,后来在寻找的过程中看到原来wget能实现整站抓取,所以怎样简单怎样来!# 抓取整站wget -r -p -np -k -E http://www.xxx.com# w抓取第一级wget -l 1 -p -np -k http://www.xxx.com-r 递归抓取-k 抓取后修正链接,适合本地浏览-e robots=off 忽略robots协议,强制抓取(流氓抓取)-E 将text/html类型的文档保存为.html的文件使用基于python的翻译工具这个在github上找了几个工具,同时也考虑过使用官方提供的API(微软和google均有提供),最后得出使用soimort/translate-shell(并不想花钱和花时间再看文档上了>w<)这个trans shell工具提供几个翻译源(google, bing, yandex, apertium),不知道为何能用的只有google (!゚゚)。google也很有保证了,问题不大。安装并不复杂,只需要安装gawk,其他在ubuntu系统下默认都有包含的:GNU Awkgawk安装$ sudo apt-get install gawk尝试:$ gawk -f <(curl -Ls git.io/translate) – -shell安装trans本体,官方文档提供三种方式,方式1不知道为何有bug,方式2并不太熟悉,最后选择方式3:$ git clone https://github.com/soimort/translate-shell$ cd translate-shell/$ make$ [sudo] make install使用起来也是简单:$ trans ‘Saluton, Mondo!‘Saluton, Mondo!Hello, World!Translations of Saluton, Mondo![ Esperanto -> English ]Saluton , Hello,Mondo ! World!简短输出方式:$ trans -brief ‘Saluton, Mondo!‘Hello, World!翻译文件:$ trans -b en:zh -i input.txt -o output.txt使用trans调取google translate进行翻译不能频频调用,频频调用之后会令后续请求503,被google限制请求!!使用nodejs调取命令行完成翻译的调研后,感觉本地实现翻译需要安装各种东西,不如做成web应用好了。用express快速建立网站应用,关于在nodejs下调用命令其实是没啥头绪的,搜索得出结果发现可以使用Child Process模块实现:const util = require(‘util’)const exec = util.promisify(require(‘child_process’).exec)exec(trans -V) .then(({stdout, stderr}) => { if(stdout.indexOf(“not installed”) > -1) return Error(stdout) }) .then(()=>exec(trans -b ${language} -i ${input} -o ${output})) .then(({stdout, stderr})=>{ return { input, output, message: stdout } })使用nodejs读写文件这个就不详细说明了,简单列一下用到的函数:fs.readFileSync(path)同步读取文件例子:const data = fs.readFileSync(’./test.txt’)console.log(data.toString())// testing!fs.writeFileSync(path, data) 同步写入文件例子:try{ fs.writeFileSync(’./test.txt’, ’testing!!’)}catch(e){ console.error(e)}fs.unlinkSync(path) 同步删除文件例子:try{ fs.unlinkSync(’./test.txt’)}catch(e){ console.error(e)}为express添加jwt先说一下jwt,全名叫JSON Web Tokens,是一种开放的,行业标准的RFC 7519方法,用于表示两端之间的应用安全。RFC是由Internet Society(ISOC)赞助发行的互联网通信协议规范,包含各种各样的协议,同时包含互联网新开发的协议及发展中所有的记录。jwt这种实现已经成为互联网通讯安全标准,那么在express怎样实现?首先安装下面两个包:auth0/node-jsonwebtokenauth0/express-jwtnpm i -S express-jwt jsonwebtoken使用:const { router } = require(’express’)const decode_jwt = require(’express-jwt’)const jwt = require(‘jsonwebtoken’)const secret = “your-secret” //盐// 登录router.get(’/login’, function(req, res, next) { /+[登录逻辑]…/ const token = jwt.sign(user, secret) res.status(200).send({ user, token })})//受限的接口router.get(’/user/star’, decode_jwt({secret: secret}), (req, res)=>{ const { user } = req const stars = [] /+[获取用户star列表]/ res.status(200).send(stars)})解释一下,jsonwebtoken包为加密作用, secret作为盐用来混淆内容(出于安全是不能对客户端公开),然后经过express-jwt解密处理http header里带有的authorization: Bearer [token]中的token来获得user信息。这样在/user/star接口中就能获取到用户资料做后续的业务处理了。基于express实现上传文件忘了说明这里提及的express版本为4,那么在新版的express 4文档中提及了这么一段关于上传文件的处理说明:In Express 4, req.files is no longer available on the req object by default. To access uploaded files on the req.files object, use multipart-handling middleware like busboy, multer, formidable, multiparty, connect-multiparty, or pez.意思是:express 4 版本req.files字段不在有效,需要使用上面提及的中间件提供支持才能实现读取上传来的文件。看了一番文档,最后选择了multer。下面讲一下如何使用:安装npm i -S multer使用const multer = require(‘multer’)const limits = { fieldSize: 102410243 }const extname = ‘html’//创建本地储存const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, ‘./uploads’); }, //储存文件时自定义文件名称 filename: function (req, file, cb) { cb(null, file.fieldname + ‘-’ + Date.now()); }})//创建上传处理const uploader = require(‘multer’)({ storage, limits, fileFilter(req, file, cb){ if(path.extname(file.originalname) === .${extname}) cb(null, true) else cb(new Error(upload file extname must be ${extname})) }})/** * 上传接口 * 只接受http头是content-type: multipart/form-data的数据 * 这里设定获取字段是file的内容储存成文件来完成文件上传工作 /router.post(’/trans_on_upload’, uploader.single(‘file’), (req, res)=>{ const { file } = req const fileData = fs.readFileSync(file.path) console.log(fileData.toString()) res.status(200)})multer接受多种文件上传方式:uploader.single(fieldname) 接受一个以 fieldname 命名的文件。这个文件的信息保存在 req.fileuploader.array(fieldname[, maxCount]) 接受一个以 fieldname 命名的文件数组。可以配置 maxCount 来限制上传的最大数量。这些文件的信息保存在 req.files。uploader.fields(fields) 接受指定 fields 的混合文件。这些文件的信息保存在 req.files。fields 应该是一个对象数组,应该具有 name 和可选的 maxCount 属性。例子:[ { name: ‘avatar’, maxCount: 1 }, { name: ‘gallery’, maxCount: 8 }]uploader.none() 只接受文本域。如果任何文件上传到这个模式,将发生 “LIMIT_UNEXPECTED_FILE” 错误。这和 upload.fields([]) 的效果一样。uploader.any() 接受一切上传的文件。文件数组将保存在 req.files。警告: 确保你总是处理了用户的文件上传。 永远不要将 multer 作为全局中间件使用,因为恶意用户可以上传文件到一个你没有预料到的路由,应该只在你需要处理上传文件的路由上使用。multer使用起来有一定限制,并不是所有项目合适使用,所以不做深入说明。在nodejs环境下读取并编辑html文件这里处理的流程是使用fs.readFileSync(path)读取html文件内容后,希望能以dom方式编辑内容,使用jsdom/jsdom能像在浏览器一样的方式处理DOM,是非常好用的工具。比如我的需求是获取所有TextNode提取内容进行翻译并替换原来内容,最后导出html内容:const { JSDOM } = require(“jsdom”)const { minify } = require(“html-minifier”)//递归获得所有TextNodeconst getAllTextNode = (node)=>{ var all = []; for (node=node.firstChild;node;node=node.nextSibling){ const { parentNode } = node if (node.nodeType==3){ all.push(node) } else all = all.concat(getAllTextNode(node)); } return all;}const html = “”/+[获取html内容]**/const vbrows = new JSDOM(minify(html, { collapseWhitespace: true}))const { document } = vbrows.windowconst textNodes = getAllTextNode(document.body)textNodes.forEach(textNodes=>{ const transStr = textNodes.textContent /翻译处理/ textNodes.textContent = transStr})//翻译结果console.log(’trans result’, vbrows.serialize())总结完成一个应用涉及的范围其实挺广的,内容如果再深入讨论应该能写更多内容吧。由于手上还有其他工作,只能大致记录关键词和使用的方式,原理方向等有时间再深入研究。 ...

January 17, 2019 · 2 min · jiezi

彻底弄懂session,cookie,token

session,cookie和token究竟是什么简述我在写之前看了很多篇session,cookie的文章,有的人说先有了cookie,后有了session。也有人说先有session,后有cookie。感觉都没有讲的很清楚,泛泛而谈。希望本篇文章对大家有所帮助注:本文需要读者有cookie,session,token的相关基础知识。http是一个无状态协议什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。这种无状态的的好处是快速。坏处是假如我们想要把www.zhihu.com/login.html和www.zhihu.com/index.html关联起来,必须使用某些手段和工具cookie和session由于http的无状态性,为了使某个域名下的所有网页能够共享某些数据,session和cookie出现了。客户端访问服务器的流程如下首先,客户端会发送一个http请求到服务器端。服务器端接受客户端请求后,建立一个session,并发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。该头部包含了sessionId。Set-Cookie格式如下,具体请看Cookie详解Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]在客户端发起的第二次请求,假如服务器给了set-Cookie,浏览器会自动在请求头中添加cookie服务器接收请求,分解cookie,验证信息,核对成功后返回response给客户端注意cookie只是实现session的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用cookie后还有其他方法存储,比如放在url中现在大多都是Session + Cookie,但是只用session不用cookie,或是只用cookie,不用session在理论上都可以保持会话状态。可是实际中因为多种原因,一般不会单独使用用session只需要在客户端保存一个id,实际上大量数据都是保存在服务端。如果全部用cookie,数据量大的时候客户端是没有那么多空间的。如果只用cookie不用session,那么账户信息全部保存在客户端,一旦被劫持,全部信息都会泄露。并且客户端数据量变大,网络传输的数据量也会变大小结简而言之, session 有如用户信息档案表, 里面包含了用户的认证信息和登录状态等信息. 而 cookie 就是用户通行证tokentoken 也称作令牌,由uid+time+sign[+固定参数]token 的认证方式类似于临时的证书签名, 并且是一种服务端无状态的认证方式, 非常适合于 REST API 的场景. 所谓无状态就是服务端并不会保存身份认证相关的数据。组成uid: 用户唯一身份标识time: 当前时间的时间戳sign: 签名, 使用 hash/encrypt 压缩成定长的十六进制字符串,以防止第三方恶意拼接固定参数(可选): 将一些常用的固定参数加入到 token 中是为了避免重复查库存放token在客户端一般存放于localStorage,cookie,或sessionStorage中。在服务器一般存于数据库中token认证流程token 的认证流程与cookie很相似用户登录,成功后服务器返回Token给客户端。客户端收到数据后保存在客户端客户端再次访问服务器,将token放入headers中服务器端采用filter过滤器校验。校验成功则返回请求数据,校验失败则返回错误码token可以抵抗csrf,cookie+session不行假如用户正在登陆银行网页,该网页未对csrf攻击进行防护。攻击者就可以注入一张图片,该图片src为http://www.bank.com/api/transfer?count=1000&to=Tom。倘若是session+cookie,用户打开网页的时候就已经转给Tom1000元了。因为session一旦建立,当前域页面以及该页面路径以下所有页面都共享cookie。在img请求的瞬间,cookie会被浏览器自动添加到请求头中。但token不同,开发者在每次发起请求时手动将 Token 添加到请求中。即打开页面请求img时,该请求头中没有token分布式情况下的session和token我们已经知道session时有状态的,一般存于服务器内存或硬盘中,当服务器采用分布式或集群时,session就会面对负载均衡问题。负载均衡多服务器的情况,不好确认当前用户是否登录,因为多服务器不共享seesion。这个问题也可以将session存在一个服务器中来解决,但是就不能完全达到负载均衡的效果。当今的几种解决session负载均衡的方法。而token是无状态的,token字符串里就保存了所有的用户信息客户端登陆传递信息给服务端,服务端收到后把用户信息加密(token)传给客户端,客户端将token存放于localStroage等容器中。客户端每次访问都传递token,服务端解密token,就知道这个用户是谁了。通过cpu加解密,服务端就不需要存储session占用存储空间,就很好的解决负载均衡多服务器的问题了。这个方法叫做JWT(Json Web Token)总结session存储于服务器,可以理解为一个状态列表,拥有一个唯一识别符号sessionId,通常存放于cookie中。服务器收到cookie后解析出sessionId,再去session列表中查找,才能找到相应session。依赖cookiecookie类似一个令牌,装有sessionId,存储在客户端,浏览器通常会自动添加。token也类似一个令牌,无状态,用户信息都被加密到token中,服务器收到token后解密就可知道是哪个用户。需要开发者手动添加。jwt只是一个跨域认证的方案参考文章Cookie、Session、Token那点事儿cookie,token验证的区别有了cookie为什么需要sessionCSRF Token的设计是否有其必要性cookie,token,session三者的问题和解决方案负载均衡集群中的session解决方案JWT介绍Json Web Token 入门教程谢谢本文如有错误,欢迎留言讨论

January 9, 2019 · 1 min · jiezi

JWT - just what?

初见JWT,不知所云,赶紧Google(百度)一下,原来是跨域身份验证解决方案。JWT只是缩写,全拼则是 JSON Web Tokens ,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT 原理jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。加密前的用户信息,如:{ “username”: “vist”, “role”: “admin”, “expire”: “2018-12-08 20:20:20”}客户端收到的token:7cd357af816b907f2cc9acbe9c3b4625JWT 结构一个token分为3部分:头部(header)载荷(payload)签名(signature)3个部分用“.”分隔,如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 头部JWT的头部分是一个JSON对象,描述元数据,通常是:{ “typ”: “JWT”, “alg”: “HS256”}typ 为声明类型,指定 “JWT"alg 为加密的算法,默认是 “HS256"也可以是下列中的算法:JWS算法名称描述HS256HMAC256HMAC with SHA-256HS384HMAC384HMAC with SHA-384HS512HMAC512HMAC with SHA-512RS256RSA256RSASSA-PKCS1-v1_5 with SHA-256RS384RSA384RSASSA-PKCS1-v1_5 with SHA-384RS512RSA512RSASSA-PKCS1-v1_5 with SHA-512ES256ECDSA256ECDSA with curve P-256 and SHA-256ES384ECDSA384ECDSA with curve P-384 and SHA-384ES512ECDSA512ECDSA with curve P-521 and SHA-512载荷载荷(payload)是数据的载体,用来存放实际需要传递的数据信息,也是一个JSON对象。JWT官方推荐字段:iss: jwt签发者sub: jwt所面向的用户aud: 接收jwt的一方exp: jwt的过期时间,这个过期时间必须要大于签发时间nbf: 定义在什么时间之前,该jwt都是不可用的.iat: jwt的签发时间jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。也可以使用自定义字段,如:{ “username”: “vist”, “role”: “admin”}签名签名部分是对前两部分(头部,载荷)的签名,防止数据篡改。按下列步骤生成:1、先指定密钥(secret)2、把头部(header)和载荷(payload)信息分别base64转换3、使用头部(header)指定的算法加密最终,签名(signature) = HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret)客户端得到的签名:header.payload.signature也可以对JWT进行再加密。JWT 使用1、服务端根据用户登录状态,将用户信息加密到token中,返给客户端2、客户端收到服务端返回的token,存储在cookie中3、客户端和服务端每次通信都带上token,可以放在http请求头信息中,如:Authorization字段里面4、服务端解密token,验证内容,完成相应逻辑JWT 特点JWT更加简洁,更适合在HTML和HTTP环境中传递JWT适合一次性验证,如:激活邮件JWT适合无状态认证JWT适合服务端CDN分发内容相对于数据库Session查询更加省时JWT默认不加密使用期间不可取消令牌或更改令牌的权限JWT建议使用HTTPS协议来传输代码原文:https://www.bestvist.com/p/62

December 10, 2018 · 1 min · jiezi

使用postman调试jwt开发的接口

我的github博客:https://zgxxx.github.io/上一篇博客文章https://segmentfault.com/a/11… 介绍了laravel使用dingo+jwt开发API的几个步骤,那么在实际操作中,我们需要测试API$api = app(‘Dingo\Api\Routing\Router’);$api->version(‘v1’, [’namespace’ => ‘App\Http\Controllers\V1’], function ($api) { $api->post(‘register’, ‘AuthController@register’); $api->post(’login’, ‘AuthController@login’); $api->post(’logout’, ‘AuthController@logout’); $api->post(‘refresh’, ‘AuthController@refresh’); $api->post(‘me’, ‘AuthController@me’); $api->get(’test’, ‘AuthController@test’);});设置了这几个路由,对应的url类似这样:http://www.yourweb.com/api/me 使用postman来调试这些API。请求API的大致流程我们使用jwt代替session,首先是通过登录(jwt的attempt方法验证账号密码),成功后会返回一个JWT,我们把这个字符串统一叫做token,这个token需要我们客户端保存起来,然后后面需要认证的接口就在请求头里带上这个token,后台验证正确后就会进行下一操作,如果token错误,或者过期就返回401或500错误,拒绝后面的操作。前端可以保存在localStorage,小程序可以 使用wx.setStorageSync保存所以请求头信息Authorization:Bearer + token很重要,但是有个问题,这个token是有一个刷新时间和过期时间的:’ttl’ => env(‘JWT_TTL’, 60),‘refresh_ttl’ => env(‘JWT_REFRESH_TTL’, 20160),refresh_ttl是过期时间,默认14天,很好理解,就像一些网站一样,你好几个月没去登录,你的账号会自动退出登录,因为过期了,需要重新输入账号密码登录。ttl刷新时间默认60分钟,也就是说你拿这个一小时前的token去请求是不行的,会报The token has been blacklisted的错误,意思是说这个旧的token已经被列入黑名单,无法使用token是会被别人盗取的,所以token需要每隔一段时间就更新一次这时候有个问题,每隔一小时就更新,那岂不是要每小时就重新登录一遍来获取新token?当然不需要,我们可以写个中间件来实现无痛刷新token,用户不会察觉我们已经更新了token。<?phpnamespace App\Http\Middleware;use Closure;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;class RefreshToken extends BaseMiddleware{ /** * @author: zhaogx * @param $request * @param Closure $next * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed * @throws JWTException */ public function handle($request, Closure $next) { // 检查此次请求中是否带有 token,如果没有则抛出异常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常 try { // 检测用户的登录状态,如果正常则通过 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException(‘jwt-auth’, ‘未登录’); } catch (TokenExpiredException $exception) { // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中 try { // 刷新用户的 token $token = $this->auth->refresh(); // 使用一次性登录以保证此次请求的成功 \Auth::guard(‘api’)->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[‘sub’]); } catch (JWTException $exception) { // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。 throw new UnauthorizedHttpException(‘jwt-auth’, $exception->getMessage()); } } return $next($request)->withHeaders([ ‘Authorization’=> ‘Bearer ‘.$token, ]); }}一旦中间件检测到token过时了,就自动刷新token,然后在响应头把新的token返回来,我们客户端可以根据响应头是否有’Authorization’来决定是否要替换token在使用postman调试这些API的时候就有个问题,postman又没有前端代码,我怎么及时更新这个token,难道每次请求都要去看响应头,发现Authorization后手动去复制黏贴吗,当然也不需要,postman有个强大的环境变量,其实也是前端js的东西。postman自动刷新请求头token登录后自动获取token首先点击设置环境这个按钮,点击Add按钮添加一个变量,我们设置key值为access_token,接着我们在登录接口的Tests中去赋值这个变量var data = JSON.parse(responseBody); if (data.result.access_token) { tests[“Body has token”] = true; var tokenArray = data.result.access_token.split(" “); postman.setEnvironmentVariable(“access_token”, tokenArray[1]); } else { tests[“Body has token”] = false; } 这段js代码就是获取请求成功后返回的access_token值,将其赋值给postman的环境变量,我们看到请求成功后我们后台返回了一个json,其中就有我们需要的access_token,我们可以再去环境变量那里看看这时候的变量有什么变化可以看到这里的变量access_token已经有值了,就是我们后台返回来的access_token字符串,说明赋值成功接着我们到另一个需要认证的接口测试我们在Authorization类型type选择Bearer Token,在后面Token表单那里打一个’{‘就会自动提示我们设置过的变量{{access_token}}发送请求测试下已经成功了。无痛刷新token那如果token刷新了,经过后台中间件无痛刷新后,会在响应头返回一个新的token(这一次请求用的是旧的token,默认认证通过)现在我们需要在这个接口上直接更新我们的变量access_token(如下图),而不需要去再请求一遍登录接口var authHeader = postman.getResponseHeader(‘Authorization’);if (authHeader){ var tokenArray = authHeader.split(” “); postman.setEnvironmentVariable(“access_token”, tokenArray[1]); tests[“Body has refreshtoken”] = true; } else { tests[“Body has no refreshtoken”] = false; }这段js代码就是将响应头中的Authorization赋值给我们的access_token这是响应头的Authorization,截取了最后面的字符串刷新时间过了后,我们试着再发一次请求,我们可以看到,还是可以访问的,而且请求头里的Authorization已经自动更新过来了用一句话整理大概就是,你需要在哪个接口响应后更新变量,就去这个这个口的Test下写js赋值代码postman.setEnvironmentVariable(“access_token”, token);,只要没错误你就可以在别的地方使用{{access_token}}更新替换了。 ...

November 9, 2018 · 1 min · jiezi

laravel5.5+dingo+JWT开发后台API

dingo api 中文文档: https://www.bookstack.cn/read...Laravel中使用JWT:https://laravel-china.org/art…辅助文章: https://www.jianshu.com/p/62b…参考https://www.jianshu.com/p/62b… 这篇文章基本就能搭建出环境,我使用的版本跟他一样 “dingo/api”: “2.0.0-alpha1”,“tymon/jwt-auth”: “^1.0.0-rc.1”,不知道别的版本有啥大的区别,但是网上找的其他一些文章使用的是旧的版本,jwt封装的东西路径可能不一样,可能会保错,有些文档还说要手动添加TymonJWTAuthProvidersLaravelServiceProvider::class和DingoApiProviderLaravelServiceProvider::class,其实新版本不需要。1. composer.json引入包,执行composer update: “require”: { …… “dingo/api”: “2.0.0-alpha1”, “tymon/jwt-auth”: “^1.0.0-rc.1” },2. 执行下面两个语句自动生成dingo和jwt的配置文件:php artisan vendor:publish –provider=“Dingo\Api\Provider\LaravelServiceProvider”//config文件夹中生成dingo配置文件—> api.phpphp artisan vendor:publish –provider=“Tymon\JWTAuth\Providers\LaravelServiceProvider”//config文件夹中生成dingo配置文件—> jwt.php3. 配置 .env具体配置可参考 文档https://www.bookstack.cn/read… ,我的配置是API_STANDARDS_TREE=vndAPI_PREFIX=apiAPI_VERSION=v1API_DEBUG=trueAPI_SUBTYPE=myapp 还需在命令行执行 php artisan jwt:secret,会在.env自动添加JWT_SECRET,其他若需要,可以到各种的配置文件中看,在.env添加即可4. 关键处理’defaults’ => [ ‘guard’ => ‘web’, ‘passwords’ => ‘users’, ], ‘guards’ => [ ‘web’ => [ ‘driver’ => ‘session’, ‘provider’ => ‘users’, ], ‘api’ => [ ‘driver’ => ‘jwt’, ‘provider’ => ‘users’, ], ],这里需要把api原本的driver => session 改为使用jwt机制,provider对应你要用的用户认证表,一般就是登录注册那张表<?phpnamespace App\Models;use Tymon\JWTAuth\Contracts\JWTSubject;use Illuminate\Notifications\Notifiable;use Illuminate\Foundation\Auth\User as Authenticatable;class User extends Authenticatable implements JWTSubject { use Notifiable; /** * The attributes that are mass assignable. * * @var array / protected $fillable = [ ’name’, ’email’, ‘password’, ‘unionid’ ]; /* * The attributes that should be hidden for arrays. * * @var array / protected $hidden = [ ‘password’, ‘remember_token’, ]; // Rest omitted for brevity /* * Get the identifier that will be stored in the subject claim of the JWT. * * @return mixed / public function getJWTIdentifier() { return $this->getKey(); } /* * Return a key value array, containing any custom claims to be added to the JWT. * * @return array / public function getJWTCustomClaims() { return []; }}5. 设置控制器考虑到可能后面需要开发不同版本api,所以在app/Http/Controller下建立了V1,V2目录,根据你自己的需求来,只要写好命名空间就ok <?php/* * Date: 17/10/12 * Time: 01:07 /namespace App\Http\Controllers\V1;use App\Http\Controllers\Controller;use Illuminate\Http\Request;use Illuminate\Support\Facades\Auth;use Validator;use App\User;class AuthController extends Controller{ protected $guard = ‘api’;//设置使用guard为api选项验证,请查看config/auth.php的guards设置项,重要! /* * Create a new AuthController instance. * * @return void / public function __construct() { $this->middleware(‘refresh’, [’except’ => [’login’,‘register’]]); } public function test(){ echo “test!!”; } public function register(Request $request) { $rules = [ ’name’ => [‘required’], ’email’ => [‘required’], ‘password’ => [‘required’, ‘min:6’, ‘max:16’], ]; $payload = $request->only(’name’, ’email’, ‘password’); $validator = Validator::make($payload, $rules); // 验证格式 if ($validator->fails()) { return $this->response->array([’error’ => $validator->errors()]); } // 创建用户 $result = User::create([ ’name’ => $payload[’name’], ’email’ => $payload[’email’], ‘password’ => bcrypt($payload[‘password’]), ]); if ($result) { return $this->response->array([‘success’ => ‘创建用户成功’]); } else { return $this->response->array([’error’ => ‘创建用户失败’]); } } /* * Get a JWT token via given credentials. * * @param \Illuminate\Http\Request $request * * @return \Illuminate\Http\JsonResponse / public function login(Request $request) { $credentials = $request->only(’email’, ‘password’); if ($token = $this->guard()->attempt($credentials)) { return $this->respondWithToken($token); } return $this->response->errorUnauthorized(‘登录失败’); } /* * Get the authenticated User * * @return \Illuminate\Http\JsonResponse / public function me() { //return response()->json($this->guard()->user()); return $this->response->array($this->guard()->user()); } /* * Log the user out (Invalidate the token) * * @return \Illuminate\Http\JsonResponse / public function logout() { $this->guard()->logout(); //return response()->json([‘message’ => ‘Successfully logged out’]); return $this->response->array([‘message’ => ‘退出成功’]); } /* * Refresh a token. * * @return \Illuminate\Http\JsonResponse / public function refresh() { return $this->respondWithToken($this->guard()->refresh()); } /* * Get the token array structure. * * @param string $token * * @return \Illuminate\Http\JsonResponse / protected function respondWithToken($token) { return response()->json([ ‘access_token’ => $token, ’token_type’ => ‘bearer’, ’expires_in’ => $this->guard()->factory()->getTTL() * 60 ]); } /* * Get the guard to be used during authentication. * * @return \Illuminate\Contracts\Auth\Guard / public function guard() { return Auth::guard($this->guard); }}控制器中命名空间namespace需要设置好,路由的时候需要用到, $this->middleware(‘refresh’, [’except’ => [’login’,‘register’]]);这里的中间件使用的是网上找的,用于无痛刷新jwt的token,具体可以参考这篇文章:https://www.jianshu.com/p/9e9…6. refresh中间件<?phpnamespace App\Http\Middleware;use Closure;use Tymon\JWTAuth\Exceptions\JWTException;use Tymon\JWTAuth\Http\Middleware\BaseMiddleware;use Tymon\JWTAuth\Exceptions\TokenExpiredException;use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;class RefreshToken extends BaseMiddleware{ /* * @author: zhaogx * @param $request * @param Closure $next * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response|mixed * @throws JWTException */ public function handle($request, Closure $next) { // 检查此次请求中是否带有 token,如果没有则抛出异常。 $this->checkForToken($request); // 使用 try 包裹,以捕捉 token 过期所抛出的 TokenExpiredException 异常 try { // 检测用户的登录状态,如果正常则通过 if ($this->auth->parseToken()->authenticate()) { return $next($request); } throw new UnauthorizedHttpException(‘jwt-auth’, ‘未登录’); } catch (TokenExpiredException $exception) { // 此处捕获到了 token 过期所抛出的 TokenExpiredException 异常,我们在这里需要做的是刷新该用户的 token 并将它添加到响应头中 try { // 刷新用户的 token $token = $this->auth->refresh(); // 使用一次性登录以保证此次请求的成功 \Auth::guard(‘api’)->onceUsingId($this->auth->manager()->getPayloadFactory()->buildClaimsCollection()->toPlainArray()[‘sub’]); } catch (JWTException $exception) { // 如果捕获到此异常,即代表 refresh 也过期了,用户无法刷新令牌,需要重新登录。 throw new UnauthorizedHttpException(‘jwt-auth’, $exception->getMessage()); } } return $next($request)->withHeaders([ ‘Authorization’=> ‘Bearer ‘.$token, ]); }}写好中间件后需要在app/Http/Kernel.php中注入 protected $routeMiddleware = [ …… ‘refresh’ => RefreshToken::class, ];7. routes/api.php 设置路由$api = app(‘Dingo\Api\Routing\Router’);$api->version(‘v1’, [’namespace’ => ‘App\Http\Controllers\V1’], function ($api) { $api->post(‘register’, ‘AuthController@register’); $api->post(’login’, ‘AuthController@login’); $api->post(’logout’, ‘AuthController@logout’); $api->post(‘refresh’, ‘AuthController@refresh’); $api->post(‘me’, ‘AuthController@me’); $api->get(’test’, ‘AuthController@test’);});这里有个坑,不要这样写$api->post(‘me’,[‘middleware’ =>‘refresh’], ‘AuthController@me’);这样虽然能执行这个中间件但执行到$next($request)这里会出错,貌似是一个回调报错 Function name must be a string ,不太清楚具体原因,可以这样写$api->post(‘me’,, ‘AuthController@me’)->middleware(‘refresh’);根据以上几个步骤就可以建立起简单的api后台基础,获取api路由列表可以使用命令行: php artisan api:routesroutes:list貌似无法显示以上api路由,需要在api.php那里再写一遍原始的laravel路由定义才可以显示:比如这样Route::post(‘api/test’, ‘AuthController@test’);后续会用另一篇幅来记录postman和小程序相关知识,可以关注我的博客:https://zgxxx.github.io ...

November 8, 2018 · 3 min · jiezi

关于JWT(Json Web Token)的思考及使用心得

什么是JWT?JWT(Json Web Token)是一个开放的数据交换验证标准rfc7519(https://tools.ietf.org/html/r…,一般用来做轻量级的API鉴权。由于许多API接口设计是遵循无状态的(比如Restful),所以JWT是Cookie Session这一套机制的替代方案。组成JWT由三部分组成头部(header)、载荷(payload)、签名(signature)。头部定义类型和加密方式;载荷部分放不是很重要的数据;签名使用定义的加密方式加密base64后的header和payload和一段你自己的加密key。最后的token由base64(header).base64(payload).base64(signatrue)组成。使用方式平时需要鉴权的接口需要传这个token,可以post字段提交,但是一般建议放在header头中 ,因为JWT一般配合https使用,这样就万无一失。为什么安全?首先token是服务端签发,然后验证时是用同样加密方式把header、payload和key再加密遍 然后看是不是和签名一致 如果不一致就说明token是非法的 这里主要靠的是加密(比如HS256)难以被攻破 至少目前吧 另外不得不说这里的加密对服务器来说是一个开销 这也是JWT的缺点使用我很早就听说过JWT 但那时候还没用上 感觉近几年前后端分离思想加速了JWT的使用 MVC前置到前端(VUE、REACT)后端只用提供API API强调无状态 自然而然使用了JWT这套方案之前做小程序时 没有绝对使用JWT这套方案 我把它简化了下最近开发的一套项目用的是Laravel做后端提供API服务,所以自然而然使用了JWT,使用的扩展是tymon/jwt-auth 关于这套扩展机制的具体使用可以参考这篇文章 写的很不错:https://laravel-china.org/art…项目使用时的具体细节JWT三个时间概念JWT有三个时间概念: 过期时间 宽限时间 刷新时间上面那文章说token过了过期时间是不可刷新的,但其实是可以刷新的,我这边使用时可以(开启了黑名单机制和1min宽限时间) 但是过了刷新时间不能刷新这是肯定的token刷新可以借用laravel的中间件实现自动刷新 服务端判断过期了但是在刷新时间内主动刷新一次 并把新的token在Header头中返回给客户端记住我功能我感觉定义刷新时间(比如一个月) + token自动刷新机制这一套就是记住我功能不能主动销毁Token默认JWT这套方案好像不可以主动剔除用户的,因为服务端不会存token,只是验证。这和session不一样。 但是我感觉可以借用黑名单机制 来判断 中间件中判断该token在剔除黑名单中就销毁token并返回鉴权失败单点登录结合缓存比如redis存客户端标识是可以的多套用户权限机制比如客户端有用户端和商家端两套用户机制(不同表),可以在auth.php定义两个guard 分别指定对应的model Login逻辑单独写 比如auth(‘userApi’) auth(‘corpApi’) 可以拿到不同的guard 刷新中间件是可以共用的 不过router中的middleware方法可以指定具体的middleware为什么不同权限体系的刷新中间件可以公用感觉是jwt-auth实现了这机制 验证token时会根据sub字段判断 我base64decode两个token的payload后发现sub一个是1 一个是2如果有问题欢迎指正!本文最早发布于Rootrl’s blog https://rootrl.github.io/2018…

October 4, 2018 · 1 min · jiezi