概述
因为做的项目涉及到用户认证和授权,所以好好总结了一下这块。
认证和授权
一般我们说的认证主要指的是用户登录认证;一般我们说的授权主要是第三方授权。
用户登录认证主要有2种方法,一种是基于session-id的认证,另一种是基于token的认证。
第三方授权主要是Oauth2.0标准的授权,它主要包括授权码模式的授权和简单模式的授权。
基于session-id的认证
基于session-id的认证是比较传统的认证方式,它包含以下几个步骤:
- 用户输入用户名和密码,发送给服务器。
- 服务器验证这个用户名和密码,如果成功就生成一个会话id(session-id),然后把这个会话id同时保存在服务器和浏览器的cookie里面。因为保存的地方是浏览器的cookie,所以这种认证方式也叫基于cookie的认证方式。
- 用户每次请求资源,都会把这个会话id发送给服务器,服务器端就在本地找这个会话id,如果能够找到,就返回用户请求的数据。
- 当用户退出登录时,会话同时在客户端和服务器端被销毁。
对于这种方式,我们前端的处理方式是:
- 发送用户名和密码给服务器,并接收服务器返回的会话id。
- 服务器会自动把会话id储存在浏览器的cookie里面,不需要我们前端处理。
- 由于在发送http请求的时候会自动带上cookie里面的数据,所以在发送其它http请求的时候不需要前端特殊处理。
- 用户登出的时候(注意是用户主动注销或者退出登录),前端需要请求一个登出接口,然后服务器会销毁储存在浏览器cookie中的会话id。
需要注意的是,上面所有的一切都由服务器来完成,前端只请求接口就行了。服务器能够自己把会话id写入到浏览器的cookie里面。
基于token的认证
基于token的认证主要是指json web token认证,即jwt认证。
它主要包含如下几个步骤:
- 用户输入用户名和密码,发送给服务器。
- 服务器验证用户名和密码,成功则返回一个token(可以认为是一个很长的字符串)给浏览器。
- 后续每次请求,浏览器都会把token发送给服务器,服务器验证token是否有效,有效则返回用户请求的数据。
- 当用户退出登录时,浏览器端销毁这个token就行了。
需要注意的是,jwt认证有一个很重要的特点:服务器端没有储存token。
为什么服务器端不需要储存token呢,我们来看一下token的生成过程。
jwt这个字符串是由三部分组成:header(头部),payload(主体信息),signature(签名)。
头部是给JWT的基本信息,然后通过加密(base64加密)得到的。通过解密可以得到原始信息。比如下面这段基本信息:类型是jwt,签名算法是HS256。
{
"typ": "JWT",
"alg": "HS256"
}
加密后的头部如下:
ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9
主体信息是描述用户的信息,然后通过加密(base64加密)得到的。通过解密可以得到原始信息。比如下面这段主体信息。
{
"iss": "Yang JWT",
"iat": 1441593502,
"exp": 1441594722,
"aud": "www.example.com",
"sub": "yang@example.com",
"from_user": "B",
"target_user": "A"
}
加密后的主体信息如下:
ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9
将上面加密后的2段字符串用点号连接在一起,如下所示:
ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9
再对上面的字符串进行HS256算法加密,加密的时候需要我们提供一个密钥(比如我们设置密钥为63161014009),加密后的内容就是签名:
rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
最后再把签名通过点号拼在最后就得到了token:
ewogICJ0eXAiOiAiSldUIiwKICAiYWxnIjogIkhTMjU2Igp9.ew0KICAgICJpc3MiOiAiWWFuZyBKV1QiLA0KICAgICJpYXQiOiAxNDQxNTkzNTAyLA0KICAgICJleHAiOiAxNDQxNTk0NzIyLA0KICAgICJhdWQiOiAid3d3LmV4YW1wbGUuY29tIiwNCiAgICAic3ViIjogInlhbmdAZXhhbXBsZS5jb20iLA0KICAgICJmcm9tX3VzZXIiOiAiQiIsDQogICAgInRhcmdldF91c2VyIjogIkEiDQp9.rSWamyAYwuHCo7IFAgd1oRpSP7nzL7BF5t7ItqpKViM
上面的三个加密都是可逆的,其中头部和主体信息任何人都能用base64解密,签名需要密钥才能解密。黑客没有密钥是不能伪造签名的,所以也不能伪造token。然后,在服务器端只需要重新对token里的头部和主体信息用密钥加密一次,然后和签名对比,就能知道这个token合不合法了。所以根本就不需要把token存在本地。
由于服务器不需要把token存在本地,节省了查找token的时间,在用户登出的时候,服务器也不需要销毁token等额外处理。所以jwt减少了服务器的很多压力。
值得一提的是,上面只是说明了一种jwt的模式,过程中都可以根据实际情况对流程进行改造。比如有的业务把token放到http header里面发送给服务器,有的业务把token放到url参数里面发送给服务器。
虽然jwt相比session-id有很多优势,但是它不能解决jwt失效的问题,除非设置过期时间。也就是说,如果把token放在服务器中储存的话,能够解决这个问题:就是假如我储存token在redis里面,每次认证的时候比较这个token,这样就能做到,用户A用A的信息登录之后,用户B再用A的信息登录,那么用户A的登录就会失效,因为储存了新的token(用户B收到的token)。但是jwt实现不了这种情形。
所以说,jwt只适用于有效期极短的token!!!它并不能取代session-id!!!
混合认证
由于基于session-id的认证有如下缺点:
- 全部靠服务端完成,服务端拥有状态,压力会很大。
- 对于跨域请求,不会自动发送cookie。
基于jwt的认证又有如下缺点:
- 只能通过设置有效期来废除token,如果token的有效期设置太长,那么在这段时间内没有办法废除这个token。
所以对于一般的认证,现在通常用混合方法,具体如下:
- 用户输入用户名和密码,发送给服务器。
- 服务器验证用户名和密码,成功则返回一个token给浏览器,并且把这个token和用户名一起储存在服务器中。
- 后续每次请求,浏览器都会把token发送给服务器,服务器把收到的token和本地的对比,一致则返回用户请求的数据。
- 每次用户重新登录,服务器都会重新生成一个token,并清除以前的token,储存起来。
- 当用户退出登录时,浏览器端销毁这个token,服务器不作处理。
这种认证的优点是:
- 服务端只需要生成和储存token,压力会小很多,考虑到存取速度,可以用redis来储存token,这样读取和查找会很快。
- 服务端没有状态,也适合restful api。
授权码模式的授权
授权码模式是Oauth2.0中最精彩的授权模式。它的步骤如下:
比如说,用户需要在豆瓣上注册登录,希望用用户的qq的信息来注册,这个时候就需要取得第三方QQ的授权,并拿取QQ里面这个用户的信息。
- 用户访问豆瓣,豆瓣将用户导向QQ的认证服务器页面。
- 用户选择同意QQ的授权。
- QQ的认证服务器页面将用户导向豆瓣指定的“重定向URI”,并且附上一个授权码(code)。
- 豆瓣指定的“重定向URI”利用授权code,再加上储存在“重定向URI”里面的服务器id和密码,向QQ的认证服务器申请令牌(token)。
- QQ的认证服务器核对授权code,服务器id和密码,确认无误之后,向“重定向URI”发送令牌(token)。
- “重定向URI”利用令牌(token)向QQ的资源服务器申请用户信息,然后在豆瓣注册此用户。
- 用户在豆瓣的注册完成。(不需要用户填写任何用户信息)
有下面几点需要注意:
黑客即使获取了第二步中的授权码也没用,因为黑客没有服务器id和密码,所以黑客还是不能访问QQ的服务器中的用户信息。
服务器id和密码是什么?服务器id和密码是豆瓣的“重定向URI”向QQ的认证服务器注册的时候获得的id和密码,作为豆瓣的标识。在注册的时候,QQ的认证服务器也会储存这个“重定向URI”,所以不同的用户都会导向同一个“重定向URI”。
第4步中的请求必须用https请求,否则授权code,服务器id和密码有可能被黑客截获,认证服务器发送回的令牌也可能被截获。
为什么需要一个“重定向URI”?首先,豆瓣的服务器id和密码不能储存在其它页面,它只能储存在豆瓣的“重定向URI”里面,否则很可能被泄漏。其次,“重定向URI”和认证服务器的通信必须为https,一般的豆瓣页面与认证服务器的通信可能不是https。
在第6步中,“重定向URI”可能会把令牌(token)发回给豆瓣原页面,也可能制造一个豆瓣的token发回给原页面。
Oauth2.0还有一个refresh_token。一般QQ的认证服务器发回的token的有效期很短,而refresh_token的有效期比较长,所以当token过期的时候,可以通过refresh_token向QQ的认证服务器申请token。
用户一共要进行3次uri跳转,一次是跳转到QQ的认证服务器。第二次是跳转到“重定向URI”。第三次跳转回以前的页面。其中第一次需要用户操作,点击是否同意QQ授权;第二次不需要用户操作,自动进行第三次跳转。
简单模式的授权
简单模式是不利用“重定向URI”,直接向认证服务器中申请token的模式。还是以豆瓣和QQ为例,它的步骤如下:
- 用户访问豆瓣,豆瓣将用户导向QQ的认证服务器页面。
- 用户选择同意QQ的授权。
- QQ的认证服务器页面将用户导向豆瓣指定的“重定向URI”,并且直接在URI的hash部分附带了令牌(token)。
- “重定向URI”利用令牌(token)向QQ的资源服务器申请用户信息,然后在豆瓣注册此用户。
- 用户在豆瓣的注册完成。(不需要用户填写任何用户信息)
有下面几点需要注意:
和授权码模式的授权的区别是,没有授权码,从而没有检测服务器id和密码。
黑客可以通过这2种方式进行攻击:1.黑客直接截取这个token。2.黑客伪造一个第三方网站,然后引导用户在伪造的第三方网站进行授权,最后就能理所当然的得到token了。授权码模式的授权可以防止第二种攻击,但是同样不能防止第一种攻击(可以利用https加强安全性)。
授权码模式的授权和简单模式的授权为了防止CSRF攻击,都会在跳转的URI上面加一个state参数来进行校验。
微信的静默授权和这个差不多,只是没有第二步,直接默认同意授权。
URL跳转才会在safari浏览器下面留下白条,非URL的URI跳转不会。由于微信的非静默授权会跳转到一个用户确认是否进行授权的URL,所以非静默授权会留下白条,静默授权不会。(不是因为有code才留下白条的)
为什么要在hash部分附带令牌?因为浏览器处理hash参数的时候不会使页面重新加载。(虽然说新的history api能够解决这个问题。)
如果认证服务器不支持跨域,那么只能用简单模式的授权。因为授权码模式的授权会提交服务器id,密码和code到认证服务器,所以需要认证服务器支持跨域的post请求;但是简单模式的授权全部是URI跳转,所以不用担心跨域。