当前位置:   article > 正文

fastapi学习记录【七】_fastapi token过期

fastapi token过期

代码地址:https://github.com/wendingming/fastapi-vue-postgresql

昨天我用VUE完善了登录页面,并访问登录接口,获得了token返回值。

今天我们来学习一下,VUE前端,利用store来建立【保存token,判断token有没有过期,过期则弹出提示,跳转到重新登录页面,】这些方法。

唉,学来学去,尼玛的,VUE事情最多。。。

果然,前端都是神,后端一个接口,前端薅成秃头。

首先:改axios调用接口的方法,也就是前面写的http.js。

在其中响应拦截时,判断是否有token过期提示代码,【这个需要和后端约定好,用某个代码代表超时】

还是说一段fastapi代码吧,不然真的都变成VUE学习了。。。

后端fastapi的login.py文件,在我之前有从官方文档复制出来,现在详细解析了一下【主要是加了中文注释说明】,代码如下:

  1. # 导入相关的模块
  2. from datetime import datetime, timedelta
  3. from typing import Optional
  4. from fastapi import Depends, FastAPI, HTTPException, status, Form
  5. from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
  6. from jose import JWTError, jwt
  7. from passlib.context import CryptContext
  8. from pydantic import BaseModel
  9. from starlette.middleware.cors import CORSMiddleware
  10. import json
  11. """定义关于token的相关常量
  12. SECRET_KEY : 用于加密解密的密钥,只允许服务器知道,打死不告诉别人
  13. 可以执行 openssl rand -hex 32 获取一串随机的字符
  14. ALGORITHM : 定义加密解密所使用的算法
  15. ACCESS_TOKEN_EXPIRE_MINUTES : token的有效期
  16. """
  17. SECRET_KEY = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
  18. ALGORITHM = "HS256"
  19. ACCESS_TOKEN_EXPIRE_MINUTES = 30
  20. dict_permisson = [
  21. {
  22. "menu_id":1,
  23. "menu_name":"系统首页",
  24. "parent_id":0,
  25. "pageurl":"/sys/index"
  26. },
  27. {
  28. "menu_id":2,
  29. "menu_name":"菜单管理",
  30. "parent_id":1,
  31. "pageurl":"/sys/list"
  32. },
  33. {
  34. "menu_id":3,
  35. "menu_name":"角色管理",
  36. "parent_id":1,
  37. "pageurl":"/sys/role"
  38. },
  39. {
  40. "menu_id":4,
  41. "menu_name":"管理员管理",
  42. "parent_id":1,
  43. "pageurl":"/sys/admin"
  44. },
  45. {
  46. "menu_id":5,
  47. "menu_name":"用户管理",
  48. "parent_id":0,
  49. "pageurl":"/user/index"
  50. },
  51. {
  52. "menu_id":6,
  53. "menu_name":"会员列表",
  54. "parent_id":5,
  55. "pageurl":"/user/list"
  56. },
  57. ]
  58. json_permisson = json.dumps(dict_permisson)
  59. # 这里定义一个字典,来模拟数据库中的数据
  60. fake_users_db = {
  61. "johndoe": {
  62. "uid": 1,
  63. "username": "johndoe",
  64. "full_name": "John Doe",
  65. "avatar": "https://up.enterdesk.com/2021/edpic/c4/9f/09/c49f090757360f843141fe2bab2cfc8f_1.jpg",
  66. "email": "johndoe@example.com",
  67. "hashed_password": "$2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW",#默认密码secret
  68. "disabled": False,
  69. "permisson": json_permisson
  70. }
  71. }
  72. class Token(BaseModel):
  73. """定义token的数据模型"""
  74. access_token: str
  75. token_type: str
  76. class TokenData(BaseModel):
  77. username: Optional[str] = None
  78. class FormData(BaseModel):
  79. uname: str
  80. passwd: str
  81. class User(BaseModel):
  82. """定义用户的数据模型"""
  83. uid: str
  84. username: str
  85. full_name: Optional[str] = None
  86. avatar: Optional[str] = None
  87. email: Optional[str] = None
  88. disabled: Optional[bool] = None
  89. permisson: Optional[str] = None
  90. class UserInDB(User):
  91. hashed_password: str
  92. # 创建一个加密解密上下文环境(甚至可以不用管这两句话啥意思)
  93. pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
  94. oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
  95. # 实例化一个FastAPI实例
  96. app = FastAPI()
  97. # 设置允许访问的域名
  98. origins = [
  99. "http://localhost",
  100. "http://localhost:8080",
  101. "http://127.0.0.1",
  102. "*"
  103. ] #也可以设置为"*",即为所有。
  104. # 设置跨域传参
  105. app.add_middleware(
  106. CORSMiddleware,
  107. allow_origins=origins, # 设置允许的origins来源
  108. allow_credentials=True,
  109. allow_methods=["*"], # 设置允许跨域的http方法,比如 get、post、put等。
  110. allow_headers=["*"]) # 允许跨域的headers,可以用来鉴别来源等作用。
  111. def verify_password(plain_password, hashed_password):
  112. """验证密码是否正确
  113. :param plain_password: 明文
  114. :param hashed_password: 明文hash值
  115. :return:
  116. """
  117. return pwd_context.verify(plain_password, hashed_password)
  118. def get_password_hash(password):
  119. """获取密码的hash值
  120. :param password: 欲获取hash的明文密码
  121. :return: 返回一个hash字符串
  122. """
  123. return pwd_context.hash(password)
  124. def get_user(db, username: str):
  125. """查询用户
  126. :param db: 模拟的数据库
  127. :param username: 用户名
  128. :return: 返回一个用户的BaseModel(其实就是字典的BaseModel对象,二者可互相转换)
  129. """
  130. if username in db:
  131. user_dict = db[username]
  132. return UserInDB(**user_dict)
  133. def authenticate_user(fake_db, username: str, password: str):
  134. """验证用户
  135. :param fake_db: 存储用户的数据库(这里是上面用字典模拟的)
  136. :param username: 用户名
  137. :param password: 密码
  138. :return:
  139. """
  140. # 从数据库获取用户信息
  141. user = get_user(fake_db, username)
  142. # 如果获取为空,返回False
  143. if not user:
  144. return False
  145. # 如果密码不正确,也是返回False
  146. if not verify_password(password, user.hashed_password):
  147. return False
  148. # 如果存在此用户,且密码也正确,则返回此用户信息
  149. return user
  150. def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
  151. """创建tokens函数
  152. :param data: 对用JWT的Payload字段,这里是tokens的载荷,在这里就是用户的信息
  153. :param expires_delta: 缺省参数,截止时间
  154. :return:
  155. """
  156. # 深拷贝data
  157. to_encode = data.copy()
  158. # 如果携带了截至时间,就单独设置tokens的过期时间
  159. if expires_delta:
  160. expire = datetime.utcnow() + expires_delta
  161. else:
  162. # 否则的话,就默认用15分钟
  163. expire = datetime.utcnow() + timedelta(minutes=15)
  164. to_encode.update({"exp": expire})
  165. # 编码,至此 JWT tokens诞生
  166. encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
  167. return encoded_jwt
  168. async def get_current_user(token: str = Depends(oauth2_scheme)):
  169. """获取当前用户信息,实际上是一个解密token的过程
  170. :param token: 携带的token
  171. :return:
  172. """
  173. credentials_exception = HTTPException(
  174. status_code=status.HTTP_401_UNAUTHORIZED,
  175. detail="Could not validate credentials",
  176. headers={"WWW-Authenticate": "Bearer"},
  177. )
  178. try:
  179. # 解密tokens
  180. payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
  181. # 从tokens的载荷payload中获取用户名
  182. username: str = payload.get("sub")
  183. # 如果没有获取到,抛出异常
  184. if username is None:
  185. raise credentials_exception
  186. token_data = TokenData(username=username)
  187. except JWTError:
  188. raise credentials_exception
  189. # 从数据库查询用户信息
  190. user = get_user(fake_users_db, username=token_data.username)
  191. if user is None:
  192. raise credentials_exception
  193. return user
  194. async def get_current_active_user(current_user: User = Depends(get_current_user)):
  195. """获取当前用户信息,实际上是作为依赖,注入其他路由以使用。
  196. :param current_user:
  197. :return:
  198. """
  199. # 如果用户被禁,抛出异常
  200. if current_user.disabled:
  201. raise HTTPException(status_code=400, detail="Inactive user")
  202. return current_user
  203. #-----修改部分star--------------------
  204. # @app.post("/token", response_model=Token)<----------原本的代码
  205. # async def login_for_access_token(form_data: FormData):<--------------原本的代码
  206. def login_for_access_token(form_data: FormData):
  207. """这里定义了一个接口,路径为 /token, 用于用户申请tokens
  208. :param form_data:
  209. :return:
  210. """
  211. # 首先对用户做出检查
  212. user = authenticate_user(fake_users_db, form_data.uname, form_data.passwd)
  213. if not user:
  214. raise HTTPException(
  215. status_code=status.HTTP_401_UNAUTHORIZED,
  216. detail="Incorrect username or password",
  217. headers={"WWW-Authenticate": "Bearer"},
  218. )
  219. # 定义tokens过期时间
  220. access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
  221. # 创建token
  222. access_token = create_access_token(
  223. data={"sub": user.username}, expires_delta=access_token_expires
  224. )
  225. # 返回token信息,JavaScript接收并存储,用于下次访问
  226. baktoken = {
  227. "access_token": access_token, "token_type": "bearer"
  228. }
  229. #return {"access_token": access_token, "token_type": "bearer"}<----原来
  230. return baktoken
  231. #-----修改部分end--------------------
  232. @app.get("/users/me/", response_model=User)
  233. async def read_users_me(current_user: User = Depends(get_current_active_user)):
  234. """获取当前用户信息
  235. :param current_user:
  236. :return:
  237. """
  238. return current_user
  239. @app.get("/users/me/items/")
  240. async def read_own_items(current_user: User = Depends(get_current_active_user)):
  241. return [{"item_id": "Foo", "owner": current_user.username}]

