认证与授权

  • 认证(authentication,authn):验证某个用户是否具有访问系统的权限。
  • 授权(authorization,authz):验证某个用户是否具有访问某个资源的权限。

即认证证明了用户是谁,授权决定了用户能够干什么。

认证的四种方式

常见的认证方式有四种,Basic,Digest,OAuth,Bearer。 在设计系统的时候,要遵循:不要在请求参数中使用明文密码,也不要在任何存储中保存明文密码。

Basic

这是最简单的认证方式,将用户名:密码进行Base64编码后,放入到HTTP Authorization Header中。HTTP请求到达后端,服务就会解码获得用户名和密码,并与数据库中的值进行比较来认证用户。

Basic需要与SSL配置来保证安全性。

Digest

摘要认证。修复了基本认证的严重缺陷。

  • 绝不用明文方式在网络上发送密码。
  • 可以有效防止重放攻击。
  • 有选择地防止对报文内容的篡改。

摘要认证过程如下:

  1. 客户端请求服务端资源
  2. 服务端认证失败,返回401 Unauthorized并返返回WWW-Authenticate头,里面包含了认证需要的信息。
  3. 客户端根据WWW-Authenticate头中的信息,选择加密算法,使用密码随机数nonce,计算出密码摘要response,并再次请求服务端。
  4. 服务端将客户端提供的密码摘要与服务器内部计算的摘要进行对比,匹配则表明认证通过,然后返回一些与授权会话相关的附加信息,放在Authorization-Info中。

下面是WWW-Authenticate中携带的信息: 使用摘要避免了明文发送密码,同时nonce的随机数每次请求都会变化,可以防止重放攻击。

OAuth

允许第三方用户访问该用户在某个Web服务上存储的私密资源,而无需将用户名和密码提供给第三方应用。

OAuth2.0的授权方式有四种:密码式、隐藏式、拼接式、授权码模式。

1. 密码式

直接将用户名和密码告诉第三方应用,然后第三方应用通过用户名和密码换取令牌。 除非其它授权方式无法使用,并且高度信任第三方应用,否则不要使用这种方法。

流程如下:

  1. 网站A向用户发出获取用户名和密码的请求。
  2. 用户同意后,网站A拼接用户名和密码向网站B获取令牌。
  3. 网站B验证用户身份,给出网站A令牌,网站A凭借令牌可以访问网站B对应权限的资源。

2. 隐藏式

适用于前端,认证流程如下:

  1. A网站提供一个跳转到B网站的链接,用户点击跳转到B网站,并向用户请求授权。
  2. 用户登录B网站,同意授权后,跳转回A网站指定的重定向的redirect_url地址,并携带B网站返回的令牌,用户在B网站的数据给A使用。 这种方式存在中间人攻击的风险,只适合一些安全性不高的场景,并且令牌有效时间要非常短。

3. 凭借式

在命令行中请求,适用于没有前端的命令行应用。认证流程如下:

  1. 应用A在命令行向应用B请求授权,A需要携带B提前颁发的secretID和secretKey,其中secretKey出于安全性考虑,需要在后端发送。
  2. B收到secretID和secretKey,进行身份验证,通过后返回给A令牌。

4. 授权码式

第三方应用提前申请一个授权码,然后使用授权码来获取令牌。认证流程如下:

  1. A提供一个跳转到B的链接+redirect_url,用户点击之后跳转到B。
  2. 用户携带向B网站提前申请的client_id,向B发起身份验证请求。
  3. 用户登录B网站,通过验证,授权A网站权限,然后网站跳转回redirect_url,其中会有B网站通过验证后的授权码附在url后面。
  4. 网站A携带授权码向B网站请求令牌,网站B验证授权码后,返回令牌即access_token。

Bearer

也成为令牌认证,核心是bearer token。这是一个加密字符串,通常由服务端根据密钥生成。 客户端请求服务端的时候,必须在请求头中包含Authorization: Bearer <token>。服务端校验token来进行认证。

token最流行的方式是JSON Web Token(JWT)。就通过JWT来描述Bearer认证原理。

在一次登录之后,产生一个有一定有效期的token,每个请求都携带这个token,后端对这个token进行验证。 可以利用文件、缓存、数据库将token存放下来,也可以利用更简单的方式,使用密钥来签发Token,可以省略存储。

认证流程如下:

  1. 客户端使用用户名和密码登录。
  2. 服务端验证用户名和密码,通过后,就签发一个token给客户端。
  3. 客户端将token缓存起来,比如cookie或者localStorage。之后每次请求都会携带token。
  4. 服务端收到请求之后,会验证token,通过才处理请求。

JWT的格式由三部分组成: Header和Payload都是存放一些不包含敏感信息的声明,Signature签名则是将base64编码后的Header和Payload通过服务端的密钥进行加密得到的。 可以了解到服务端的密钥对于安全性至关重要。

认证实现

每一个认证当作一个策略,使用策略模式实现。 定义一个策略的接口

// AuthStrategy 定义了用于资源认证的方法集合
type AuthStrategy interface {
    AuthFunc() gin.HandlerFunc
}

然后定义一个调用策略的执行者

// AuthOperator 用于不同的认证策略之间的切换
type AuthOperator struct {
    strategy AuthStrategy
}
// SetStrategy 用来设置成指定的认证策略
func (operator *AuthOperator) SetStrategy(strategy AuthStrategy) {
    operator.strategy = strategy
}
 
// AuthFunc 执行资源认证
func (operator *AuthOperator) AuthFunc() gin.HandlerFunc {
    return operator.strategy.AuthFunc()
}

这样就只需要为对应的策略实现AuthStrategy接口即可。 下面实现了四种策略

basic策略

实现Basic认证。用来验证用户名和密码。然后为其实现AuthFunc方法即可。

type BasicStrategy struct {
    compare func(username string, password string) bool
}

jwt策略

利用gin-jwt包来实现。其内部的GinJWTMiddleware包含了认证相关函数。

type JWTStrategy struct {
    ginjwt.GinJWTMiddleware
}

将需要认证的API加载到需要认证的API路由上,来实现API认证。

jwtStrategy, _ := newJWTAuth().(auth.JWTStrategy)
g.POST("/login", jwtStrategy.LoginHandler)
g.POST("/logout", jwtStrategy.LogoutHandler)
// Refresh time can be longer than token timeout
g.POST("/refresh", jwtStrategy.RefreshHandler)

其中jwtStrategy提供的了一些认证函数:

  • LoginHandler: 实现了Basic认证,完成登录认证。
  • RefreshHandler:刷新Token的过期时间。
  • LogoutHandler:用户注销时调用,清楚认证相关信息。

auto策略

根据HTTP头Authorization: Basic xxx和Authorization: Bearer xxx来自动选择使用Basic还是Bearer认证。


tags: 权限认证