认证授权
1. 授权认证
目前主流使用的授权认证方案是用户-角色-权限的模式
如下图所示:
对于一个用户的账号,其可以满足多种角色,对于每一种角色其可以有多种权限,对于这种多对多对多的关系,我们可以建立相应的数据库进行维护
如何存储用户的信息:
Cookie-Session是一种存储用户信息的方法,可以用来进行身份的认证。
对于Cookie来说,其存在于客户端,用于存放用户登录等信息,对于敏感字段需要进行加密,前后端编码都可以设置Cookie。Session存在于服务器,所以它更加安全。利用两者可以实现对当前登录用户的身份认证。
用户使用客户端登陆后,服务端创建一个session,并把session-id返回给客户端,我们把这个id存放在cookie中。当用户以后发送请求时带上这个id,就可以验证用户的身份。但是这个方法存在一定的安全性问题,当第三方获取到用户的cookie后可以伪造身份向服务端发送请求,造成用户的不安全状态。
这个问题的解决方案是使用token进行身份验证。在我们登录成功获得 Token 之后,一般会选择存放在 localStorage (浏览器本地存储)中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个 token,这样就不会出现上述的问题。
2. 权限系统设计
用户的权限完全由他所拥有的角色来控制,但是这样会有一个缺点,就是给用户加权限必须新增一个角色,导致实际操作起来效率比较低。所以我们在 RBAC模型 的基础上,新增了给用户直接增加权限的能力,也就是说既可以给用户添加角色,也可以给用户直接添加权限。最终用户的权限是由拥有的角色和权限点组合而成。新权限系统的权限模型:用户最终权限 = 用户拥有的角色带来的权限 + 用户独立配置的权限,两者取并集。
3.OAuth2.0
OAuth 是一个行业的标准授权协议,主要用来授权第三方应用获取有限的权限。OAuth 2.0 比较常用的场景就是第三方登录,当你的网站接入了第三方登录的时候一般就是使用的 OAuth 2.0 协议。
相关链接:
https://tools.ietf.org/html/rfc6749
OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
第三方系统进入系统直接分享密码是一种不妥的行为,第三方系统会长期保持密码而造成不安全的问题,所以采用令牌的方式代替分享密码。对于令牌token来说,他是短期有效的,并且它的有效性在系统管理员的控制之下。并且我们能对令牌做一些权限设置,比如令牌可以设置成只读令牌。
3.1四种获得令牌的流程
3.1.1授权码
授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。
这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。
第一步,A 网站提供一个链接,用户点击后就会跳转到 B 网站,授权用户数据给 A 网站使用。下面就是 A 网站跳转 B 网站的一个示意链接。
https://b.com/oauth/authorize?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=CALLBACK_URL&
scope=read
上面 URL 中,response_type参数表示要求返回授权码(code),client_id参数让 B 知道是谁在请求,redirect_uri参数是 B 接受或拒绝请求后的跳转网址,scope参数表示要求的授权范围(这里是只读)。
第二步,用户跳转后,B 网站会要求用户登录,然后询问是否同意给予 A 网站授权。用户表示同意,这时 B 网站就会跳回redirect_uri参数指定的网址。跳转时,会传回一个授权码,就像下面这样。
https://a.com/callback?code=AUTHORIZATION_CODE
上面 URL 中,code参数就是授权码。
第三步,A 网站拿到授权码以后,就可以在后端,向 B 网站请求令牌。
https://b.com/oauth/token?
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
grant_type=authorization_code&
code=AUTHORIZATION_CODE&
redirect_uri=CALLBACK_URL
上面 URL 中,client_id参数和client_secret参数用来让 B 确认 A 的身份(client_secret参数是保密的,因此只能在后端发请求),grant_type参数的值是AUTHORIZATION_CODE,表示采用的授权方式是授权码,code参数是上一步拿到的授权码,redirect_uri参数是令牌颁发后的回调网址
第四步,B 网站收到请求以后,就会颁发令牌。具体做法是向redirect_uri指定的网址,发送一段 JSON 数据。
{
"access_token":"ACCESS_TOKEN",
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN",
"scope":"read",
"uid":100101,
"info":{...}
}
上面 JSON 数据中,access_token字段就是令牌,A 网站在后端拿到了。
3.1.2隐藏式
适用于 纯前端web应用,这里不做介绍
3.1.3密码式
通过账号密码而不是授权码获得token,流程和方法一基本一致
3.1.4凭证式
不适用
3.2使用和更新令牌
A 网站拿到令牌以后,就可以向 B 网站的 API 请求数据了。
此时,每个发到 API 的请求,都必须带有令牌。具体做法是在请求的头信息,加上一个Authorization字段,令牌就放在这个字段里面。
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.b.com"
上面命令中,ACCESS_TOKEN就是上一步拿到的令牌。
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,B 网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
3.3实际操作
GitHub OAuth 第三方登录示例教程 - 阮一峰的网络日志 (ruanyifeng.com)
4.JWT
4.1Jwt的概念
JWT 本质上就是一组字串,通过(.)切分成三个为 Base64 编码的部分:Header : 描述 JWT 的元数据,定义了生成签名的算法以及 Token 的类型。Payload : 用来存放实际需要传递的数据Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。
如:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
在线解析网站:https://jwt.io/
Header 通常由两部分组成:typ(Type):令牌类型,也就是 JWT。alg(Algorithm):签名算法,比如 HS256。
示例:
{
"alg": "HS256",
"typ": "JWT"
}
Payload:
iss(issuer):JWT 签发方。
iat(issued at time):JWT 签发时间。
sub(subject):JWT 主题。
aud(audience):JWT 接收方。
exp(expiration time):JWT 的过期时间。
nbf(not before time):JWT 生效时间,早于该定义的时间的 JWT 不能被接受处理。
jti(JWT ID):JWT 唯一标识。
示例:{
"uid": "ff1212f5-d8d1-4496-bf41-d2dda73de19a",
"sub": "1234567890",
"name": "John Doe",
"exp": 15323232,
"iat": 1516239022,
"scope": ["admin", "user"]
}
Payload 部分默认是不加密的,一定不要将隐私信息存放在 Payload 当中!!!JSON 形式的 Payload 被转换成 Base64 编码,成为 JWT 的第二部分。
Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。这个签名的生成需要用到:Header + Payload。存放在服务端的密钥(一定不要泄露出去)。签名算法。签名的计算公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,这个字符串就是 JWT 。
4.2如何利用JWT进行身份验证
用户向服务器发送用户名、密码以及验证码用于登陆系统。
如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
用户以后每次向后端发请求都在 Header 中带上这个 JWT 。服务端检查 JWT 并从中获取用户相关信息。
两点建议:
建议将 JWT 存放在 localStorage 中,放在 Cookie 中会有 CSRF 风险。
请求服务端并携带 JWT 的常见做法是将其放在 HTTP Header 的 Authorization 字段中(Authorization: Bearer Token)。
4.3安全性
有了签名之后,即使 JWT 被泄露或者截获,黑客也没办法同时篡改 Signature、Header、Payload。
这是为什么呢?因为服务端拿到 JWT 之后,会解析出其中包含的 Header、Payload 以及 Signature 。服务端会根据 Header、Payload、密钥再次生成一个 Signature。拿新生成的 Signature 和 JWT 中的 Signature 作对比,如果一样就说明 Header 和 Payload 没有被修改。
不过,如果服务端的秘钥也被泄露的话,黑客就可以同时篡改 Signature、Header、Payload 了。黑客直接修改了 Header 和 Payload 之后,再重新生成一个 Signature 就可以了。
密钥一定保管好,一定不要泄露出去。JWT 安全的核心在于签名,签名安全的核心在密钥。
项目中使用
下面是我自己在项目中使用的token工具类
package com.java.common.util;
import cn.hutool.core.date.DateField;
import cn.hutool.core.date.DateTime;
import cn.hutool.json.JSONObject;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTPayload;
import cn.hutool.jwt.JWTUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class JwtUtil {
private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class);
/**
* 盐值很重要,不能泄漏,且每个项目都应该不一样,可以放到配置文件中
*/
private static final String key = "Jiawa12306";
public static String createToken(Long id, String mobile) {
DateTime now = DateTime.now();
DateTime expTime = now.offsetNew(DateField.HOUR, 24);
Map<String, Object> payload = new HashMap<>();
// 签发时间
payload.put(JWTPayload.ISSUED_AT, now);
// 过期时间
payload.put(JWTPayload.EXPIRES_AT, expTime);
// 生效时间
payload.put(JWTPayload.NOT_BEFORE, now);
// 内容
payload.put("id", id);
payload.put("mobile", mobile);
String token = JWTUtil.createToken(payload, key.getBytes());
LOG.info("生成JWT token:{}", token);
return token;
}
public static boolean validate(String token) {
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
// validate包含了verify
boolean validate = jwt.validate(0);
LOG.info("JWT token校验结果:{}", validate);
return validate;
}
public static JSONObject getJSONObject(String token) {
JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes());
JSONObject payloads = jwt.getPayloads();
payloads.remove(JWTPayload.ISSUED_AT);
payloads.remove(JWTPayload.EXPIRES_AT);
payloads.remove(JWTPayload.NOT_BEFORE);
LOG.info("根据token获取原始内容:{}", payloads);
return payloads;
}
public static void main(String[] args) {
createToken(1L, "123");
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE2NzY4OTk4MjcsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE2NzY4OTk4MzcsImlhdCI6MTY3Njg5OTgyN30.JbFfdeNHhxKhAeag63kifw9pgYhnNXISJM5bL6hM8eU";
validate(token);
getJSONObject(token);
}
}