聊聊鉴权那些事

在系统级项目开发时经常会遇到一个问题就是鉴权,身为一个前端来讲可能咱们距离鉴权可能比较远,通常来讲咱们也只是去应用,并无对权限这一部分进行深刻的理解。html

什么是鉴权

鉴权:是指验证用户是否拥有访问系统的权利。传统的鉴权是经过密码来验证的。这种方式的前提是,每一个得到密码的用户都已经被受权。在创建用户时,就为此用户分配一个密码,用户的密码能够由管理员指定,也能够由用户自行申请。这种方式的弱点十分明显:一旦密码被偷或用户遗失密码,状况就会十分麻烦,须要管理员对用户密码进行从新修改,而修改密码以前还要人工验证用户的合法身份。 -- 节选自百度百科前端

上述简单扼要的说明了一下鉴权的概念,可是这也只是简单的鉴权,也是项目中最最多见的及安全形式了,可是对于后端鉴权又是如何去作的,咱们还是一无所知,通常来讲对于后端来讲,鉴权最长见的方式分为三种:vue

  1. Session/Cookie
  2. Token或Jwt
  3. OAuth

这种受权方式是浏览器遵照http协议实现的基本受权方式,HTTP协议进行通讯的过程当中,HTTP协议定义了基本认证认证容许HTTP服务器对客户端进行用户身份证的方法。接下来就一一介绍一下这三种鉴权方式。node

Session/Cookie

Cookie是一个很是具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存储功能。Cookie由服务器生成,发送给浏览器,浏览器把CookieKV形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该Cookie发送给服务器。因为Cookie是存在客户端上的,因此浏览器加入了一些限制确保Cookie不会被恶意使用,同时不会占据太多磁盘空间,因此每一个域的Cookie数量是有限的。ios

Cookie.jsweb

const Http = require("http");
const app = Http.createServer((req,res) => {
    if(req.url === "/favicon.ico"){
        return;
    }else{
        res.setHeader("Set-Cookie","cx=Segmentfault");
        res.end("hello cookie");
    };
});
app.listen(3000);

使用node Cookie.js运行上面代码,等程序启动后访问http://localhost:3000/,就能够看到hello cookie字样,这样的话就表明该服务已经启动了。若想查看到到咱们所设置的Cookie,首先观察一下在NetworkResponse Headers中,能够看到咱们所写的Set-Cookie属性,当咱们访问http://localhost:3000/的时候,当浏览器接收到Set-Cookie这个属性的时候,浏览器会根据其内部约定,并在其浏览器内部对其cookie进行存储,打开浏览器控制台,在Application中找到Cookies中找到相对应的域名,就能够看到咱们所设置的cookie值了。当在同域的状况下,当再次请求数据的时候浏览器会默认发送cookie在该请求中,一块儿发送给后端。为了证明上面的说法,刷新一下http://localhost:3000/页面,在控制台Network找到Request Headers中能够看到Cookie: cx=Segmentfault属性,既然发送给服务端以后,相应的在后端也是能够接收到该Cookie的,修改一下上面的例子:redis

const Http = require("http");
const app = Http.createServer((req,res) => {
    if(req.url === "/favicon.ico"){
        return;
    }else{
        console.log("cookie",req.headers.cookie)
        res.setHeader("Set-Cookie","cx=Segmentfault");
        res.end("hello cookie");
    };
});
app.listen(3000);

在接收到访问的时候,就能够接收到了cx=Segmentfault,若是说如今这份Cookie是一份加密的数据的话,里面包含一些用户信息,在经过先后端进行交互以后,当客户端再次请求服务端的时候,服务端拿到相对应的Cookie并对其进行解密,对其中用户的信息进行鉴权处理就能够了。算法

