鉴权技术
一、HTTP Basic Authentication
这种授权方式是浏览器遵守http协议实现的基本授权方式。HTTP协议进行通信的过程中,HTTP协议定义了基本认证认证允许HTTP服务器对客户端进行用户身份证的方法。
1、客户端向服务器请求数据此时,此时客户端尚未被验证。
Get /index.html HTTP/1.0
Host:www.google.com
2、服务器向客户端发送验证请求代码401,(WWW-Authenticate: Basic realm=”google.com”这句话是关键,如果没有客户端不会弹出用户名和密码输入界面)服务器返回的数据大抵如下:
HTTP/1.0 401 Unauthorised
Server: SokEvo/1.0
WWW-Authenticate: Basic realm=”google.com”
Content-Type: text/html
3、当符合http1.0或1.1规范的客户端(WEB浏览器如IE,FIREFOX)收到401返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。
4、用户输入用户名和密码后,将用户名及密码以BASE64编码,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容:
Get /index.html HTTP/1.0
Host:www.google.com
Authorization: Basic dXNlcjE6cGFzcw==
注:dXNlcjE6cGFzcw==表示加密后的用户名及密码(用户名:密码 然后通过base64编码,编码过程是浏览器默认的行为,不需要我们人为加密,我们只需要输入用户名密码即可)
5、服务器收到上述请求信息后,将Authorization字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证。
- 若用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端
- 若用户名及密码不正确,服务器则会返回401,表示认证失败
缺点:
- base64加密是可逆的,不安全
二、session-cookie
利用服务器端的session(会话)和浏览器端的cookie来实现前后端的认证。
1、服务器在接受客户端首次访问时在服务器端创建seesion,然后保存session(我们可以将seesion保存在内存中,也可以保存在redis中,推荐使用后者),然后给这个session生成一个唯一的标识字符串,然后在响应头中种下这个唯一标识字符串。
2、浏览器中收到请求响应的时候会解析响应头,然后将sid保存在本地cookie中。浏览器在下次请求时,请求头中会带上该域名下的cookie信息(其中就包括sid)。
3、服务器在接受客户端请求时会去解析请求头cookie中的sid,根据这个sid去找服务器端保存的该客户端的session,判断该请求是否合法。
缺点:
- cookie依赖于浏览器
- cookie容易被窃取(例XSS攻击)
- 随着认证用户的增多,服务端的开销会明显增大
三、Token
1、客户端使用用户名跟密码请求登录
2、服务端收到请求,去验证用户名与密码
3、验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4、客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
5、客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
6、服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
JWT实现
JWT长什么样?
JWT是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串。就像这样:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT的构成
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。
🌳header
jwt的头部承载两部分信息:
- 声明类型,这里是jwt
- 声明加密的算法 通常直接使用 HMAC SHA256
完整的头部就像下面这样的JSON:
{
'typ': 'JWT',
'alg': 'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
🌳payload
载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分
- 标准中注册的声明
- 公共的声明
- 私有的声明
标准中注册的声明 (建议但不强制使用) :
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个payload:
{
"sub": "1234567890",
"name": "John Doe",
"userId": 34
}
然后将其进行base64加密,得到Jwt的第二部分
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwidXNlcklkIjozNH0=
🌳signature
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
//TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
var signature = HMACSHA256(encodedString, 'secret');
将这三部分用.
连接成一个完整的字符串,构成了最终的jwt:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
优点
- 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
- 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
- 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
- 它不需要在服务端保存会话信息, 所以它易于应用的扩展
安全相关
- 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
- 保护好secret私钥,该私钥非常重要。
- 如果可以,请使用https协议
自定义实现
1、客户端发来用户名和密码,服务端验证之后,会从数据库得到一个userId(用户的唯一标识)
2、服务端根据这个userId来生成TOKEN,组装一个字符串 date()+"system-token-"+userId
,将这段字符串用md5加密作为token;token有效期不能过长,但是不能让用户频繁登录,所以可以使用refresh_token用来刷新token,组装一个字符串 date()+"system-refresh_token-"+userId
,将这段字符串用md5加密作为token。
1618632535180system-token-34 -> 0b80013ba2b117e62336a14f2b040dc4
1618632535182system-refresh_token-34 -> 0b4b264d76c934d6ef85323187086afe
3、将token和userId作为一个键值存到redis数据库,将refresh_tokentoken和userId作为一个键值存到redis数据库,同时设置一个过期时间。
token:0b80013ba2b117e62336a14f2b040dc4 : 34 -> redis # token过期时间2小时
refresh_token:0b4b264d76c934d6ef85323187086afe : 34 -> redis # refresh_token过期时间一个月
4、若用户密码正确,就将token和refresh_token发送给客户端
5、接下来,就可以愉快地玩耍了。客户端在需要鉴权的接口上,使用请求头带上这个token。
6、服务端接收到这个token,可以在redis中查询这个token对应的userId
redis.get("token:0b80013ba2b117e62336a14f2b040dc4") -> 34
7、若token过期,服务端在redis查询的值为空,就返回一个token失效的状态码
,客户端利用refresh_token来获取新的token,避免用户重复登录。
四、OAuth
OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
我们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信。
OAuth 2.0 规定了四种获得令牌的流程。
- 授权码(authorization-code)
- 隐藏式(implicit)
- 密码式(password):
- 客户端凭证(client credentials)
注意,不管哪一种授权方式,第三方应用申请令牌之前,都必须先到系统备案,说明自己的身份,然后会拿到两个身份识别码:客户端 ID(client ID)和客户端密钥(client secret)。这是为了防止令牌被滥用,没有备案过的第三方应用,是不会拿到令牌的。
-
授权码方式
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这里以码云(gitee.com)的QQ登录为例(QQ官方文档):
1、登录 QQ 互联平台,注册成为开发者,注册一个应用
2、获取Authorization Code
请求地址: https://graph.qq.com/oauth2.0/authorize
如果用户成功登录并授权,则会跳转到指定的回调地址,并在redirect_uri地址后带上Authorization Code和原始的state值。如 http://graph.qq.com/demo/index.jsp?code=9A5F************************06AF&state=test
注意:此code会在10分钟内过期。3、通过Authorization Code获取Access Token(后端请求)
4、之后即可通过Access Token获取QQ的用户信息
-
隐藏式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)“隐藏式”(implicit)。
-
密码式
如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式"(password)。
-
凭证式
适用于没有前端的命令行应用,即在命令行下请求令牌。
参考https://blog.csdn.net/wang839305939/article/details/78713124
http://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html