试水token认证

486 阅读7分钟

从零起步到能实现jwt认证,工作流程:

1 阅读rfc7519

rfc7519是关于jwt的认证协议,jwt有两种实现(jws和jwe),但是jws使用比较广泛,一般默认jwt指jws,jws的组成:

  • header(typ、cty)
  • playload
    • registered claims name,注册声明。是一组预定义的声明,不是强制性的,但推荐使用,常见字段含义:iss(发行者)、sub(主题)、aud(接收者)、exp(到期时间)、iat(发行者)、jti(jwt id)、nbf(什么时候之前不可用)
    • public claim names、private claim names,自行按需定义。
  • signature,用来验证发送者身份,由前两部分加密形成。

使用jwt-go包写了个简单的demo,作用是读jwt-go相关的源码了解包的使用:

package main

import (
   "fmt"
   "github.com/dgrijalva/jwt-go"
   "time"
)

const (
   SECRETKEY = "243223ffslsfsldfl412fdsfsdf" //私钥
)

type CustomClaims struct {
   UserId int64
   jwt.StandardClaims
}
// 忽略错误处理,因为只是个demo
func main() {
   maxAge := 60 * 60 * 24
   customClaims := &CustomClaims{
      UserId: 11,
      StandardClaims: jwt.StandardClaims{
         ExpiresAt: time.Now().Add(time.Duration(maxAge) * time.Second).Unix(),
         Issuer:    "marry",
      },
   }
   // customClaims对应的做法是,先声明一个结构体,然后定义claims
   // 因为声明的结构体包含StandardClaims,所以相当于实现了Claims接口
   // claims1是直接用包里自带的MapClaims类型(而MapClaims已经实现了Cliams接口)
   claims1 := jwt.MapClaims{
      "id":11,
      "name":"mary",
      "exp":time.Now().Add(time.Duration(maxAge)*time.Second).Unix(),
   }
   // new token with sha256
   token := jwt.NewWithClaims(jwt.SigningMethodHS256, customClaims)
   token1 := jwt.NewWithClaims(jwt.SigningMethodHS256, claims1)
   // signature with secret, return string
   tokenString, _ := token.SignedString([]byte(SECRETKEY))
   tokenString1, _ := token1.SignedString([]byte(SECRETKEY))
   fmt.Printf("token: %v\ntoken1: %v\n",tokenString, tokenString1)
   // 解析tokenString
   ret, _ := ParseToken(tokenString)
   ret1, _ := ParseToken(tokenString1)
   fmt.Printf("user: %v\nuser1:%v",ret, ret1)
}

func ParseToken(tokenString string) (*CustomClaims, error) {
   token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (i interface{}, err error) {
      if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
         return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
      }
      return []byte(SECRETKEY), nil
   })
   if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
      return claims, nil
   } else {
      return nil, err
   }
}

至此,大概了解了jwt的使用流程。

2 阅读rfc6749

rfc6749是关于oauth2.0的规范。在本次作业中用不上,但也是我感兴趣的地方。

2.1 Protocol Flow

这里展示了鉴权中4个角色的交互:

(A)client向resource owner请求授权。该授权请求可以直接发给resource owner,也可以通过authorization server间接进行(例如常见的跳转到授权服务器)。

(B)client获得授权,协议规定了4种授权方式:授权码模式、简化模式、密码模式、客户端模式,github授权第三方使用的是授权码模式,这也是常用的做法,此处不赘述。

(C)client通过authorization grant像authorization server请求token。

(D)authorization server验证client发的grant,如果有效,颁发token。

(E)client通过token向resource server请求资源。

预计(F)resource server验证token,有效则返回相应资源。

规范中ch4-ch9讲的是关于请求和响应的各种规范,比较细节,不赘述。

2.2 github授权第三方登录的demo

写了一个github授权第三方网站登录并获取用户信息的demo,流程是:

  • 先在github注册第三方应用。之后要使用注册的url和重定向url,github生成的clientId和secret。

  • 用户在第三方应用选择github授权登录,浏览器跳转到github登录页面。

  • 用户在github登陆成功。按照设置的重定向url返回第三方应用,此时url上会携带授权码参数code

  • 第三方应用获取授权码,通过授权码向github索取token。

  • github返回token。

  • 第三方应用收到token之后,通过token获取用户信息。

在这里,第三方应用是client,github是resource server,github有自己的authorization serer,用户是resource owner。

3 了解各种token的存储策略