服务端经过Set-CookieResponse Headers设置了一段加密数据,客户端接收到了其相对应的数据以后,浏览器对其进行存储,当可客户端再次发送请求的时候,会携带已有的CookieRequest Headers中一并发送给服务端,服务端解密数据完成鉴权,由此能够得出Cookie是服务端存储在客服端的状态标志,再由客户端发送给服务端,由服务端解析。Cookie在使用中必须是同域的状况下才能够,通常经常使用的是在MVC这种开发形式中很经常使用。npm

说了半天Cookie,可是对于Session却只字未提,接下来就介绍一下SessionSession从字面上讲,就是会话。这个就相似于你和一我的交谈,你怎么知道当前和你交谈的是张三而不是李四呢?对方确定有某种特征(长相等)代表他就是张三。Session也是相似的道理,服务器要知道当前发请求给本身的是谁。为了作这种区分,服务器就要给每一个客户端分配不一样的身份标识,而后客户端每次向服务器发请求的时候,都带上这个身份标识,服务器就知道这个请求来自于谁了。至于客户端怎么保存这个身份标识,能够有不少种方式,对于浏览器客户端,你们都默认采用Cookie的方式。json

const Http = require("http");
let session = {};
const app = Http.createServer((req,res) => {
    const sessionKey = "uId";
    if(req.url === "/favicon.ico"){
        return;
    }else{
        const uId = parseInt(Math.random() * 10e10);
        const cookie = req.headers.cookie;
        if(cookie && cookie.indexOf(sessionKey) !== -1){
            let _uId = cookie.split("=")[1];
            res.end(`${session[_uId].name} Come back`);
        }
        else{
            res.setHeader("Set-Cookie",`${sessionKey}=${uId}`);
            session[uId] = {"name":"Aaron"};
            res.end("hello cookie");
        }
    };
});
app.listen(3000);

代码中解析cookie只是用了和很简单的方式,只是为了完成Dome而已,在实际项目中获取cookie比这个要复杂不少。

Session/Cookie认证主要分四步:

  1. 服务器在接受客户端首次访问时在服务器端建立seesion,而后保存seesion(咱们能够将seesion保存在内存中,也能够保存在redis中,推荐使用后者),而后给这个session生成一个惟一的标识字符串,而后在响应头中种下这个惟一标识字符串。
  2. 签名。这一步只是对sid进行加密处理,服务端会根据这个secret密钥进行解密。(非必需步骤)
  3. 浏览器中收到请求响应的时候会解析响应头,而后将sid保存在本地cookie中,浏览器在下次http请求的时候,请求头中会带上该域名下的cookie信息,
  4. 服务器在接受客户端请求时会去解析请求头cookie中的sid,而后根据这个sid去找服务器端保存的该客户端的session,而后判断该请求是否合法。

o_session_cookie.png

利用服务器端的session和浏览器端的cookie来实现先后端的认证,因为http请求时是无状态的,服务器正常状况下是不知道当前请求以前有没有来过,这个时候咱们若是要记录状态,就须要在服务器端建立一个会话(seesion),将同一个客户端的请求都维护在各自得会会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端建立seesion,若是有则已经认证成功了,不然就没有认证。

redis结合使用:

const koa = require("koa");
const session = require("koa-session");
const redisStore = require("koa-redis");
const redis = require("redis");
const wrapper = require("co-redis");
const app = new koa();
const redisClient = redis.createClient(6379,"localhost");
const client = wrapper(redisClient);
//  相似于密钥
app.keys = ["Aaron"];
const SESSION_CONFIG = {
    //  所设置的session的key
    key:"sId",
    //  最大有效期
    maxAge:8640000,
    //  是否防止js读取
    httpOnly:true,
    //  cookie二次签名
    signed:true,
    //  存储方式
    stroe:redisStore({client})
};
app.use(session(SESSION_CONFIG,app));
app.use((ctx) => {
    redisClient.keys("*",(err,keys) => {
        keys.forEach(key => {
            redisClient.get(key,(err,val) => {
                console.log(val);
            });
        })
    })
    if(ctx.path === "/favicon.ico") return;
    let n = ctx.session.count || 0;
    ctx.session.count = ++n;
    ctx.body = `第${n}次访问`
});
app.listen(3000);

