FastAPI + Vue 实现 OAuth2 的 jwt token 登录验证

后端使用 FastAPI,前端使用 Vue 来完成登录过程的用 jwt token 实现登录验证功能。

一、后端 FastAPI

1.1 设计用户表

本文采用的是 MySQL 数据库。
首先连接 MySQL 数据库,关于 MySQL 数据库的连接可参见另外一篇文章:FastAPI 连接 MySQL

用户的数据库表如下:

class User(Base):
    __tablename__ = 'users'  # 数据库表名

    username = Column(String(255), primary_key=True, nullable=False, unique=True, index=True)
    hashed_password = Column(String(255), nullable=False)
    name = Column(String(255))
    phone = Column(String(255), nullable=False)

在数据库表中添加一条记录:

  • username:test
  • hashed_password:$2b 12 12 12EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW
  • name:test
  • phone:11111111111

对用户的查询功能实现:

from sqlalchemy.orm import Session

def get_user(db: Session, username: str) -> mysql_po.User:
    """
    根据username获取该用户
    """
    return db.query(mysql_po.User).filter(mysql_po.User.username == username).first()

1.2 连接 Redis

首先启动 redis:命令行中输入 redis-server
在 python 中连接 redis:

async def get_redis() -> StrictRedis:
    """
    获取Redis对象
    """
    redis = StrictRedis(host=redis_conf.HOST,
                        port=redis_conf.PORT,
                        db=redis_conf.db,
                        password=redis_conf.PASSWORD)
    try:
        yield redis
    finally:
        redis.close()

1.3 实现验证类

采用 FastAPI 自带的 OAuth 实现即可:

from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseSettings
from sqlalchemy.orm import Session
from passlib.context import CryptContext
from datetime import datetime, timedelta
from jose import jwt

class UserTokenConfig(BaseSettings):
    """对用户登录时处理token的相关配置"""
    SECRET_KEY: str = "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"  # 通过命令行 `openssl rand -hex 32` 可以生成该安全密钥
    ALGORITHM: str = "HS256"
    ACCESS_TOKEN_EXPIRE_MINUTES: int = 30

user_token_conf = UserTokenConfig()

__oauth2_scheme = OAuth2PasswordBearer(tokenUrl="api/login/token")
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')

def get_pwd_hash(pwd):
    return pwd_context.hash(pwd)

def authenticate_user(mysql_db: Session, username: str, password: str) -> Union[bool, mysql_po.User]:
    """
    验证用户合法性
    :return: 若验证成功则返回 User 对象;若验证失败则返回 False
    """
    user = get_user(mysql_db, username)
    if not user:
        return False
    if not pwd_context.verify(password, user.hashed_password):
        return False
    return user

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, user_token_conf.SECRET_KEY, algorithm=user_token_conf.ALGORITHM)
    return encoded_jwt


async def fetch_token(request: Request) -> Optional[str]:
    """
    在 request 中获取到 oauth2 认证所需要的信息
    :return: 取出的 token
    """
    try:
        token = await __oauth2_scheme(request)
    except HTTPException as e:
        raise TokenVerifyException(e.detail)
    return token


def verify_token(token: str = Depends(fetch_token),
                 redis: StrictRedis = Depends(get_redis)) -> str:
    """
    根据请求头部的 Authorization 字段,在 Redis 进行验证并获取用户的 username
    :return: 验证成功时返回用户的 username,验证失败则抛出异常
    :raise: TokenVerifyException 验证失败时抛出此异常
    """
    # 验证 token 是否为空
    if token is None:
        raise TokenVerifyException()
    # 查询 redis_db 进行验证
    username = redis.get(token)
    if username is None:
        raise TokenVerifyException()
    return username
  • TokenVerifyException 为自定义异常。

1.4 controller 实现

from fastapi import APIRouter, Depends, HTTPException, status,
from redis import StrictRedis
from sqlalchemy.orm import Session

router = APIRouter()

class Token(BaseModel):
    code: int = 0
    access_token: str = Field(description='本次登录的token')
    token_type: str = Field(default='Bearer', description='token的类型,统一为 Bearer')