其中一个方法很重要:get_current_user

相关代码解析如下:

  1. from fastapi import Depends, FastAPI, HTTPException, status, Form
  2. from jose import JWTError, jwt
  3. async def get_current_user(token: str = Depends(oauth2_scheme)):
  4. """获取当前用户信息,实际上是一个解密token的过程
  5. :param token: 携带的token
  6. :return:
  7. """
  8. credentials_exception = HTTPException(
  9. status_code=status.HTTP_401_UNAUTHORIZED,
  10. detail="Could not validate credentials",
  11. headers={"WWW-Authenticate": "Bearer"},
  12. )
  13. try:
  14. # 解密tokens
  15. payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
  16. # 从tokens的载荷payload中获取用户名
  17. username: str = payload.get("sub")
  18. # 如果没有获取到,抛出异常
  19. if username is None:
  20. raise credentials_exception
  21. token_data = TokenData(username=username)
  22. except JWTError:
  23. raise credentials_exception
  24. # 从数据库查询用户信息
  25. user = get_user(fake_users_db, username=token_data.username)
  26. if user is None:
  27. raise credentials_exception
  28. 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加载

  1. import { createApp } from "vue";
  2. import App from "./App.vue";
  3. import router from "./router";
  4. import store from "./store";
  5. import ElementPlus from 'element-plus';
  6. import { ElMessage } from 'element-plus';
  7. import 'element-plus/theme-chalk/index.css';
  8. const app = createApp(App);
  9. app.use(ElementPlus, { zIndex: 3000, size: 'small' });
  10. app.provide("$message", ElMessage);
  11. app.use(store).use(router).mount('#app');
  12. //createApp(App).use(ElementPlus).use(store).use(router).mount("#app");