虽然Session/Cookie能够解决鉴权问题,可是会有很大的问题,对于服务端来讲说是一个巨大的开销,严重的限制了服务器扩展能力,好比说我用两个机器组成了一个集群,小F经过机器A登陆了系统,那sessionId会保存在机器A上,假设小F的下一次请求被转发到机器B怎么办?机器B可没有小F的sessionId,有时候会采用一点小伎俩:session sticky,就是让小F的请求一直粘连在机器A上,可是这也无论用,要是机器A挂掉了, 还得转到机器B去。那只好作session的复制了,把sessionId在两个机器之间搬来搬去,再好的服务器也经不起这样的折腾。

Token或Jwt

在计算机身份认证中是令牌(临时)的意思,在词法分析中是标记的意思。通常做为邀请、登陆系统使用。如今先后端分离火热,Token混的风生水起,不少项目开发过程当中都会用到Token,其实Token是一串字符串,一般由于做为鉴权凭据,最经常使用的使用场景是API鉴权。

o_Token.png

  1. 客户端使用用户名跟密码请求登陆
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个Token,再把这个Token发送给客户端
  4. 客户端收到Token之后能够把它存储起来,好比放在Cookie里或者Local Storage
  5. 客户端每次向服务端请求资源的时候须要带着服务端签发的Token
  6. 服务端收到请求,而后去验证客户端请求里面带着的Token,若是验证成功,就向客户端返回请求的数据

示例:

前端

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<title>Document</title>
</head>
<body>
<div id="app">
    <div>
        <input type="text" v-model="username">
        <input type="text" v-model="passwrold">
    </div>
    <div>
        <button @click="login">登录</button>
        <button @click="loginOut">退出</button>
        <button @click="getUserInfo">获取用户信息</button>
    </div>
    <div>
        <button @click="logs = []">清空日志</button>
    </div>
    <ul>
        <li v-for="(item,index) of logs" :key="index">{{item}}</li>
    </ul>
</div>
<script>
axios.defaults.baseURL = "http://localhost:3000"
//  请求拦截
axios.interceptors.request.use((config) => {
    const token = localStorage.getItem("token");
    if(token){
        //  判断是否存在token,若是存在的话
        //  每次发起HTTP请求时在headers中添加token
        //  Bearer是JWT的认证头部信息
        config.headers["Authorization"] = `Bearer ${token}`
    }
    return config;
},error => alert(error));
//  响应拦截
axios.interceptors.response.use((res) => {
    app.logs.push(JSON.stringify(res.data))
    return res;
},error => alert(error));
const app = new Vue({
    el:"#app",
    data:{
        username:"",
        passwrold:"",
        logs:[]
    },
    methods:{
        login() {
            let {username,passwrold} = this;
            axios.post("/users/login/token",{
                username,passwrold
            }).then((res) => {
                localStorage.setItem("token",res.data.token)
            })
        },
        loginOut(){
            axios.post("/users/logout").then((res) => {
                localStorage.removeItem("token")
            })
            
        },
        getUserInfo(){
            axios.get("/users/get/user/info").then((res) => {
                console.log(res)
            });
        }
    }
})
</script>
</body>
</html>

后端:

const Koa = require("koa");
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
const Router = require('koa-router'); // koa 路由中间件
const bodyParser = require("koa-bodyparser");
const cors = require("koa2-cors");
const app = new Koa();
const router = new Router();
//  密钥
const secret = "this is a secret";
app.use(bodyParser());
app.use(cors());
router.post("/users/login/token",(ctx) => {
   const {body} = ctx.request;
   const {username} = body;
   ctx.body = {
       code:1,
       message:"登录成功",
       body:{
        username
       },
       token:jwt.sign({
           data:body,
           exp:Math.floor(Date.now() / 1000) + 60 * 60,
       },secret)
   }
});
router.post("/users/logout",(ctx) => {
    const {body} = ctx.request;
    ctx.body = {
        code:1,
        message:"退出成功"
    }
})
router.get("/users/get/user/info",jwtAuth({secret}),(ctx) => {
    //  jwtAuth token参数
    console.log(ctx.state.user.data)
    ctx.body = {
        code:1,
        message:"成功",
        data:ctx.state.user.data
    }
})
app.use(router.routes());
app.listen(3000);

