HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态。
用户首次登录时:
第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:
Session 的分布式问题:通常服务端是集群,而用户请求过来会走一次负载均衡,不一定打到哪台机器上。那一旦用户后续接口请求到的机器和他登录请求的机器不一致,或者登录请求的机器宕机了,session 不就失效了吗?这个问题现在有几种解决方式。
Cookie + Session存在的问题:
服务器端Session 的存储方式
为了解决Session + Cookie机制暴露出的诸多问题,我们可以使用Token的登录方式。
Token是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个Token并返回给客户端,客户端后续访问时,只需带上这个Token即可完成身份认证。
用户首次登录时:
Token的优缺点:
最常见的生成方式是使用JWT(Json Web Token),它让通信双方之间以JSON对象的形式安全的传递信息。
其实Token是通过多种算法拼接组合而成的字符串
JWT算法主要分为3个部分:header(头信息),playload(消息体),signature(签名)。
header = '{"alg":"HS256","typ":"JWT"}' // HS256表示使用HMAC-SHA256来生成签名。
payload = '{"loggedInAs":"admin","iat":1422779638}' //iat 表示令牌生成的时间
const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const unsignedToken = `${base64Header}.${base64Payload}`
const key = '服务器私钥'
signature = HMAC(key, unsignedToken)
最后的 Token 计算如下:
const base64Header = encodeBase64(header)
const base64Payload = encodeBase64(payload)
const base64Signature = encodeBase64(signature)
token = `${base64Header}.${base64Payload}.${base64Signature}`
服务器在判断Token时:
const [base64Header, base64Payload, base64Signature] = token.split('.')
const signature1 = decodeBase64(base64Signature)
const unsignedToken = `${base64Header}.${base64Payload}`
const signature2 = HMAC('服务器私钥', unsignedToken)
if(signature1 === signature2) {
return '签名验证成功,token 没有被篡改'
}
const payload = decodeBase64(base64Payload)
if(new Date() - payload.iat < 'token 有效期'){
return 'token 有效'
}
业务接口用来鉴权的token,我们称之为access token。越是权限敏感的业务,我们越希望access token有效期足够短,以避免被盗用。但过短的有效期会造成access token经常过期,过期后怎么办呢?
access token用来访问业务接口,由于有效期足够短,盗用风险小,也可以使请求方式更宽松灵活
refresh token用来获取access token,有效期可以长一些,通过独立服务和严格的请求方式增加安全性;由于不常验证,也可以如前面的session一样处理
有了refresh token后,几种情况的请求流程变成这样:
如果 refresh token 也过期了,就只能重新登录了。
狭义上,我们通常认为session是「种在cookie上、数据存在服务端」的认证方案,token 是「客户端存哪都行、数据存在token里」的认证方案。对 session 和 token 的对比本质上是「客户端存 cookie/存别地儿」、「服务端存数据/不存数据」的对比。
单点登录指的是在公司内部搭建一个公共的认证中心,公司下的所有产品的登录都可以在认证中心里完成,一个产品在认证中心登录后,再去访问另一个产品,可以不用再次登录,即可获取登录状态。
用户首次访问时,需要在认证中心登录:
www.sso.com?return_uri=a.com/pageA
,以便登录后直接进入对应页面。a.com?ticket=123
带上授权码ticket,并将认证中心sso.com的登录态写入Cookie。在a.com服务器中,拿着ticket向认证中心确认,授权码ticket真实有效。验证成功后,服务器将登录信息写入Cookie(此时客户端有2个Cookie分别存有a.com和sso.com的登录态)。认证中心登录完成之后,继续访问a.com下的其他页面:
这个时候,由于a.com存在已登录的Cookie信息,所以服务器端直接认证成功。
如果认证中心登录完成之后,访问b.com下的页面:
这个时候,由于认证中心存在之前登录过的Cookie,所以也不用再次输入账号密码,直接返回第4步,下发ticket给b.com即可。
目前我们已经完成了单点登录,在同一套认证中心的管理下,多个产品可以共享登录态。现在我们需要考虑退出了,即:在一个产品中退出了登录,怎么让其他的产品也都退出登录?回过头来看第 5 步,每一个产品在向认证中心验证ticket时,其实可以顺带将自己的退出登录 api 发送到认证中心。
当某个产品c.com退出登录时:
登录认证,要做的也就两件事情:告诉系统我是谁、向系统证明我是谁
二维码的内容不止可以存数字,还可以存任何的字符串。我们可以认为,它就是字符的另外一种表现形式。
手机扫码这个过程,其实是对二维码的解码,获取二维码中包含的数据。
二维码包含什么:服务端必须给数据生成惟一的标识作为二维码ID,同时还应该设置二维码过期的时间。服务端也应该保存二维码的一些状态:未扫描、已成功、已失效。PC端根据二维码ID等数据生成二维码。
我们发现,只有装载APP,第一次登录的时候,才需要进行基于账号密码的登录,之后即使清理掉这个应用进程,甚至手机重启,都是不需要再次输入账号密码的,它可以自动登录。手机端不存储登录密码,他有一套基于token的认证机制。
这个token其实就是一串有着特殊意义的字符串,它的意义就在于,通过它可以找到对应的账号与设备信息
可能有些同学会想,这个token这么重要,万一被别人知道了怎么办。实际上,知道了也没有影响, 因为设备信息是唯一的,只要你的设备信息别人不知道, 别人拿其他设备来访问,验证也是不通过的。
可以说,客户端登录的目的,就是获得属于自己的token。
轮询:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接。
优点:后端程序编写比较容易。
缺点:请求中有大半是无用,浪费带宽和服务器资源。
实例:适于小型应用。
长轮询:客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。
优点:在无消息的情况下不会频繁的请求,耗费资源小。
缺点:服务器hold连接会消耗资源,返回数据顺序无保证,难于管理维护。
实例:WebQQ、Hi网页版、Facebook IM。
传统的轮询一般是由C向S询问:“有我的信件吗?”。S接到询问之后,会立即查询,并且把查询结果告诉C,不管有没有C的信件,要么回复:“嗯,你有X封信。”,要么回复:“没,没有你的信”。而长轮询更像是这样,C向S发出询问:“有我的信件吗?”,S开始查询,如果有则回复C:“嗯,有你x封信”。如果没有,则不作任何回复,而是让C等着,自己一遍一遍地查询是否有订阅者的信。换句话说:当S收到C的查询请求之后,轮询则只查询一次,并且把查询结果告诉C;而长轮询收到请求之后,则会一遍一遍地查询,直到有消息才会响应C,不然将此连接挂起。
一、思路:用户登录时若勾选“记住我”功能选项,则将登录名和密码(加密后)保存至本地缓存中,下次登录页面加载时自动获取保存好的账号和密码(需解密),回显到登录输入框中。
二、存储账号密码的方法:
localStorage
cookies
三、界面
<el-form :model="loginForm" :rules="rules" ref="loginForm" label-width="100px" class="loginForm demo-ruleForm">
<el-form-item label="账号" prop="userId" autocomplete="on">
<el-input v-model="loginForm.userId" placeholder="请输入账号">el-input>
el-form-item>
<el-form-item label="密码" prop="password">
<el-input type="password" v-model="loginForm.password" placeholder="请输入密码" @keyup.enter="submitForm('loginForm')">el-input>
el-form-item>
<div class="tip">
<el-checkbox v-model="checked" class="rememberMe">记住我el-checkbox>
<el-button type="text" @click="open()" class="forgetPw">忘记密码?el-button>
div>
<el-form-item>
<el-button type="primary" @click="submitForm('loginForm')" class="submit-btn">登录el-button>
el-form-item>
el-form>
四、加密算法选用base64
//安装
npm install --save js-base64
//引入
const Base64 = require("js-base64").Base64
五、localStorage实现
export default {
data() {
return {
loginForm: {
userId: "",
password: "",
},
checked: false,
};
},
mounted() {
let username = localStorage.getItem("userId");
if (username) {
this.loginForm.userId = localStorage.getItem("userId");
this.loginForm.password = Base64.decode(localStorage.getItem("password"));// base64解密
this.checked = true;
}
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
/* ------ 账号密码的存储 ------ */
if (this.checked) {
let password = Base64.encode(this.loginForm.password); // base64加密
localStorage.setItem("userId", this.loginForm.userId);
localStorage.setItem("password", password);
} else {
localStorage.removeItem("userId");
localStorage.removeItem("password");
}
/* ------ http登录请求 ------ */
} else {
console.log("error submit!!");
return false;
}
});
},
},
};
六、cookie实现
export default {
data() {
return {
loginForm: {
userId: "",
password: "",
},
checked: false,
};
},
mounted() {
this.getCookie();
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
/* ------ 账号密码的存储 ------ */
if (this.checked) {
let password = Base64.encode(this.loginForm.password); // base64加密
this.setCookie(this.loginForm.userId, password, 7);
} else {
this.setCookie("", "", -1); // 修改2值都为空,天数为负1天就好了
}
/* ------ http登录请求 ------ */
} else {
console.log("error submit!!");
return false;
}
});
},
// 设置cookie
setCookie(userId, password, days) {
let date = new Date(); // 获取时间
date.setTime(date.getTime() + 24 * 60 * 60 * 1000 * days); // 保存的天数
// 字符串拼接cookie
window.document.cookie =
"userId" + "=" + userId + ";path=/;expires=" + date.toGMTString();
window.document.cookie =
"password" + "=" + password + ";path=/;expires=" + date.toGMTString();
},
// 读取cookie 将用户名和密码回显到input框中
getCookie() {
if (document.cookie.length > 0) {
let arr = document.cookie.split("; "); //分割成一个个独立的“key=value”的形式
for (let i = 0; i < arr.length; i++) {
let arr2 = arr[i].split("="); // 再次切割,arr2[0]为key值,arr2[1]为对应的value
if (arr2[0] === "userId") {
this.loginForm.userId = arr2[1];
} else if (arr2[0] === "password") {
this.loginForm.password = Base64.decode(arr2[1]);// base64解密
this.checked = true;
}
}
}
},
},
};