二、新增member.js并在store里面挂载

store/index.js代码如下

  1. import { createStore } from "vuex";
  2. import member from './modules/member'
  3. export default createStore({
  4. state: {},
  5. mutations: {},
  6. actions: {},
  7. modules: {
  8. member:member,
  9. },
  10. });

新增的store/modules/member.js代码如下:

  1. import{getToken,setToken,setTokenType,removeToken} from '@/common/token'
  2. //挂载api接口组件
  3. import api from '@/api/api'
  4. //import { createSocket } from '@/common/websocket'
  5. const user = {
  6. state: {//定义
  7. token: getToken(),
  8. tokentype: '',
  9. uid:'', //管理员id
  10. username: '', //管理员名
  11. fullname: '', //管理员全名
  12. avatar: '', //管理员头像
  13. email:'', //管理员邮箱
  14. permisson:[] //其它备用【例如:权限】
  15. },
  16. mutations: {//赋值
  17. SET_TOKEN: (state, token) => {
  18. state.token = token
  19. },
  20. SET_TOKENTYPE: (state, tokentype) => {
  21. state.tokentype = tokentype
  22. },
  23. SET_UID: (state, uid) => {
  24. state.uid = uid
  25. },
  26. SET_USERNAME: (state, username) => {
  27. state.username = username
  28. },
  29. SET_FULLNAME: (state, fullname) => {
  30. state.fullname = fullname
  31. },
  32. SET_AVATAR: (state, avatar) => {
  33. state.avatar = avatar
  34. },
  35. SET_EMAIL:(state,email)=>{
  36. state.email = email
  37. },
  38. SET_PERMISSON:(state,permisson)=>{
  39. state.permisson = permisson
  40. }
  41. },
  42. actions: {//响应方法
  43. /*GetInfo({ commit, state }) {
  44. return new Promise((resolve, reject) => {
  45. getInfo(state.token).then(res => {
  46. //console.log(res);
  47. const user = res.data.userInfo; //绑定管理员信息到常量
  48. const avatar = user.avatar == null ? require("@/assets/img/empty-face.png") : user.avatar;//解析头像地址,没有头像则绑定一张默认头像
  49. commit('SET_USERNAME', user.username) //绑定姓名
  50. commit('SET_AVATAR', avatar) //绑定头像
  51. commit('SET_UID',user.id); //绑定id
  52. commit('SET_EMAIL',res.data.email); //绑定email
  53. commit('SET_PERMISSON',res.data.perms); //绑定权限
  54. resolve(res)
  55. }).catch(error => {
  56. reject(error)
  57. })
  58. })
  59. },*/
  60. Login({commit},userInfo){//访问登录接口
  61. console.log('开始登录');
  62. return new Promise((resolve,reject)=>{
  63. api.login(userInfo).then(res=>{
  64. console.log(res);
  65. setToken(res.access_token);
  66. setTokenType(res.token_type);
  67. commit('SET_TOKEN',res.access_token)
  68. commit('SET_TOKENTYPE',res.token_type)
  69. resolve()
  70. }).catch(error=>{
  71. reject(error)
  72. })
  73. })
  74. },
  75. loginOut({ commit, state }) {//退出登录
  76. return new Promise((resolve, reject) => {
  77. console.log(state);
  78. console.log(reject);
  79. commit('SET_TOKEN', '');
  80. commit('SET_TOKENTYPE','')
  81. commit('SET_UID','');
  82. commit('SET_USERNAME','');
  83. commit('SET_FULLNAME','');
  84. commit('SET_AVATAR','');
  85. commit('SET_EMAIL','');
  86. commit('SET_ACCOUNT','');
  87. commit('SET_PERMISSON',[]);
  88. removeToken();
  89. resolve();
  90. })
  91. },
  92. }
  93. }
  94. export default user

