fastapi学习记录【七】

代码地址: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 />
            密&nbsp;&nbsp;&nbsp;&nbsp;码:<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>

嗯,再次向前端大神致敬。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值