后端使用 FastAPI,前端使用 Vue 来完成登录过程的用 jwt token 实现登录验证功能。
本文采用的是 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)
在数据库表中添加一条记录:
对用户的查询功能实现:
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()
首先启动 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()
采用 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
为自定义异常。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'
)
运行 FastAPI 后进入 /docs
页面,会看到上面有一个 Authorize
用来测试认证过程:
输入用户名 test、密码 secret 后发现可以认证成功。
在 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);
});
按照官网安装 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;
以 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 的认证。