@router.post("/token",
                   response_model=Token,
                   summary='登录接口,获取 token',
                   description="""采用OAuth2.0认证协议,登录时获取用户的token,
                                使用 x-www-form-urlencoded 格式, 
                                在 Request Body 提交 username 和 password 即可获得本次用户登录的token,
                                并会被缓存到 Redis 中保持一定的时间段""")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),
                                 mysql_db: Session = Depends(get_mysql_db),
                                 redis_db: StrictRedis = Depends(get_redis)):
    user = authenticate_user(mysql_db, form_data.username, form_data.password)  # 查询数据库进行用户验证
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    # 根据 username 生成 token
    access_token_expires = timedelta(minutes=user_token_conf.ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )

    redis_db.set(access_token, user.username, ex=user_token_conf.ACCESS_TOKEN_EXPIRE_MINUTES * 60)  # 设置 redis_db 缓存

    return Token(
        code=0,
        access_token=access_token,
        token_type='Bearer'
    )

1.5 测试

运行 FastAPI 后进入 /docs 页面,会看到上面有一个 Authorize 用来测试认证过程:

输入用户名 test、密码 secret 后发现可以认证成功。

二、前端 Vue

2.1 添加拦截器

在 main.ts 中添加如下 axios 的拦截器:

axios.defaults.baseURL = process.env.VUE_APP_SERVER;  // 在使用 axios 发送请求时全局的base域
axios.defaults.headers.common['Authorization'] = "Bearer " + store.state.localUser.access_token;


// 添加请求拦截器
axios.interceptors.request.use(function(config: any) {
    config.headers.Authorization = "Bearer " + store.state.localUser.access_token;
    console.info(config);
    return config;
}, function(error: any) {
    return Promise.reject(error);
});

2.2 引入 vuex

按照官网安装 vuex 后,在 store/index.ts 中使用 vuex:

import { createStore } from 'vuex'

declare let SessionStorage: any;
const USER = 'USER';

const store = createStore({
  state: {
    localUser: SessionStorage.get(USER) || {}  // 表示当前登录的用户
  },
  mutations: {
    setLocalUser(state, user) {
      state.localUser = user;
      SessionStorage.set(USER, user);  // 将该用户的信息存放于 SessionStorage 中
    }
  },
  actions: {
  },
  modules: {
  }
})


export default store;

2.3 加入登录页面

以 antdv 为例:

<a-modal
        title="登录"
        v-model:visible="loginModalVisible"
        :confirm-loading="loginModalLoading"
        @ok="login"
    >
      <a-form :model="loginUser" :label-col="{ span: 6 }" :wrapper-col="{ span: 18 }">
        <a-form-item label="登录名">
          <a-input v-model:value="loginUser.loginName" />
        </a-form-item>
        <a-form-item label="密码">
          <a-input v-model:value="loginUser.password" type="password" />
        </a-form-item>
      </a-form>
</a-modal>

脚本处编写函数:

<script lang="ts">
import {computed, defineComponent, ref} from "vue";
import axios from 'axios';
import store from "@/store";
import qs from 'qs';


