赞
踩
代码地址:https://github.com/wendingming/fastapi-vue-postgresql
昨天我用VUE完善了登录页面,并访问登录接口,获得了token返回值。
今天我们来学习一下,VUE前端,利用store来建立【保存token,判断token有没有过期,过期则弹出提示,跳转到重新登录页面,】这些方法。
唉,学来学去,尼玛的,VUE事情最多。。。
果然,前端都是神,后端一个接口,前端薅成秃头。
首先:改axios调用接口的方法,也就是前面写的http.js。
在其中响应拦截时,判断是否有token过期提示代码,【这个需要和后端约定好,用某个代码代表超时】
还是说一段fastapi代码吧,不然真的都变成VUE学习了。。。
后端fastapi的login.py文件,在我之前有从官方文档复制出来,现在详细解析了一下【主要是加了中文注释说明】,代码如下:
-
- # 导入相关的模块
- from datetime import datetime, timedelta
- from typing import Optional
-
- from fastapi import Depends, FastAPI, HTTPException, status, Form
- from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
- from jose import JWTError, jwt
- from passlib.context import CryptContext
- from pydantic import BaseModel
- from starlette.middleware.cors import CORSMiddleware
-
- import json
- """定义关于token的相关常量
- SECRET_KEY : 用于加密解密的密钥,只允许服务器知道,打死不告诉别人
- 可以执行 openssl rand -hex 32 获取一串随机的字符
- ALGORITHM : 定义加密解密所使用的算法
- ACCESS_TOKEN_EXPIRE_MINUTES : token的有效期
- """
- SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
- ALGORITHM = "HS256"
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
-
- dict_permisson = [
- {
- "menu_id":1,
- "menu_name":"系统首页",
- "parent_id":0,
- "pageurl":"/sys/index"
- },
- {
- "menu_id":2,
- "menu_name":"菜单管理",
- "parent_id":1,
- "pageurl":"/sys/list"
- },
- {
- "menu_id":3,
- "menu_name":"角色管理",
- "parent_id":1,
- "pageurl":"/sys/role"
- },
- {
- "menu_id":4,
- "menu_name":"管理员管理",
- "parent_id":1,
- "pageurl":"/sys/admin"
- },
- {
- "menu_id":5,
- "menu_name":"用户管理",
- "parent_id":0,
- "pageurl":"/user/index"
- },
- {
- "menu_id":6,
- "menu_name":"会员列表",
- "parent_id":5,
- "pageurl":"/user/list"
- },
- ]
- json_permisson = json.dumps(dict_permisson)
- # 这里定义一个字典,来模拟数据库中的数据
- fake_users_db = {
- "johndoe": {
- "uid": 1,
- "username": "johndoe",
- "full_name": "John Doe",
- "avatar": "https://up.enterdesk.com/2021/edpic/c4/9f/09/c49f090757360f843141fe2bab2cfc8f_1.jpg",
- "email": "johndoe@example.com",
- "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",#默认密码secret
- "disabled": False,
- "permisson": json_permisson
- }
- }
-
-
- class Token(BaseModel):
- """定义token的数据模型"""
- access_token: str
- token_type: str
-
-
- class TokenData(BaseModel):
- username: Optional[str] = None
-
- class FormData(BaseModel):
- uname: str
- passwd: str
-
- class User(BaseModel):
- """定义用户的数据模型"""
- uid: str
- username: str
- full_name: Optional[str] = None
- avatar: Optional[str] = None
- email: Optional[str] = None
- disabled: Optional[bool] = None
- permisson: Optional[str] = None
-
-
- class UserInDB(User):
- hashed_password: str
-
-
- # 创建一个加密解密上下文环境(甚至可以不用管这两句话啥意思)
- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
-
- # 实例化一个FastAPI实例
- app = FastAPI()
-
-
- # 设置允许访问的域名
- origins = [
- "http://localhost",
- "http://localhost:8080",
- "http://127.0.0.1",
- "*"
- ] #也可以设置为"*",即为所有。
-
-
- # 设置跨域传参
- app.add_middleware(
- CORSMiddleware,
- allow_origins=origins, # 设置允许的origins来源
- allow_credentials=True,
- allow_methods=["*"], # 设置允许跨域的http方法,比如 get、post、put等。
- allow_headers=["*"]) # 允许跨域的headers,可以用来鉴别来源等作用。
-
-
- def verify_password(plain_password, hashed_password):
- """验证密码是否正确
- :param plain_password: 明文
- :param hashed_password: 明文hash值
- :return:
- """
- return pwd_context.verify(plain_password, hashed_password)
-
-
- def get_password_hash(password):
- """获取密码的hash值
- :param password: 欲获取hash的明文密码
- :return: 返回一个hash字符串
- """
- return pwd_context.hash(password)
-
-
- def get_user(db, username: str):
- """查询用户
- :param db: 模拟的数据库
- :param username: 用户名
- :return: 返回一个用户的BaseModel(其实就是字典的BaseModel对象,二者可互相转换)
- """
- if username in db:
- user_dict = db[username]
- return UserInDB(**user_dict)
-
-
- def authenticate_user(fake_db, username: str, password: str):
- """验证用户
- :param fake_db: 存储用户的数据库(这里是上面用字典模拟的)
- :param username: 用户名
- :param password: 密码
- :return:
- """
- # 从数据库获取用户信息
- user = get_user(fake_db, username)
- # 如果获取为空,返回False
- if not user:
- return False
- # 如果密码不正确,也是返回False
- if not verify_password(password, user.hashed_password):
- return False
- # 如果存在此用户,且密码也正确,则返回此用户信息
- return user
-
-
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
- """创建tokens函数
- :param data: 对用JWT的Payload字段,这里是tokens的载荷,在这里就是用户的信息
- :param expires_delta: 缺省参数,截止时间
- :return:
- """
- # 深拷贝data
- to_encode = data.copy()
- # 如果携带了截至时间,就单独设置tokens的过期时间
- if expires_delta:
- expire = datetime.utcnow() + expires_delta
- else:
- # 否则的话,就默认用15分钟
- expire = datetime.utcnow() + timedelta(minutes=15)
- to_encode.update({"exp": expire})
- # 编码,至此 JWT tokens诞生
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
- return encoded_jwt
-
-
- async def get_current_user(token: str = Depends(oauth2_scheme)):
- """获取当前用户信息,实际上是一个解密token的过程
- :param token: 携带的token
- :return:
- """
- credentials_exception = HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials",
- headers={"WWW-Authenticate": "Bearer"},
- )
- try:
- # 解密tokens
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
- # 从tokens的载荷payload中获取用户名
- username: str = payload.get("sub")
- # 如果没有获取到,抛出异常
- if username is None:
- raise credentials_exception
- token_data = TokenData(username=username)
- except JWTError:
- raise credentials_exception
- # 从数据库查询用户信息
- user = get_user(fake_users_db, username=token_data.username)
- if user is None:
- raise credentials_exception
- return user
-
-
- async def get_current_active_user(current_user: User = Depends(get_current_user)):
- """获取当前用户信息,实际上是作为依赖,注入其他路由以使用。
- :param current_user:
- :return:
- """
- # 如果用户被禁,抛出异常
- if current_user.disabled:
- raise HTTPException(status_code=400, detail="Inactive user")
- return current_user
-
-
- #-----修改部分star--------------------
- # @app.post("/token", response_model=Token)<----------原本的代码
- # async def login_for_access_token(form_data: FormData):<--------------原本的代码
- def login_for_access_token(form_data: FormData):
- """这里定义了一个接口,路径为 /token, 用于用户申请tokens
- :param form_data:
- :return:
- """
- # 首先对用户做出检查
- user = authenticate_user(fake_users_db, form_data.uname, form_data.passwd)
- if not user:
- raise HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Incorrect username or password",
- headers={"WWW-Authenticate": "Bearer"},
- )
- # 定义tokens过期时间
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
- # 创建token
- access_token = create_access_token(
- data={"sub": user.username}, expires_delta=access_token_expires
- )
- # 返回token信息,JavaScript接收并存储,用于下次访问
- baktoken = {
- "access_token": access_token, "token_type": "bearer"
- }
- #return {"access_token": access_token, "token_type": "bearer"}<----原来
- return baktoken
- #-----修改部分end--------------------
-
- @app.get("/users/me/", response_model=User)
- async def read_users_me(current_user: User = Depends(get_current_active_user)):
- """获取当前用户信息
- :param current_user:
- :return:
- """
- return current_user
-
-
- @app.get("/users/me/items/")
- async def read_own_items(current_user: User = Depends(get_current_active_user)):
- return [{"item_id": "Foo", "owner": current_user.username}]
其中一个方法很重要:get_current_user
相关代码解析如下:
-
- from fastapi import Depends, FastAPI, HTTPException, status, Form
- from jose import JWTError, jwt
-
-
- async def get_current_user(token: str = Depends(oauth2_scheme)):
- """获取当前用户信息,实际上是一个解密token的过程
- :param token: 携带的token
- :return:
- """
- credentials_exception = HTTPException(
- status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Could not validate credentials",
- headers={"WWW-Authenticate": "Bearer"},
- )
- try:
- # 解密tokens
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
- # 从tokens的载荷payload中获取用户名
- username: str = payload.get("sub")
- # 如果没有获取到,抛出异常
- if username is None:
- raise credentials_exception
- token_data = TokenData(username=username)
- except JWTError:
- raise credentials_exception
- # 从数据库查询用户信息
- user = get_user(fake_users_db, username=token_data.username)
- if user is None:
- raise credentials_exception
- return user
其中status是fastapi的错误代码,的集合,它定义了常用错误代码:【具体可以看fastapi的starlette的status,如下图:】
HTTPException是fastapi的一个返回类,包含了:状态码,详细描述,和头文件
get_current_user方法中,首先解密token,然后从解密中获取用户名,
如果没有获取到用户名,那么返回HTTPException异常
【
status_code:401,
detail:"Could not validate credentials",
headers:{"WWW-Authenticate": "Bearer"}
】
这个异常401,就代表token超时,
所以前端VUE,登录后,每次请求接口,都需要判断返回值的status_code是否=401
在前端VUE的axios拦截响应的时候,需要加上判断status_code。
前端VUE修改的地方:
一、加载element-plus的ElMessage控件
main.js加载
- import { createApp } from "vue";
- import App from "./App.vue";
- import router from "./router";
- import store from "./store";
- import ElementPlus from 'element-plus';
- import { ElMessage } from 'element-plus';
- import 'element-plus/theme-chalk/index.css';
-
- const app = createApp(App);
- app.use(ElementPlus, { zIndex: 3000, size: 'small' });
- app.provide("$message", ElMessage);
- app.use(store).use(router).mount('#app');
- //createApp(App).use(ElementPlus).use(store).use(router).mount("#app");
二、新增member.js并在store里面挂载
store/index.js代码如下
- import { createStore } from "vuex";
- import member from './modules/member'
-
- export default createStore({
- state: {},
- mutations: {},
- actions: {},
- modules: {
- member:member,
- },
- });
新增的store/modules/member.js代码如下:
- import{getToken,setToken,setTokenType,removeToken} from '@/common/token'
- //挂载api接口组件
- import api from '@/api/api'
- //import { createSocket } from '@/common/websocket'
-
- const user = {
- state: {//定义
- token: getToken(),
- tokentype: '',
- uid:'', //管理员id
- username: '', //管理员名
- fullname: '', //管理员全名
- avatar: '', //管理员头像
- email:'', //管理员邮箱
- permisson:[] //其它备用【例如:权限】
- },
- mutations: {//赋值
- SET_TOKEN: (state, token) => {
- state.token = token
- },
- SET_TOKENTYPE: (state, tokentype) => {
- state.tokentype = tokentype
- },
- SET_UID: (state, uid) => {
- state.uid = uid
- },
- SET_USERNAME: (state, username) => {
- state.username = username
- },
- SET_FULLNAME: (state, fullname) => {
- state.fullname = fullname
- },
- SET_AVATAR: (state, avatar) => {
- state.avatar = avatar
- },
- SET_EMAIL:(state,email)=>{
- state.email = email
- },
- SET_PERMISSON:(state,permisson)=>{
- state.permisson = permisson
- }
- },
- actions: {//响应方法
- /*GetInfo({ commit, state }) {
- return new Promise((resolve, reject) => {
- getInfo(state.token).then(res => {
- //console.log(res);
- const user = res.data.userInfo; //绑定管理员信息到常量
- const avatar = user.avatar == null ? require("@/assets/img/empty-face.png") : user.avatar;//解析头像地址,没有头像则绑定一张默认头像
- commit('SET_USERNAME', user.username) //绑定姓名
- commit('SET_AVATAR', avatar) //绑定头像
- commit('SET_UID',user.id); //绑定id
- commit('SET_EMAIL',res.data.email); //绑定email
- commit('SET_PERMISSON',res.data.perms); //绑定权限
- resolve(res)
- }).catch(error => {
- reject(error)
- })
- })
- },*/
- Login({commit},userInfo){//访问登录接口
- console.log('开始登录');
- return new Promise((resolve,reject)=>{
- api.login(userInfo).then(res=>{
- console.log(res);
- setToken(res.access_token);
- setTokenType(res.token_type);
- commit('SET_TOKEN',res.access_token)
- commit('SET_TOKENTYPE',res.token_type)
- resolve()
- }).catch(error=>{
- reject(error)
- })
- })
- },
- loginOut({ commit, state }) {//退出登录
- return new Promise((resolve, reject) => {
- console.log(state);
- console.log(reject);
- commit('SET_TOKEN', '');
- commit('SET_TOKENTYPE','')
- commit('SET_UID','');
- commit('SET_USERNAME','');
- commit('SET_FULLNAME','');
- commit('SET_AVATAR','');
- commit('SET_EMAIL','');
- commit('SET_ACCOUNT','');
- commit('SET_PERMISSON',[]);
- removeToken();
- resolve();
- })
- },
- }
- }
- export default user
三、新增src/common/token.js并在member.js里面挂载
token.js使用js_cookie保存token,代码如下:
- import Cookies from 'js-cookie';
- const TokenKey = 'my-admin-token'
- const TokenType = 'bearer'
- export function getToken() {
- return Cookies.get(TokenKey)
- }
- export function getType() {
- return Cookies.get(TokenType)
- }
-
- export function setToken(token) {
- return Cookies.set(TokenKey, token)
- }
-
- export function setTokenType(tokentype) {
- return Cookies.set(TokenType, tokentype)
- }
-
- export function removeToken() {
- Cookies.remove(TokenKey)
- return Cookies.remove(TokenType)
- }
四、http.js调用axios拦截响应,判断返回status_code数字代码,并跟预设的code对比,判断各种返回状态,如果status_code=401,则token超时,需要重新登录。
修改后的http.js如下:
- // eslint-disable-next-line no-unused-vars
- import Axios from 'axios'
- import store from '@/store'
- import { getToken, getType } from "@/common/token";
- import errorCode from '@/common/errorCode'
- import {ElMessageBox,ElMessage} from 'element-plus'
- import 'element-plus/theme-chalk/src/message.scss'
- //import VueAxios from 'vue-axios'
-
- const BaseURL = 'http://127.0.0.1:9000'
- //创建http对象
- let http = Axios.create({
- baseURL: BaseURL,
- headers: {
- //增加了表单application/x-www-form-urlencoded格式<<<<<<<<<<<<<注意这里
- 'Content-Type': 'application/x-www-form-urlencoded;application/json;charset=utf-8'
- },
- transformRequest: [function(data) {
- let ret = ''
- for (let it in data) {//解析data并拼接
- ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
- }
- return ret
- }],
- timeout: 10000
- })
-
- ///请求拦截
- http.interceptors.request.use(config => {
- const isToken = (config.headers || {}).isToken === false
- if (getToken() && !isToken) {
- config.headers['Authorization'] = getType() + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
- }
- return config
- }, err => {
- return Promise.reject(err)
- })
- //响应拦截
- http.interceptors.response.use(res => {
- const code = res.data.status_code || 200;
- const msg = errorCode[code] || res.data.detail || errorCode['default']
- if (code === 401) {
- ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
- confirmButtonText: '重新登录',
- cancelButtonText: '取消',
- type: 'warning'
- }).then(() => {
- store.dispatch('loginOut').then(() => {
- location.href = '/index';
- })
- })
- } else if (code === 500) {
- ElMessage.error(msg)
- return Promise.reject(new Error(msg))
- } else if (code !== 200) {
- ElMessage.error(msg)
- return Promise.reject('error')
- } else {
- return res.data
- }
- }, err => {
- return Promise.reject(err)
- })
-
- export default {//可供调用的方法:get,post,其它自行添加
- get(url, params) {
- let config = {
- method: 'get',
- url: url
- }
- if (params) config.params = params
- return http(config)
- },
- post(url, params) {
- let config = {
- method: 'post',
- url: url,
- }
- if (params) config.data = params
- //console.log(config)
- return http(config)
- }
- }
新增一个common/errorCode.js文件,记录和后端约定好的错误返回码:
- export default {
- '401': '认证失败,无法访问系统资源',
- '403': '当前操作没有权限',
- '404': '访问资源不存在',
- '400':'请求错误',
- '408':'请求超时',
- '500':'服务器内部错误',
- '501':'服务未实现',
- '0':'系统未知错误,请反馈给管理员',
- 'default': '系统未知错误,请反馈给管理员'
- }
五、最后修改login/index.vue,当用户输入账号密码,点击登录时,通过
this.$store.dispatch调用store的member的Login方法,实现登录,然后返回首页,代码如下:
- <template>
- <div class="loginContainer">
- <h1>登录</h1>
- <div>
- <el-form :model="loginForm" label-width="0px" class="login_form" :ref="loginForm">
- 用户名:<el-input id="username" class="inputStyle" size="large" v-model="loginForm.username"></el-input>
- <br />
- 密 码:<el-input
- id="password"
- class="inputStyle"
- size="large"
- type="password"
- v-model="loginForm.password"
- autocomplete="off">
- </el-input>
- <br />
- <el-button type="primary" @click="submitForm('loginForm')">登录</el-button>
- <el-button @click="resetForm('loginForm')">重置</el-button>
- </el-form>
-
- </div>
- </div>
- </template>
- <script>
- //挂载api.js组件
- //import Api from '@/api/api.js'
- //import { setToken,setTokenType } from '@/common/token.js'
-
- export default {/* eslint-disable */
-
- data() {
- return {
- loginForm: {
- username: '',
- password: ''
- }
- };
- },
- mounted()
- {
- //const login_Form = ref(null);
- },
- methods: {
- submitForm() {
- //console.log(this.loginForm),,,为什么,VUE会给数组套一层proxy壳,草,怎么想的?,没办法,用JSON.parse转换成可以正常用的数组
- let params = JSON.parse(JSON.stringify(this.loginForm));
- this.$store.dispatch("Login", params).then(() => {
- this.$message.success("登录成功");
- this.$router.push({
- path: this.redirect || "/"
- })
- })
- //console.log(params)
- /*Api.login(params).then((res) => {//访问接口,并传递表单数据
- //console.log(res.access_token);
- //console.log(res.token_type);
- setToken(res.access_token);
- setTokenType(res.token_type);
- //console.log(getToken());
- //console.log(getType());
- }).catch(err=>{
- console.log(err)
- });*/
- },
- resetForm(formName) {
- this.loginForm.username = "";
- this.loginForm.password = "";
- },
- },
- };
- </script>
-
- <style scoped>
- .loginContainer {
- margin: 0 auto;
- width: 600px;
- text-align: center;
- padding-top: 20px;
- padding-bottom: 50px;
- border: 1px solid;
- }
- .loginContainer input {
- margin-bottom: 20px;
- }
- .loginStyle {
- width: 160px;
- height: 40px;
- background: rgb(50, 203, 77);
- color: white;
- font-size: 17px;
- }
- .inputStyle {
- width: 200px;
- height: 60px;
- padding: 5px;
- outline: none;
- }
-
- .inputStyle:focus {
- border: 1px solid rgb(50, 203, 77);
- }
- form {
- position: relative;
- }
- .exchange {
- position: absolute;
- top: 8px;
- right: 65px;
- color: red;
- }
- </style>
嗯,再次向前端大神致敬。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。