三、新增src/common/token.js并在member.js里面挂载

token.js使用js_cookie保存token,代码如下:

  1. import Cookies from 'js-cookie';
  2. const TokenKey = 'my-admin-token'
  3. const TokenType = 'bearer'
  4. export function getToken() {
  5. return Cookies.get(TokenKey)
  6. }
  7. export function getType() {
  8. return Cookies.get(TokenType)
  9. }
  10. export function setToken(token) {
  11. return Cookies.set(TokenKey, token)
  12. }
  13. export function setTokenType(tokentype) {
  14. return Cookies.set(TokenType, tokentype)
  15. }
  16. export function removeToken() {
  17. Cookies.remove(TokenKey)
  18. return Cookies.remove(TokenType)
  19. }

四、http.js调用axios拦截响应,判断返回status_code数字代码,并跟预设的code对比,判断各种返回状态,如果status_code=401,则token超时,需要重新登录。

修改后的http.js如下:

  1. // eslint-disable-next-line no-unused-vars
  2. import Axios from 'axios'
  3. import store from '@/store'
  4. import { getToken, getType } from "@/common/token";
  5. import errorCode from '@/common/errorCode'
  6. import {ElMessageBox,ElMessage} from 'element-plus'
  7. import 'element-plus/theme-chalk/src/message.scss'
  8. //import VueAxios from 'vue-axios'
  9. const BaseURL = 'http://127.0.0.1:9000'
  10. //创建http对象
  11. let http = Axios.create({
  12. baseURL: BaseURL,
  13. headers: {
  14. //增加了表单application/x-www-form-urlencoded格式<<<<<<<<<<<<<注意这里
  15. 'Content-Type': 'application/x-www-form-urlencoded;application/json;charset=utf-8'
  16. },
  17. transformRequest: [function(data) {
  18. let ret = ''
  19. for (let it in data) {//解析data并拼接
  20. ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&'
  21. }
  22. return ret
  23. }],
  24. timeout: 10000
  25. })
  26. ///请求拦截
  27. http.interceptors.request.use(config => {
  28. const isToken = (config.headers || {}).isToken === false
  29. if (getToken() && !isToken) {
  30. config.headers['Authorization'] = getType() + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
  31. }
  32. return config
  33. }, err => {
  34. return Promise.reject(err)
  35. })
  36. //响应拦截
  37. http.interceptors.response.use(res => {
  38. const code = res.data.status_code || 200;
  39. const msg = errorCode[code] || res.data.detail || errorCode['default']
  40. if (code === 401) {
  41. ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', {
  42. confirmButtonText: '重新登录',
  43. cancelButtonText: '取消',
  44. type: 'warning'
  45. }).then(() => {
  46. store.dispatch('loginOut').then(() => {
  47. location.href = '/index';
  48. })
  49. })
  50. } else if (code === 500) {
  51. ElMessage.error(msg)
  52. return Promise.reject(new Error(msg))
  53. } else if (code !== 200) {
  54. ElMessage.error(msg)
  55. return Promise.reject('error')
  56. } else {
  57. return res.data
  58. }
  59. }, err => {
  60. return Promise.reject(err)
  61. })
  62. export default {//可供调用的方法:get,post,其它自行添加
  63. get(url, params) {
  64. let config = {
  65. method: 'get',
  66. url: url
  67. }
  68. if (params) config.params = params
  69. return http(config)
  70. },
  71. post(url, params) {
  72. let config = {
  73. method: 'post',
  74. url: url,
  75. }
  76. if (params) config.data = params
  77. //console.log(config)
  78. return http(config)
  79. }
  80. }

