因为HTTP是无状态的协议,也就是说,这个协议是无法记录用户访问状态的,其每次请求都是独立的无关联的,一笔是一笔。而我们的网站都是设计成多个页面的,所在页面跳转过程中我们需要知道用户的状态,尤其是用户登录的状态,这样我们在页面跳转后我们才知道是否可以让用户有权限来操作一些功能或是查看一些数据。
所以,我们每个页面都需要对用户的身份进行认证。为了实现这一功能,用得最多的技术就是浏览器的cookie,我们会把用户登录的信息存放在客户端的cookie里,这样,我们每个页面都从这个cookie里获得用户是否登录的信息,从而达到记录状态,验证用户的目的。下面是使用cookie的一些原则。
千万不要在cookie中存放用户的密码。加密的密码都不行
。因为这个密码可以被人获取并尝试离线穷举。所以,一定不能把用户的密码保存在cookie中。
关于设计“记住密码”
。一般的设计是当用户勾选了这个功能,系统会生成一个cookie,cookie包括用户名和一个固定的散列值,这个固定的散列值一直使用。这样,你就可以在所有的设备和客户上都可以登录,而且可以有多个用户同时登录。
在cookie中,保存三个东西——用户名,登录序列,登录token。
用户名
:明文存放。
登录序列
:一个被MD5散列过的随机数,仅当强制用户输入口令时更新(如:用户修改了口令)。
登录token
:一个被MD5散列过的随机数,仅一个登录session内有效,新的登录session会更新它。
上述三个东西会存在服务器上,服务器的验证用户需要验证客户端cookie里的这三个事。
流程
a.com/pageA
,并输入密码登录。SessionId
,并将它保存起来。SessionId
写入 Cookie
中。二次访问时
a.com/pageB
页面时,会自动带上第一次登录时写入的 Cookie。Cookie
中的 SessionId
和保存在服务器端的 SessionId
是否一致。缺点
Token 是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。
token生成方式:
最常见的 Token 生成方式是使用 JWT(Json Web Token
,它是一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。
JWT 算法主要分为 3 个部分:header
(头信息),playload
(消息体),signature
(签名)。
登录流程
生成 token
,返回给前端;存储在 localStorage 和 Vuex 里
;判断 localStorage 有无 token
,没有则跳转到登录页,有则请求获取用户信息,改变登录状态;在 Axios 请求头里携带 token
;没有或者 token 过期
,返回401
;前端得到 401 状态码,重定向到登录页面。单点登录指的是在公司内部搭建一个公共的认证中心,公司下的所有产品的登录都可以在认证中心里完成,一个产品在认证中心登录后,再去访问另一个产品,可以不用再次登录,即可获取登录状态。
a.com
下的 pageA
页面。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
的登录态)。当某个产品 c.com 退出登录时:
c.com 中
的登录态 Cookie。sso.com
中的退出 api。a.com
的运营者需要在微信开放平台注册账号,并向微信申请使用微信登录功能。appid、appsecret
。a.com
上选择使用微信登录。这时会跳转微信的 OAuth
授权登录,并带上 a.com
的回调地址。a.com?code=123
,这时带上了一个临时票据code
。code 、appid、appsecret
,向微信服务器申请 token,验证成功后,微信会下发一个 token
。token
拿到对应的微信用户头像,用户昵称等信息了。Cooke
,以作为后续访问的凭证。使用 respone 拦截器,对 2xx 状态码以外的结果进行拦截。
if (window.localStorage.getItem('token')) {
Axios.defaults.headers.common['Authorization'] = `Bearer ` + window.localStorage.getItem('token')
}
如果状态码是401,则有可能是 Token 过期,跳转到登录页
instance.interceptors.response.use(
response => {
return response
},
error => {
if (error.response) {
switch (error.response.status) {
case 401:
router.replace({
path: 'login',
query: { redirect: router.currentRoute.fullPath } // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}
}
return Promise.reject(error.response)
}
)
localStorage 里有 Token ,就调用获取 userInfo 的方法,并继续执行,如果没有 Token ,调用退出登录的方法,重定向到登录页。
router.js
import { createRouter, createWebHashHistory, createWebHistory, useRouter } from 'vue-router';
import store from '../store/index.js'
const routes = [
{ path: '/', redirect: '/login' },
//添加这个字段,表示进入这个页面需要登录
{ path: '/publish_found', component: PublishFound, meta: { title: "招领", requireAuth: true } },
{ path: '/publish_lost', component: PublishLost, meta: { title: "寻物", requireAuth: true } },
{ path: '/my_publish', component: MinePublish, meta: { title: "我的发布", requireAuth: true } },
{ path: '/my_msg', component: MineMessage, meta: { title: "我的信息", requireAuth: true } }
]
// 全局路由前置守卫(这里做权限的判断,路由的控制)
router.beforeEach((to, from, next) => {
document.title = to.meta.title;
if (to.meta.requireAuth) { // 判断该路由是否需要登录权限
const token = store.state.token ? store.state.token : window.sessionStorage.getItem('token')
if (token) {
next()
} else {
next({ path: '/login' })
}
} else {
next()
}
})
// 3.导出路由对象
export default router;
export default {
user: '张三',
token: '',
userImage: '',
}
export const GET_USER = 'getUser'
export const SET_TOKEN = 'setToken'
export const SET_USERIMAGE = 'setUserImage'
export default {
GET_USER(state, value) {
state.user = value;
},
SET_TOKEN(state, value) {
state.token = value;
console.log(value);
},
SET_USERIMAGE(state, value) {
state.userImage = value;
},
OUT_LOGIN(state) {
state.token = ''; //退出登录,清空token
}
}
export default {
setToken({ commit }, value) {
commit('SET_TOKEN', value)
},
setUser({ commit }, value) {
commit('SET_USER', value)
},
setImage({ commit }, value) {
commit('SET_IMAGE', value)
},
}
const submitUserMsg = () => {
if (phone.value.trim() == "" || password.value.trim() == "") {
Toast({
message: "账号/密码不能为空",
position: "top",
});
} else if (identify.value !== data.identifyCode) {
{
Toast({
message: "验证码错误",
position: "top",
});
}
} else {
//通过所有校验,获取token存储在store里
request
.post("/login", { data: formData })
.then((res) => {
console.log(res.token);
store.dispatch("setToken", res.token);
router.push("/user");
})
.catch((err) => {
console.log(err);
});
}
};
OUT_LOGIN
const backLogin = () => {
console.log("退出登录");
Dialog.confirm({
title: "确认退出登录吗",
message: "",
})
.then(() => {
store.commit("OUT_LOGIN");
router.push({
path: "/login",
});
})
.catch(() => {});
};
import { createStore } from 'vuex';
import createPersistedSatte from 'vuex-persistedstate'
import state from './state'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
const store = createStore({
state,
getters,
actions,
mutations,
plugins: [createPersistedSatte()]
})
export default store;
import { createApp } from 'vue'
import App from './App.vue'
import router from './router/router.js'
import Vant from 'vant';
import 'vant/lib/index.css'
import ('./mock/mock.js');
import store from './store/index.js'
const app = createApp(App);
app.use(router);
app.use(Vant)
app.use(store);
app.mount('#app')
npm install express-session