然而了解了上述内容之后,我还是对一个标准的、可应用的sso机制存在问题,尤其是token的存储。虽然jwt有加密和签名,但一旦签名泄漏就和明文没有区别。显然不应当把敏感的信息放在jwt。然而,jwt在微服务中确实是应用广泛。因此,我需要学习各种token的存储机制。

3.1 Postman的鉴权方式

Postman中提及了常用的鉴权方式,因此我去了解各种Authorization type的含义:

  • inherit auth from parent:继承,默认鉴权方式

  • no auth:不鉴权

  • bearer token: 一般也叫jwt==>发送一个json格式的token,服务端针对token校验,粗略地阅了rfc6750。

  • basic token:基础校验,提供用户名密码验证,postman自动生成authorization,是常用的鉴权方式。

  • digest auth: 摘要式认证。在基本身份认证上面扩展了安全性,服务器为每一个连接生成一个唯一的随机数,客户端用这个随机数对密码进行 MD5 加密,然后返回服务器,服务器也用这个随机数对密码进行加密,然后和客户端传送过来的加密数据进行比较,如果一致就返回结果。它是一个二次验证的过程,会有两次认证交互消息。客户端请求资源->服务器返回认证标示->客户端发送认证信息->服务器查验认证。

  • hawk authentication: 另一种认证方案,采用的叫消息码认证算法,和 Digest 认证类似,它也是需要二次交互的

  • aws signature: AWS 签名认证,是针对亚马逊的 AWS 公有云用户签名的认证方式

  • ntlm authentication[beta]: 微软的局域网管理认证协议。

然而,看完这些之后,我的问题依然没有得到解决。于是我去了解github、百度翻译、b站的做法,这里仅说说github。

3.2 github

直接postman访问github的一些url,结果:

eg1:不设置cookie,直接用postman发请求

eg2:

eg3:

相关cookie含义:

  • logged_in:是否登录

  • _octo: This cookie is used by our internal analytics service to distinguish unique users and clients.

  • _gh_sess: This cookie is used for temporary application and framework state between pages like what step the user is on in a multiple step form.

  • _ga: This cookie is used by Google Analytics

  • _device_id: 用于跟踪识别设备

  • tz: 浏览器告知github用户所在的时区

  • user_session: 这个字段用于登录,应该是sessionID

  • _Host-user_session_same_site:设置此cookie是为了确保支持SameSite cookie的浏览器可以检查请求是否来自GitHub。(SameSite-cookies是一种机制,用于定义cookie如何跨域发送。)

  • logged_in: 是否登录

  • dotcom_user: 告知已经登录

  • has_recent_activity: This cookie is used to prevent showing the security interstitial to users that have visited the app recently

4 我的解决策略

至此,可以看出,github采用session-cookie的机制,把鉴权认证相关的内容直接放在了cookie,核心字段是user-session(只要有这个字段就能正常访问,流程是帐号登陆-返回cookie给客户端)。因此,我觉得把token放在cookie是没有问题的。

那么如何设计一套简明可行的sso机制呢?

1 把cookie中的jwt字段置为httpOnly,仅由后端操作

原因:目前比较通用的做法是,不让前端操作敏感的cookie,以防xss攻击。参考github、b站、百度翻译的做法,github的cookie中,只有时区这类非常不敏感的字段才没有置为httpOnly,而b站仅把与session有关的一个字段置为httpOnly,百度翻译算是两者的折衷。所以我认为,是否需要把cookie置为httpOnly,取决于设计者认为这个应用会面临什么类型的风险,需要有多安全。而我认为我们cookie的几个字段说起来都有点敏感,但是真要较真,真正敏感的是token,所以可以仅将token置为httpOnly(这里涉及的代码只会是一些细节。)

2 使用白名单的机制存储token

即,每当用户登录时,颁发一个token,并把token以键值的方式存在redis,每当做token校验时,还需去redis查有没有这个token,有则有效。当用户登出时,从redis删除这个token。

为什么不考虑黑名单?因为黑名单是会膨胀的,而白名单始终存储当前有效的token,整体量不会很大。

3 用rpc和中间件实现主要的鉴权

建立一个简单的鉴权grpc服务,若token未到期、未被修改、存在白名单,即有效。

然而我并没有做grpc部分,不过中间件我倒是写了....

原因:电脑进了半瓶东方树叶....当场变傻,静置之后,刚开电脑以为好了,其实是更严重了...为了各位程序员的头发与心情,喝水要盖盖子...