export default defineComponent({
  name: 'the-header',
  setup () {
    // 登录后保存
    const currUser = computed(() => store.state.localUser);

    // 用来登录
    const loginUser = ref({
      loginName: "test",
      password: "test",
    });
    const loginModalVisible = ref(false);
    const loginModalLoading = ref(false);
    const showLoginModal = () => {
      loginModalVisible.value = true;
    };

    // 登录
    const login = () => {
      console.log("开始登录");
      loginModalLoading.value = true;
      axios.post('/api/login/token', qs.stringify({username: loginUser.value.loginName,
                                                  password: loginUser.value.password}), 
                                                  {headers: {'Content-Type': 'application/x-www-form-urlencoded'}})
            .then((response) => {
        loginModalLoading.value = false;
        const respData = response.data;
        const user = {
          'access_token': respData.access_token,
          'name': '',
          'id': ''
        }
        if (respData.code === 0) {
          loginModalVisible.value = false;
          user.id = loginUser.value.loginName;
          user.name = respData.student_name;
          store.commit("setLocalUser", user);
          message.success("登录成功!");
        } else {
          message.error(respData.msg);
        }
      });
    };
	return {
      loginModalVisible,
      loginModalLoading,
      showLoginModal,
      loginUser,
      login,
      currUser,
    }
}

由此便可打通前后端并实现 jwt token 的认证。

  • 6
    点赞
  • 43
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
FastAPI是一个快速构建API的Python框架,其主要特点是高性能、易于使用和快速编写。在FastAPI中,可以使用Python的异步功能来提高性能,同时可以使用Pydantic来实现数据验证和文档生成。下面是FastAPI编写登录注册页面前后端代码的简要介绍。 后端代码: 首先需要安装FastAPI和uvicorn,可以使用pip进行安装: ``` pip install fastapi uvicorn ``` 然后创建一个main.py文件,编写以下代码: ```python from fastapi import FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from passlib.context import CryptContext from datetime import datetime, timedelta from jose import JWTError, jwt app = FastAPI() # 设置跨域资源共享(CORS)中间件,允许前端跨域访问 app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # 设置加密算法 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 设置JWT相关参数 SECRET_KEY = "secret" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # 定义用户模型类 class User(): def __init__(self, username, hashed_password): self.username = username self.hashed_password = hashed_password # 假设存在一个用户列表users users = [ User(username="user1", hashed_password=pwd_context.hash("password1")), User(username="user2", hashed_password=pwd_context.hash("password2")), ] # 获取用户信息函数 def get_user(username: str): for user in users: if user.username == username: return user return None # 验证密码函数 def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) # 创建access_token函数 def create_access_token(data: dict, expires_delta: timedelta = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt # 定义OAuth2密码模式对象 oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login") # 定义登录路由 @app.post("/login") async def login(form_data: OAuth2PasswordRequestForm = Depends()): user = get_user(form_data.username) if not user: raise HTTPException(status_code=400, detail="用户名或密码错误") if not verify_password(form_data.password, user.hashed_password): raise HTTPException(status_code=400, detail="用户名或密码错误") access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) access_token = create_access_token( data={"sub": user.username}, expires_delta=access_token_expires ) return {"access_token": access_token, "token_type": "bearer"} # 定义获取用户信息路由 @app.get("/users/me") async def read_users_me(token: str = Depends(oauth2_scheme)): try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise HTTPException(status_code=401, detail="无效凭证") except JWTError: raise HTTPException(status_code=401, detail="无效凭证") user = get_user(username) if user is None: raise HTTPException(status_code=404, detail="用户不存在") return {"username": user.username} ``` 前端代码: 前端代码可以使用Vue.js来实现,需要使用axios库来发送HTTP请求。首先需要安装Vue.js和axios库,可以使用npm进行安装: ``` npm install vue axios ``` 然后创建一个App.vue文件,编写以下代码: ```html <template> <div> <div v-if="!isLoggedIn"> <h3>登录</h3> <form @submit.prevent="login"> <label> 用户名: <input type="text" v-model="username"> </label> <br> <label> 密码: <input type="password" v-model="password"> </label> <br> <button type="submit">登录</button> </form> <p v-if="loginError">{{ loginError }}</p> </div> <div v-else> <h3>欢迎 {{ username }}</h3> <button @click="logout">退出登录</button> </div> </div> </template> <script> import axios from 'axios' export default { data () { return { isLoggedIn: false, username: '', password: '', token: '', loginError: '' } }, methods: { async login () { try { const response = await axios.post('http://localhost:8000/login', { username: this.username, password: this.password }) this.token = response.data.access_token localStorage.setItem('token', this.token) this.isLoggedIn = true } catch (error) { this.loginError = error.response.data.detail } }, logout () { localStorage.removeItem('token') this.isLoggedIn = false this.username = '' this.password = '' this.token = '' this.loginError = '' }, async getUserInfo () { try { const response = await axios.get('http://localhost:8000/users/me', { headers: { Authorization: `Bearer ${this.token}` } }) this.username = response.data.username this.isLoggedIn = true } catch (error) { localStorage.removeItem('token') this.isLoggedIn = false } } }, created () { const token = localStorage.getItem('token') if (token) { this.token = token this.getUserInfo() } } } </script> ``` 以上就是使用FastAPIVue.js编写登录注册页面前后端代码的简要介绍,当然实际项目中还需要考虑更多的安全问题和业务逻辑。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值