上面代码用到了不少的依赖模块,最关键的的是jsonwebtokenkoa-jwt,这两个模块一个是用来对token进行加密,一个是用来对数据进行解密的,同时在每次访问须要保护的路由的时候须要使用jwtAuth对其进行拦截处理,jwtAuth会根据其secret进行数据解密,把解密的数据存放到ctx.state中,供用户读取。

有关jwt相关请查看深刻理解令牌认证机制详细的解释了其加密后数据token的构成。

加密后的数据主要分为三个部分机密头部、载荷、数据若是咱们想查看其加密前内容是什么样子的,能够经过base64对其没一部分进行解密。

  1. 机密头部:声明加密规则,可反解
  2. 载荷:数据信息,也就是咱们须要加密的信息,可反解
  3. 验证:这部分是对前两部分使用hash算法的摘要,是不可逆的

在使用jsonwebtoken时须要注意的是,因为加密信息是能够反解的因此,尽可能不要在加密数据中存放敏感信息,好比用户的密码,用户私密信息等等(千万不要效仿Dome,这是不对的O(∩_∩)O)。同过上面所述,所传递给前端的token一旦发生变化,仅仅是一个字母大小写发生变化也是不行的,当服务端接收到token解密时,是没法正确解密的,这种token能够是发篡改的。若是想要篡改token必需要有其secret才能够对其进行篡改和伪造。

OAuth

OAuth(开放受权)是一个开放标准,容许用户受权第三方网站访问他们存储在另外的服务提供者上的信息,而不须要将用户名和密码提供给第三方网站或分享他们数据的全部内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都须要显式的向用户征求受权。咱们常见的提供OAuth认证服务的厂商有支付宝,QQ,微信

OAuth协议又有1.02.0两个版本。相比较1.0版,2.0版整个受权验证流程更简单更安全,也是目前最主要的用户身份验证和受权方式。

o_OAuth.png

OAuth认证主要经历了以下几步:

  1. 须要第三方应用存储资源全部者的凭据,以供未来使用,一般是明文密码。
  2. 须要服务器支持密码身份认证,尽管密码认证天生就有安全缺陷。
  3. 第三方应用得到的资源全部者的受保护资源的访问权限过于宽泛,从而致使资源全部者失去对资源使用时限或使用范围的控制。
  4. 资源全部者不能仅撤销某个第三方的访问权限而不影响其它,而且,资源全部者只有经过改变第三方的密码,才能单独撤销这第三方的访问权限。
  5. 与任何第三方应用的让步致使对终端用户的密码及该密码所保护的全部数据的让步。

简单归纳,就是用于第三方在用户受权下调取平台对外开放接口获取用户相关信息。OAuth引入了一个受权环节来解决上述问题。第三方应用请求访问受保护资源时,资源服务器在获准资源用户受权后,会向第三方应用颁发一个访问令牌(AccessToken)。该访问令牌包含资源用户的受权访问范围、受权有效期等关键属性。第三方应用在后续资源访问过程当中须要一直持有该令牌,直到用户主动结束该次受权或者令牌自动过时。

总结

受权方式多种多样,主要仍是要取决于咱们对于产品的定位。若是咱们的产品只是在企业内部使用,tokensession就能够知足咱们的需求,如今先后端分离如此火热jwt认证方式更加适合。

感谢你们阅读本文章,文章中如有错误请你们指正,若是感受有多帮助的话,不要忘记点赞哦。

相关文章
相关标签/搜索