以往讲使用springsecurity等具体技术的资料已经很多了,笔者这篇文章不打算写框架和代码的具体实现,而是会讨论认证和授权的区别。然后会介绍一些被业界广泛采用的技术,最后会聊聊:怎么为API构建选择合适的认证方式?
在一些互联网公司的面试中,面试官往往会问这样一个问题:“如果禁用浏览器cookie,如何实现用户追踪和认证?”
遗憾的是:依然有大量候选人答非所问,无法搞清楚cookie和session之间的区别。
而在工作中也有让人惊讶的真实案例:把userID存储到localstorage中当做token使用,原因是他们声称弃用了cookie这种落后的东西;一个移动端项目,服务器给出的API中需要客户端模拟一个cookie,从而像浏览器中ajax那样消费API。
互联网是基于HTTP协议构建的,而HTTP协议因为简单流行开来,但是HTTP协议是无状态(通信层面上虚电路比数据报昂贵太多)的。为此人们为了追踪用户想出了各种办法,包括:cookie/session机制、token、flash跨浏览器cookie甚至浏览器指纹等。
把用户身份藏在每一个地方(浏览器指纹技术甚至不需要存储介质)
认证、授权、凭证
首先,认证和授权是两个不同的概念,为了让我们的API更加安全和具有清晰的设计,理解认证和授权的不同就非常有必要了,它们在英文中也是不同的单词。
认证:是authentication,指的是当前用户的身份——当用户登陆过后系统便能追踪到他的身份做出符合相应业务逻辑的操作。
即使用户没有登录,大多数系统也会追踪他的身份,只是当做来宾或者匿名用户来处理。认证技术解决的是“我是谁?”的问题。
授权:与认证不同,授权是authorization,指的是什么样的身份被允许访问某些资源,在获取到用户身份后继续检查用户的权限。
单一的系统授权往往是伴随认证来完成的,但是在开放API的多系统结构下,授权可以由不同的系统来完成,例如OAuth。授权技术是解决“我能做什么?”的问题。
实现认证和授权的基础是需要一种媒介(credentials)来标记访问者的身份或权利,在现实生活中每个人都需要一张身份证才能访问自己的银行账户、结婚和办理养老保险等,这就是认证的凭证。
在古代军事活动中,皇帝会给出战的将军颁发兵符,下级将领不关心持有兵符的人,只需要执行兵符对应的命令即可。在互联网世界中,服务器为每一个访问者颁发sessionID存放到cookie,这就是一种凭证技术。
数字凭证还表现在方方面面:SSH登录的密匙、JWT令牌、一次性密码等。
用户账户也不一定是存放在数据库中的一张表,在一些企业IT系统中,对账户管理和权限有了更多的要求。所以,账户技术(accounting)可以帮助我们使用不同的方式管理用户账户,同时具有不同系统之间共享账户的能力,例如:微软的活动目录(AD),以及简单目录访问协议(LDAP),甚至区块链技术。
还有一个重要的概念是:访问控制策略(AC)。如果我们需要把资源的权限划分到一个很细的粒度,就不得不考虑用户以何种身份来访问受限的资源,选择基于访问控制列表(ACL)还是基于用户角色的访问控制(RBAC)或者其他访问控制策略。
在流行的技术和框架中,这些概念都无法孤立的被实现,因此在现实中使用这些技术时,大家往往为一个OAuth2是认证还是授权这种概念争论不休。
为了容易理解,我在文末附上了一份常见技术和概念的术语表。
下面,我会介绍在API开发中常常使用的几种认证和授权技术:HTTPBasicAUthentication、HAMC、OAuth2,以及凭证技术JWTtoken。
HTTPBasicAuthentication
你一定用过这种方式,但不一定知道它是什么。
在不久之前,当你访问一台家用路由器的管理界面,往往会看到一个浏览器弹出表单,要求你输入用户密码。
在这背后,当用户输入完用户名密码后,浏览器帮你做了一个非常简单的操作:
组合用户名和密码然后Base64编码。给编码后的字符串添加Basic前缀,然后设置名称为Authorization的header头部。
API也可以非常简单的提供HTTPBasicAuthentication认证方式,那么客户端可以很简单通过Base64传输用户名和密码即可:。
将用户名和密码使用冒号连接,例如:username:abc。为了防止用户名或者密码中存在超出ASCII码范围的字符,推荐使用UTF-8编码。将上面的字符串使用Base64编码,例如:dXNlcm5hbWU6YWJjMTIzNDU2。在HTTP请求头中加入“Basic+编码后的字符串”——即:Authorization:BasicQWxhZGRpbjpPcGVuU2VzYW1l。这种方式实现起来非常简单,在大量场景下被采用。
当然缺点也很明显,Base64只能称为编码,而不是加密(实际上,无需配置密匙的客户端并没有任何可靠地加密方式,我们都依赖TSL协议)。
这种方式的致命弱点是:编码后的密码,如果明文传输则容易在网络传输中泄露,在密码不会过期的情况下,密码一旦泄露,只能通过修改密码的方式。
HMAC(AK/SK)认证
在我们对接一些PASS平台和支付平台时,会要求我们预先生成一个accesskey(AK)和securekey(SK),然后通过签名的方式完成认证请求。这种方式可以避免传输securekey,且大多数情况下签名只允许使用一次,避免了重放攻击。
这种基于AK/SK的认证方式主要是利用散列的消息认证码(Hash-basedMessageAuthenticationCode)来实现的。因此,有很多地方叫HMAC认证,实际上不是非常准确。
HMAC只是利用带有key值的哈希算法生成消息摘要,在设计API时有具体不同的实现。
HMAC在作为网络通信的认证设计中作为凭证生成算法使用,避免了口令等敏感信息在网络中传输。
基本过程如下:
客户端需要在认证服务器中预先设置accesskey(AK或叫appID)和securekey(SK)。在调用API时,客户端需要对参数和accesskey进行自然排序后并使用securekey进行签名生成一个额外的参数digest。服务器根据预先设置的securekey进行同样的摘要计算,并要求结果完全一致。注意securekey不能在网络中传输,以及在不受信任的位置存放(浏览器等)。为了让每一次请求的签名变得独一无二,从而实现重放攻击,我们需要在签名时放入一些干扰信息。
在业界标准中有两种典型的做法,质疑/应答算法(OCRA:OATHChallenge-ResponseAlgorithm)、基于时间的一次性密码算法(TOTP:Time-basedOne-timePasswordAlgorithm)。
1.质疑/应答算法
质疑/应答算法需要客户端先请求一次服务器,获得一个未认证的返回,并得到一个随机字符串(nonce)。
将nonce附加到按照上面说到的方法进行HMAC签名,服务器使用预先分配的nonce同样进行签名校验,这个nonce在服务器只会被使用一次,因此可以提供唯一的摘要。
2.基于时间的一次性密码认证
为了避免额外的请求来获取nonce,还有一种算法是使用时间戳,并且通过同步时间的方式协商到一致,在一定的时间窗口内有效(1分钟左右)。
这里的只是利用时间戳作为验证的时间窗口,并不能严格的算作基于时间的一次性密码算法。
标准的基于时间的一次性密码算法在两步验证中被大量使用,例如:Google身份验证器不需要网络通信也能实现验证(但依赖准确的授时服务)。
原理是:客户端服务器共享密钥然后根据时间窗口能通过HMAC算法计算出一个相同的验证码。
TOTP基本原理和常见厂商
OAuth2和OpenID
OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
OAuth是一个授权标准,而不是认证标准。提供资源的服务器不需要知道确切的用户身份(session),只需要验证授权服务器授予的权限(token)即可。
上图只是OAuth的一个简化流程,OAuth的基本思路就是通过授权服务器获取accesstoken和refreshtoken(refreshtoken用于重新刷新accesstoken),然后通过accesstoken从资源服务器获取数据。
在特定的场景下还有下面几种模式:
授权码模式(authorizationcode)简化模式(implicit)密码模式(resourceownerpasswordcredentials)客户端模式(clientcredentials)如果需要获取用户的认证信息,OAuth本身没有定义这部分内容,如果需要识别用户信息,则需要借助另外的认证层,例如:OpenIDConnect。
1.验证accesstoken
在一些介绍OAuth的博客中,很少讲到:资源服务器是怎么验证accesstoken的?
OAuthcore标准并没有定义这部分,不过在OAuth其他标准文件中提到两种验证accesstoken的方式。
1)在完成授权流程后,资源服务器可以使用OAuth服务器提供的Introspection接口来验证accesstoken,OAuth服务器会返回accesstoken的状态以及过期时间。
在OAuth标准中验证token的术语是Introspection。同时,也需要注意accesstoken是用户和资源服务器之间的凭证,不是资源服务器和授权服务器之间的凭证。资源服务器和授权服务器之间应该使用额外的认证(例如:Basic认证)。
2)使用JWT验证:授权服务器使用私钥签发JWT形式的accesstoken,资源服务器需要使用预先配置的公钥校验JWTtoken,并得到token状态和一些被包含在accesstoken中信息。因此,在JWT的方案下,资源服务器和授权服务器不再需要通信,在一些场景下带来巨大的优势。同时,JWT也有一些弱点,我会在JWT的部分解释。
2.refreshtoken和accesstoken
几乎所有人刚开始了解OAuth时都有一个一疑问:为什么已经有了accesstoken还需要refreshtoken呢?
授权服务器会在第一次授权请求时,一起返回accesstoken和refreshtoken,在后面刷新accesstoken时只需要refreshtoken。
accesstoken和refreshtoken的设计意图是不一样的,accesstoken被设计用来客户端和资源服务器之间交互,而refreshtoken是被设计用来客户端和授权服务器之间交互。
某些授权模式下,accesstoken需要暴露给浏览器,充当一个资源服务器和浏览器之间的临时会话,浏览器和资源服务器之间不存在签名机制,accesstoken成为唯一凭证。因此,accesstoken的过期时间(TTL)应该尽量短,从而避免用户的accesstoken被嗅探攻击。
由于要求accesstoken时间很短,refreshtoken可以帮助用户维护一个较长时间的状态,避免频繁重新授权。
大家会觉得让accesstoken保持一个长的过期时间不就可以了吗?
实际上,refreshtoken和accesstoken的不同之处在于:即使refreshtoken被截获,系统依然是安全的,客户端拿着refreshtoken去获取accesstoken时,同时需要预先配置的securekey,客户端和授权服务器之前始终存在安全的认证。
3.OAuth、OpenID、OpenIDConnect
认证方面的术语实在太多,我在搭建自己的认证服务器或接入第三方认证平台时,有时候到完成开发工作的最后一刻都无法理解这些术语。
OAuth负责解决分布式系统之间的授权问题,即使有时候客户端和资源服务器或者认证服务器存在同一台机器上。OAuth没有解决认证的问题,但提供了良好的设计利于和现有的认证系统对接。
OpenID解决的问题是:分布式系统之间身份认证问题,使用OpenIDtoken能在多个系统之间验证用户,以及返回用户信息,可以独立使用,与OAuth没有关联。
OpenIDConnect解决的是:在OAuth这套体系下的用户认证问题,实现的基本原理是将用户的认证信息(IDtoken)当做资源处理。在OAuth框架下完成授权后,再通过accesstoken获取用户的身份。
这三个概念之间的关系有点难以理解,用现实场景来说:如果系统中需要一套独立的认证系统,并不需要多系统之间的授权可以直接采用OpenID。
如果使用了OAuth作为授权标准,可以再通过OpenIDConnect来完成用户的认证。
JWT
在OAuth等分布式的认证、授权体系下,对凭证技术有了更多的要求,比如:包含用户ID、过期等信息,不需要再外部存储中关联。
因此,业界对token做了进一步优化,设计了一种自包含令牌,令牌签发后无需从服务器存储中检查是否合法,通过解析令牌就能获取令牌的过期、有效等信息,这就是JWT(JSONWebToken)。
JWT是一种包含令牌(self-containedtoken),或者叫值令牌(valuetoken),我们以前使用关联到session上的hash值被叫做引用令牌(referencetoken)。
简而言之,一个基本的JWT令牌为一段点分3段式结构。
生成JWT令牌的流程为:
headerjson的base64编码为令牌第一部分。payloadjson的base64编码为令牌第二部分。拼装第一、第二部分编码后的json以及secret进行签名的令牌的第三部分。因此,只需要签名的secretkey就能校验JWT令牌,如果在消息体中加入用户ID、过期信息就可以实现验证令牌是否有效、过期了,无需从数据库/缓存中读取信息。因为使用了加密算法,所以第一、二部分即使被修改(包括过期信息)也无法通过验证。
JWT优点是:不仅可以作为token使用,同时也可以承载一些必要信息,省去多次查询。
注意:
JWTtoken的第一、二部分只是base64编码,肉眼不可读,不应当存放敏感信息。JWTtoken的自包含特性,导致了无法被撤回。JWT的签名算法可以自己拟定,为了便于调试,本地环境可以使用对称加密算法,生产环境建议使用非对称加密算法。JWTtoken在微服务的系统中优势特别突出:多层调用的API中可以直接传递JWTtoken,利用自包含的能力,可以减少用户信息查询次数;更重要的是,使用非对称的加密方式可以通过在系统中分发密匙的方式验证JWTtoken。
当然,OAuth对accesstoken等凭证所选用的技术并没有做出限制,OAuth并不强制使用JWT,在使用JWT自包含特性的优势时,必须考虑到JWT撤回困难的问题。在一些对撤回token要求很高的项目中不适合使用JWT,即使采用了一些方案实现(whitelist和blacklist)也违背了设计JWT的初衷。
Cookie、TokeninCookie、SessionToken依然被使用
在构建API时,开发者会发现我们的认证方式和网页应用有一些不同,除了像ajax这种典型的web技术外,如果我们希望API是无状态的,不推荐使用Cookie。
使用Cookie的本质是用户第一次访问时服务器会分配一个SessionID,后面的请求中客户端都会带上这个ID作为当前用户的标志。因为HTTP本身是无状态的,Cookie属于一种内建于浏览器中实现状态的方式。如果我们的API是用来给客户端使用的,强行要求API的调用者管理Cookie也可以完成任务。
在一些遗留或者不是标准的认证实现的项目中,我们依然可以看到这些做法,快速地实现认证。
使用cookie,例如web项目中ajax的方式。使用sessionID或hash作为token,但将token放入header中传递。将生成的token(可能是JWT)放入cookie传递,利用HTTPonly和Secure标签保护token。
选择合适的认证方式
随着微服务的发展,API的设计不仅仅是面向WEB或者MobileAPP,还有BFF(BackendforFrontend)和DomainAPI的认证,以及第三方服务的集成。
客户端到服务器之间认证和服务器到服务器之间认证是不同的。
我们把终端用户(Human)参与的通信,叫做Human-to-machine(H2M),服务器与服务器之间的通信叫做Machine-to-machine(M2M)。
H2M的通信需要更高的安全性,M2M的通信天然比H2M安全,因此更多的强调性能,在不同的场合下选择合适的认证技术就显得特别重要。例如:HTTPBasicAuthentication用来作为H2M认证显得有些落后,但是在M2M中被大量使用。
另外值得一提的是:H2M这种通信方式下,客户端不受控制,由于无法自主分发密匙,认证通信的安全高度依赖HTTPS。
从一个宏观的角度看待他们的关系,对我们技术选型非常有帮助。
术语表
Browserfingerprinting通过查询浏览器的代理字符串,屏幕色深,语言等,然后这些值通过散列函数传递产生指纹,不需要通过Cookie就可以识别浏览器。MAC(Messageauthenticationcode)在密码学中,讯息鉴别码,是经过特定算法后产生的一小段资讯,检查某段讯息的完整性。HOTP(HMAC-basedOne-timePasswordalgorithm)基于散列消息验证码的一次性密码算法。Two-stepverification是一种认证方法,使用两种不同的元素,合并在一起,来确认使用者的身份,是多因素验证中的一个特例。OTP(Onetimepassword)一次性密码,例如注册邮件和短信中的认证码。
参考文章