新增一个common/errorCode.js文件,记录和后端约定好的错误返回码:

  1. export default {
  2. '401': '认证失败,无法访问系统资源',
  3. '403': '当前操作没有权限',
  4. '404': '访问资源不存在',
  5. '400':'请求错误',
  6. '408':'请求超时',
  7. '500':'服务器内部错误',
  8. '501':'服务未实现',
  9. '0':'系统未知错误,请反馈给管理员',
  10. 'default': '系统未知错误,请反馈给管理员'
  11. }

五、最后修改login/index.vue,当用户输入账号密码,点击登录时,通过

this.$store.dispatch调用store的member的Login方法,实现登录,然后返回首页,代码如下:
  1. <template>
  2. <div class="loginContainer">
  3. <h1>登录</h1>
  4. <div>
  5. <el-form :model="loginForm" label-width="0px" class="login_form" :ref="loginForm">
  6. 用户名:<el-input id="username" class="inputStyle" size="large" v-model="loginForm.username"></el-input>
  7. <br />
  8. &nbsp;&nbsp;&nbsp;&nbsp;码:<el-input
  9. id="password"
  10. class="inputStyle"
  11. size="large"
  12. type="password"
  13. v-model="loginForm.password"
  14. autocomplete="off">
  15. </el-input>
  16. <br />
  17. <el-button type="primary" @click="submitForm('loginForm')">登录</el-button>
  18. <el-button @click="resetForm('loginForm')">重置</el-button>
  19. </el-form>
  20. </div>
  21. </div>
  22. </template>
  23. <script>
  24. //挂载api.js组件
  25. //import Api from '@/api/api.js'
  26. //import { setToken,setTokenType } from '@/common/token.js'
  27. export default {/* eslint-disable */
  28. data() {
  29. return {
  30. loginForm: {
  31. username: '',
  32. password: ''
  33. }
  34. };
  35. },
  36. mounted()
  37. {
  38. //const login_Form = ref(null);
  39. },
  40. methods: {
  41. submitForm() {
  42. //console.log(this.loginForm),,,为什么,VUE会给数组套一层proxy壳,草,怎么想的?,没办法,用JSON.parse转换成可以正常用的数组
  43. let params = JSON.parse(JSON.stringify(this.loginForm));
  44. this.$store.dispatch("Login", params).then(() => {
  45. this.$message.success("登录成功");
  46. this.$router.push({
  47. path: this.redirect || "/"
  48. })
  49. })
  50. //console.log(params)
  51. /*Api.login(params).then((res) => {//访问接口,并传递表单数据
  52. //console.log(res.access_token);
  53. //console.log(res.token_type);
  54. setToken(res.access_token);
  55. setTokenType(res.token_type);
  56. //console.log(getToken());
  57. //console.log(getType());
  58. }).catch(err=>{
  59. console.log(err)
  60. });*/
  61. },
  62. resetForm(formName) {
  63. this.loginForm.username = "";
  64. this.loginForm.password = "";
  65. },
  66. },
  67. };
  68. </script>
  69. <style scoped>
  70. .loginContainer {
  71. margin: 0 auto;
  72. width: 600px;
  73. text-align: center;
  74. padding-top: 20px;
  75. padding-bottom: 50px;
  76. border: 1px solid;
  77. }
  78. .loginContainer input {
  79. margin-bottom: 20px;
  80. }
  81. .loginStyle {
  82. width: 160px;
  83. height: 40px;
  84. background: rgb(50, 203, 77);
  85. color: white;
  86. font-size: 17px;
  87. }
  88. .inputStyle {
  89. width: 200px;
  90. height: 60px;
  91. padding: 5px;
  92. outline: none;
  93. }
  94. .inputStyle:focus {
  95. border: 1px solid rgb(50, 203, 77);
  96. }
  97. form {
  98. position: relative;
  99. }
  100. .exchange {
  101. position: absolute;
  102. top: 8px;
  103. right: 65px;
  104. color: red;
  105. }
  106. </style>

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/995123
推荐阅读
相关标签
  

闽ICP备14008679号