11.用户认证
11.1 登录认证概述
登录是很多系统的基本功能, 有些页面需要登录之后才能进行访问. 实现这一功能的方案大体为:
- 首先进行登录, 登录成功后, 给前端(浏览器)返回一个值"xxxx"(session或者token)
- 前端(浏览器)去访问需要登录的页面(如用户信息页面)时, 会带上上面值"xxxx"
- (后端)服务器根据传入的值"xxxx"获取到这个值对应的用户是哪一个, 那么就返回这个用户的信息
上面的方案再根据 返回值"xxx" 的方式不同, 可以细分为两种:
- session:
- 用户登录后, 在服务器这一方会生成一个随机值
session_id, 把session_id和用户的唯一标识(如user_id)映射起来保存, 可以保存在数据库中或者内存中, 然后给前端返回这个session_id - 前端下次请求时, 带上这个
session_id, 后端可以根据这个session_id找到对应的user_id, 即可知道请求的是哪个用户的信息了
但是这种方式由于需要服务器端保存这个session_id, 因此会衍生出一些问题, 如:
a. 保存session_id需要耗费服务器资源
b. 当业务量比较大需要多台机器进行负载均衡, 统一提供服务时, 多台机器的session_id需要进行同步, 否则跨机器访问时就获取不到用户了
于是就慢慢发展出了第二种方式: token
- token:
- 用户登录后, 服务器端根据
user_id和秘钥进行签名加密, 直接返回给浏览器, 不进行保存操作 - 前端下次请求时, 带上这个
token, 服务器端对这个token进行解密, 获取到解密后的user_id, 即可知道请求的是哪个用户的信息了
基于token的登录认证解决了基于session方式带来的问题, 已经是现在的首选方案了。
11.2 OAuth2
既然采用了上述的token方案, 那么如何进行token的加密和解密? 后端返回token的格式是什么? 前端访问时token是放在url参数中, 还是请求头中或者请求体中? 后端如何去提取请求头中的token?
OAuth2就是对上述具体问题的一套规范的解决方案。可以使用OAuth2PasswordBearer类来实现OAuth2的功能, 使用的是OAuth2中的一种认证方案, 通过bearer token来携带token, 具体做法就是:
在请求头中添加参数Authorization, 其值为Bearer和token中间使用空格连接形成的字符串, 如Bearer token_string, 注意, Authorization和Bearer都是规范中固定的写法, 不可修改。
a.oauth2_scheme(提取token)
在 FastAPI 里,oauth2_scheme 是一个 OAuth2PasswordBearer类的 实例,它干了两件核心的事情:
- 告诉框架: “本接口(乃至整个依赖链)期望请求在 Authorization: Bearer 头里带一只 JWT。”
- 自动把
<token>提取出来并返回给依赖函数,如果没带或格式不对,直接抛 401 + WWW-Authenticate 头,省得自己再写一堆校验。 - 代码层面:
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login") # tokenUrl 可以随便写,仅用于 OpenAPI 文档
- 参数
tokenUrl="login"并不会真正发请求,它只会在 Swagger/OpenAPI 页面 里生成一个「Authorize」按钮,告诉前端“去这个路径换 token”。 - 真正运行时,
oauth2_scheme就是一个 callable(依赖函数),FastAPI 会把它注入到路径操作函数或者嵌套依赖里:
async def verify_token(token: str = Depends(oauth2_scheme)):
...
执行顺序:
- 请求进来 →
- FastAPI 先调用
oauth2_scheme(request)→ - 它检查请求头有没有
Authorization: Bearer xxxxx- 有:把
xxxxx原样返回(字符串) - 没有 / 格式不对:立即抛
HTTPException(401, headers={"WWW-Authenticate": "Bearer"}),不会进入你的业务代码。
- 有:把
依赖返回的token值,就是请求头中的token值。
一句话总结: oauth2_scheme 是 FastAPI 提供的「Bearer 令牌提取器 + 401 守门员」,让你专注写 JWT 解码逻辑,而不用自己反复解析请求头和做错误响应。
token的生成规则和验证规则, 不属于OAuth2的范畴, 使用JWT
11.3 JWT认证

JWT全称是Json Web Token,在python中我们一般使用PyJWT这个包实现JWT的编解码操作
安装依赖:
pip install PyJWT
a.FastAPI + JWT 认证核心概念
- token字符串组成 Header.Payload.Signature
- Header:声明类型与签名算法(如 HS256)
- Payload:业务声明(sub、exp、iat、自定义字段等)
- Signature:用服务端密钥对「Header+Payload」签名,防篡改
- 典型认证流程(无刷新令牌场景) ① 用户拿用户名/密码 → ② 服务端校验 → ③ 签发 AccessToken → ④ 客户端在后续请求 Header 中携带 Authorization: Bearer
- 刷新令牌(RefreshToken)场景 AccessToken 有效期短(如 30 min),RefreshToken 长(如 7 天)。 当 AccessToken 过期,客户端用 RefreshToken 请求 /refresh,服务端验证后颁发新 AccessToken,无需再次登录。
- FastAPI 官方生态
- python-jose:JWT 编解码(编码:jwt.encode() 解码:jwt.decode())
- passlib:密码哈希(如 bcrypt)
- 中间件/依赖注入:把「解析并验证 JWT」封装成可复用的依赖函数,路由加依赖即可
b.基本使用
步骤一:首先在.env文件中配置jwt环境变量
SECRET_KEY=miyao # 密钥,应为至少32位随机字符串
ALGORITHM=HS256 # 加密算法
ACCESS_TOKEN_EXPIRE_MINUTES=240 # 过期时间
步骤二:在security.py中编写生成与校验jwt令牌的函数
# 导入的包.....
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/login")
def create_access_token(data: dict) -> str:
to_encode = data.copy()
expire = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
# JWT编码,函数参数分别为data字典,密钥,加密算法
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
return encoded_jwt
def verify_token(token: str = Depends(oauth2_scheme)):
"""验证 JWT 令牌"""
try:
# 进行解码,拿到data字典
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
except JWTError:
return None # 若出现错误返回None,这会导致get_current_user依赖注入函数因拿不到用户id报错
步骤三:在登录视图中,当账号密码校验通过后create_access_token()函数生成jwt令牌并以json数据形式返回
from src.app.core.security import verify_password, create_access_token
def login(db, data):
user = crud.get_user_by_phone(db, data)
if user:
hashed_password = user.password
# 调用verify_password函数校验密码
if verify_password(data.password, hashed_password):
data = {"sub": user.user_id} # 将user_id作为sub的值来进行加密,注意:sub的值只能是字符串,不可为整型
token = create_access_token(data=data)
return {
"token": token, # 在json中返回给客户端
"userId": user.user_id,
"phoneNumber": user.phone_number,
}
else:
raise HTTPException(status_code=400, detail="用户名或密码错误")
else:
raise HTTPException(status_code=400, detail="用户名或密码错误")
步骤四:在get_current_user()中通过依赖注入使用verify_token()函数
from src.app.core.security import verify_token
# 全局认证依赖,通过依赖注入注入verify_token函数,解码jwt令牌并拿到解码得到的payload
def get_current_user(payload=Depends(verify_token), db: Session = Depends(get_db)):
"""
通过验证 Authorization header 中的 token 获取当前用户。
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
user_id = payload.get("sub") # 从payload中拿到sub对应的user_id值
if user_id is None:
raise credentials_exception
user = get_user(db, user_id) # 通过拿到的user_id值进行数据库查询,拿到当前登录用户对象
if user is None:
raise credentials_exception
return user
except Exception:
raise credentials_exception
步骤五:在路由中添加全局认证依赖来保护接口
from fastapi import APIRouter, Depends
from src.app.core.database import get_db
from sqlalchemy.orm import Session
from src.app.core.dependencies import get_current_user
from typing import Annotated
person = APIRouter()
@person.get('/persons/self')
# 通过依赖注入将get_current_user函数注入,current_user是get_current_user函数返回的对象
def get_person_self(current_user: Annotated[User, Depends(get_current_user)], db: Session = Depends(get_db)):
return service.get_person_self(current_user, db)
整体流程是这样的:
- 访问受保护的接口的请求到来
- 会先执行注入的get_current_user函数,因为get_current_user函数依赖了verify_token函数,所以会先拿到请求头中的token进行验证。
- 若验证未通过,直接报错,不会执行业务函数,若通过,在get_current_user函数中根据解码得到的user_id进行数据库查询拿到对应的用户对象。注入了get_current_user函数的函数就可以直接使用current_user这个对象进行一系列操作了(返回用户信息等)。
未携带令牌(即未携带Authorization请求头):

携带过期令牌(距发放令牌时间已超过设置的时间):

携带正确未过期令牌:

携带错误令牌(令牌被篡改):

11.4 Refresh_token
“Access-Token 只有 5 分钟” ≠ “用户每 5 分钟就要输一次账号密码”。 业界把**「短有效期」和「刷新机制」**搭配在一起,既保证安全,又做到“一次登录,长期可用”。核心就是 Refresh-Token。
a.双令牌分工
| 令牌 | 有效期 | 作用 | 传输频率 | 存储位置 |
|---|---|---|---|---|
| Access-Token | 5–15 min | 真正访问受保护资源 | 高(每次 API) | 内存 / Cookie |
| Refresh-Token | 7–30 天 | 只用来换新的 Access-Token | 低(仅过期时) | httpOnly Cookie / Keychain |
b.自动续期流程(用户无感知)
-
用户首次登录 → 后端返回
{ accessToken: "aaa", refreshToken: "bbb" } -
客户端在内存里保存
accessToken,每次请求带Authorization: Bearer aaa。 -
5 分钟后
aaa过期,接口返回 401。 -
客户端检测到 401,自动用
refreshToken调用POST /refresh Body: { refreshToken "bbb" } -
后端验证
bbb→ 生成新accessToken(+ 可选新refreshToken)→ 返回{ accessToken: "new-aaa", refreshToken: "new-bbb" } -
客户端用新令牌重放刚才失败的请求,用户毫无感知。
c.安全收益
- Access-Token 泄露窗口短(5 min)。
- Refresh-Token 不**随每次业务请求扩散,且可配合 httpOnly、SameSite、轮换、设备绑定进一步降低风险。
- 需要踢人下线时,把对应 Refresh-Token 加入 Redis 黑名单即可,无需让用户重新输密码。
Q: refresh_token 验证通过后是分发新的access_token和refresh_token吗,refresh_token是如何传递的,也是在请求头中吗,为什么不直接将access_token时间设置长一些?
一、refresh_token 验证通过后,不一定同时发新的 refresh_token —— 有两种策略:
| 策略 | 行为 | 安全等级 | 常见度 |
|---|---|---|---|
| 滑动续期(Rotation) | 每次用 refresh_token 都下发 全新一对(access + refresh) | 高(一次性) | OAuth 2.1 / OIDC 推荐 |
| 固定续期 | 只发新 access_token,原 refresh_token 继续用直到过期 | 实现简单 | 老系统 / 对吊销要求低 |
一句话:想“强制下线”“防重放”就选 滑动续期;图简单就固定。
二、refresh_token 怎么传递?
-
统一走 httpOnly、Secure、SameSite=Strict 的 Cookie(最推荐) 好处:前端 JS 摸不到,XSS 偷不走;浏览器会自动带,减少代码。
-
或者放在请求体 / 头里(兼容移动 App、三方前端)
POST /refresh Content-Type: application/json {"refreshToken": "xxxxx"}此时必须 HTTPS + 防 CSRF
三、为什么不直接把 access_token 有效期设成 7 天、30 天?
- 泄露窗口 accessToken 随 每一次业务请求 都带在头里,被代理、日志、CDN 蹭到的概率远高于只偶尔出现的 refresh_token。 5 分钟泄露 → 最多被滥用 5 分钟;7 天泄露 → 用户要倒霉一周。
- 无法提前撤销 JWT 无状态,access_token 一旦签出去,在过期前任何拥有密钥的人都不敢说它是无效的(除非加黑名单,那就失去无状态优势)。 refresh_token 因为使用频率低,可以存数据库 / Redis,想踢就踢。
- 刷新令牌可绑定设备、加指纹 后端保存 refresh_token 时可同时记录 UA、IP、设备 ID,发现异常直接吊销; access_token 要在高并发接口里反复验,做这些成本太高。
- 符合 OAuth 2.1 / OIDC 最佳实践 规范明确区分“短期访问”与“长期续期”,已被大厂验证多年。
d.代码实践
整体流程为:用户登录成功后,服务端同时返回访问令牌和刷新令牌并将刷新令牌的哈希值存储在Redis,用户访问除获取新的访问令牌以外的其他接口时携带访问令牌,当访问令牌过期后,用户携带刷新令牌访问/refresh接口获取新的访问令牌。服务端拿着用户的刷新令牌去Redis取数据,若取到则生成新的访问令牌返回,取不到则报错。
令牌生成与验证代码参考JWT认证章节。
Redis客户端配置与下载安装启动Redis服务参考后面的Redis缓存章节。
# 用户登录路由,router.py
@user.post('/login')
def login(data: UserData, db: Session = Depends(get_db)):
user = crud.get_user_by_phone(data, db)
if user:
hashed_password = user.hashed_password
if verify_password(data.password, hashed_password):
data = {"sub": user.user_id}
# 生成访问令牌
access_token = create_access_token(data=data)
# 生成刷新令牌
refresh_token = str(uuid.uuid4())
# 计算刷新令牌哈希值
refresh_token_hashed = hashlib.sha256(refresh_token.encode()).hexdigest()
# 构建Redis存储的key值
key = f"refresh_token:{refresh_token_hashed}"
# 构建Redis存储的value值
refresh_token_data = {
"user_id": user.user_id
}
# 将刷新令牌存储到Redis
redis_client.setex(key, timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS), json.dumps(refresh_token_data))
return {
"accessToken": access_token,
"refreshToken": refresh_token,
"userId": user.user_id,
"phoneNumber": user.phone_number
}
else:
raise HTTPException(status_code=400, detail="用户名或密码错误")
else:
raise HTTPException(status_code=400, detail="用户名或密码错误")
# 获取新的访问令牌路由
@user.post('/refresh')
def get_access_token(data: RefreshToken):
# 获取请求体中的刷新令牌
refresh_token = data.refreshToken
# 对拿到的刷新令牌计算哈希值
refresh_token_hashed = hashlib.sha256(refresh_token.encode()).hexdigest()
key = f"refresh_token:{refresh_token_hashed}"
# 根据用户传来的刷新令牌从Redis中获取数据,如果能获取到,返回新生成的访问令牌,获取不到则说明令牌错误或已过期被Redis删除
stored_data = redis_client.get(key)
if stored_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="刷新令牌无效或已过期"
)
else:
# 解析拿到的value值并从中获取用户id,根据用户id生成新的访问令牌
token_info = json.loads(stored_data)
user_id = token_info['user_id']
data = {"sub": user_id}
# 生成新访问令牌
access_token = create_access_token(data=data)
return {
"access_token": access_token
}
访问接口进行测试:
登录:

获取新访问令牌:

12.中间件
12.1 什么是 FastAPI 中间件?
FastAPI 中间件是一个函数或类,它能够在请求被应用程序处理之前和响应返回给客户端之前,对请求和响应进行拦截和处理。你可以把它想象成一个“守门人”,在每个请求/响应周期的特定阶段执行一些通用任务。
12.2 中间件的整个流程
当一个请求到达你的 FastAPI 应用时,它会依次通过你添加的所有中间件,形成一个“中间件栈”或“处理链”。整个流程可以分为两个明确的阶段:
- 请求阶段: 请求从客户端发出,依次通过各个中间件,最终到达你的路径操作函数。
- 响应阶段: 从路径操作函数返回的响应,会以相反的顺序再次通过中间件栈,最终返回给客户端。
下图展示了这一完整流程:

12.3 基本使用
a.使用 @app.middleware("http") 装饰器(推荐)
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(process_time)
return response
call_next(request):将请求传递给下一个中间件或路由处理器。- 必须是 异步函数(
async def)。
b.使用 Starlette 的 BaseHTTPMiddleware 类
FastAPI 基于 Starlette,因此可以直接使用其提供的中间件基类。注意,使用这种方法需要我们手动将定义的中间件注册到应用中(在main.py中使用app.add_middleware)
from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class CustomMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 请求前逻辑
print("Before request")
response = await call_next(request)
# 响应后逻辑
print("After response")
return response
app = FastAPI()
app.add_middleware(CustomMiddleware)
注意:
dispatch方法必须是async,且接收request和call_next。
这种方式适合需要复用或封装复杂逻辑的场景。
关于dispatch()方法
当我们使用BaseHTTPMiddleware时,请求进来后会自动执行我们自定义中间件类下的dispatch方法。方法名不能更改。因为我们继承的父类BaseHTTPMiddleware类中有dispatch方法,我们只是将其重写以实现我们的自定义逻辑。
如果你这样写:
class MyMiddleware(BaseHTTPMiddleware):
async def handle(self, request, call_next): # 错误:不是 dispatch
print("This won't run!")
return await call_next(request)
那么你的 handle 方法 永远不会被执行,因为父类只认 dispatch。实际执行的是父类的默认 dispatch(即直接透传),你的逻辑被忽略。
12.4 中间件执行顺序
app.add_middleware(FirstMiddleware)
app.add_middleware(SecondMiddleware)
# 请求流程:First → Second → 路由 → Second → First
使用 @app.middleware("http") 定义的中间件会 在所有 add_middleware 添加的中间件之后执行(即更靠近路由处理)。
注意:装饰器方式的中间件实际上是在内部调用
add_middleware,但插入位置靠后。
13.权限控制

13.1 什么是 RBAC?
RBAC 是一种权限管理模型,通过将权限分配给角色,再将角色分配给用户来实现访问控制。这种模型简化了权限管理,提高了系统的安全性和可维护性。例如系统用户可以是管理员,也可以是普通用户,当是管理员时就可以访问那些需要管理员权限的接口。
13.2 基本使用
# 用户模型
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
user_id = Column(String(36), unique=True, nullable=False, default=lambda: str(uuid.uuid4()))
phone_number = Column(String(11), unique=True, nullable=False)
hashed_password = Column(String(255), nullable=False)
role = Column(String(20), default="user", nullable=False)
# 依赖项
def get_current_user(payload=Depends(verify_token), db: Session = Depends(get_db)):
"""
通过验证 Authorization header 中的 token 获取当前用户。
"""
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
user_id = payload.get("sub")
if user_id is None:
raise credentials_exception
user = get_user(db, user_id)
if user is None:
raise credentials_exception
return user
except Exception:
raise credentials_exception
async def require_admin(current_user=Depends(get_current_user)):
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions. Admin role required."
)
return current_user
# router.py 在路径操作函数中使用来给接口添加权限控制
from dependencies import get_current_user, require_admin
# 注册逻辑.... 在用户认证章节中查看
# 登录逻辑.... 在用户认证章节中查看(登录成功后生成令牌返回给客户端,令牌中包含用户信息,具体参考jwt验证章节)
@user.get('/users')
def get_users(db: Session = Depends(get_db), current_user=Depends(get_current_user)):
return crud.get_users(db)
@user.get('/users/{user_id}')
def get_user(user_id: str, db: Session = Depends(get_db), current_user=Depends(require_admin)):
return crud.get_user(db, user_id)
依赖项中的require_admin函数,又依赖了get_current_user函数,通过get_current_user函数拿到用户对象,再去判断字段role是否为admin,决定是否放行。
router.py的两个路径操作函数中,get_user函数依赖了get_current_user函数,仅需要用户认证而不需要是管理员就可以执行。get_user函数依赖了require_admin函数,所以需要是管理员才能执行。
14.使用Docker部署fastapi项目
14.1 构建Dockerfile
# 基础镜像
FROM python:3.13-slim
# 工作目录
WORKDIR /app
COPY requirements.txt .
# 安装所有依赖
RUN pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
# 拷贝项目
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["uvicorn", "src.app.main:app", "--host", "0.0.0.0", "--port", "8000"]
14.2 基于Dockerfile生成项目镜像
# docker build 告诉 Docker 要构建一个新的镜像,读取 Dockerfile 并按照指令创建镜像
# -t tag(标签)的缩写
# xxx:给镜像起的名字
# . 作用:构建上下文(build context)含义:当前目录(您运行命令时所在的目录)重要性:Docker 会把这个目录下的所有文件发送给 Docker 守护进程进行构建
docker build -t xxx .
14.3 基于镜像运行容器并挂载.env文件
# docker run 创建并启动一个新的容器,基于指定的镜像启动一个可运行的实例
# -d 在后台运行
# -p 8000:8000 格式:主机端口:容器端口 作用:将容器的 8000 端口映射到主机的 8000 端口 效果:您可以通过 http://localhost:8000 访问容器内的应用
# --env-file .env 作用:从 .env 文件加载环境变量到容器中 效果:容器内可以读取 .env 文件中的所有配置
# --name ivf-app 给容器起一个易记的名字
# ivf-patient-info-service 指定要基于哪个镜像创建容器
docker run -d -p 8000:8000 --env-file .env --name ivf-app ivf-patient-info-service
当运行这个命令时:
步骤 1:创建容器
# Docker 会:
# 1. 检查本地是否有 ivf-patient-info-service 镜像
# 2. 从镜像创建一个新的容器实例
# 3. 设置容器名称为 "ivf-app"
步骤 2:配置容器
# Docker 会:
# 1. 从 .env 文件加载所有环境变量
# 2. 设置端口映射 8000:8000
# 3. 配置容器在后台运行
步骤 3:启动应用
# Docker 会:
# 1. 执行 Dockerfile 中的 CMD 命令:
# uvicorn src.app.main:app --host 0.0.0.0 --port 8000
# 2. FastAPI 应用在容器内启动
# 3. 监听 0.0.0.0:8000
14.4 部署到linux主机上
整个流程是这样的:
- 基于Dockerfile构建本地项目镜像
- 基于本地镜像使用docker save命令构建tar文件
- 使用scp将tar文件传到主机上
- 使用docker load命令将主机上的tar文件转成项目镜像并保存在本地镜像仓库
- 编写docker-compose.yml文件
- 编写.env文件
- 使用docker compose up -d命令运行docker-compose.yml文件完成容器的编排启动
- 进入项目容器使用alembic upgrade head 命令进行数据模型的迁移
a.使用docker save将项目镜像转为tar文件
docker save -o <输出文件名.tar> <镜像名:标签>
# 示例
docker save -o my-app-v1.0.tar my-app:v1.0
执行后,当前目录下就会生成 my-app-v1.0.tar 文件。
b.使用scp将tar文件传到主机
基本语法
scp [选项] <源文件路径> <用户名>@<目标主机IP或域名>:<目标路径>
# 例如,~传到用户目录(root目录)
scp my-app-v1.0.tar root@192.168.50.16:~/
c.将tar文件转为本地镜像
基本语法:
docker load -i <文件名.tar>
执行成功后,用 docker images 命令就可以看到被加载进来的镜像。
d.编写docker-compose.yml文件
services:
frontend:
image: nginx:latest
ports:
- "9980:80"
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
restart: unless-stopped
backend:
image: ivf-service:latest
ports:
- "8000:8000"
env_file: .env
depends_on:
- mysql
restart: unless-stopped
mysql:
image: mysql:8.0
ports:
- "3306:3306"
env_file: .env
volumes:
- mysql_data:/var/lib/mysql
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
restart: unless-stopped
minio:
image: minio/minio
ports:
- "9000:9000"
- "9001:9001"
env_file: .env
volumes:
- minio_data:/data
command: server /data --console-address ":9001"
restart: unless-stopped
volumes:
mysql_data:
minio_data:
编写完毕后,执行docker compose up -d命令。容器服务就会自动启动并可以实现内部通信。
e.编写.env文件
我们的docker-compose.yml文件中,容器环境变量的加载方式是env_file: .env,容器启动时会从同级目录的.env文件中加载环境变量。
执行nano .env编写.env文件。
f.进入项目服务容器进行数据模型的迁移
执行docker ps查看正在运行的容器
执行docker exec -it 容器id bash进入容器内部
执行alembic upgrade head进行数据模型的迁移
14.5 将前端静态资源部署到linux主机上
前端会将静态资源打包发来,只需接收解压到本地。
a.将静态资源从本机传输到linux主机上
若docker-compose.yml的静态资源服务是这样配置的,则将静态资源传到与docker-compose.yml文件同级的frontend目录中(手动创建)
services:
frontend:
image: nginx:latest
ports:
- "9980:80"
volumes:
- ./frontend:/usr/share/nginx/html:ro
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- backend
restart: unless-stopped
# 其余服务配置
在本机静态资源所在目录打开终端,使用scp
scp -r dist/* root@主机ip:frontend目录位置
b.编写nginx.conf配置文件
server {
listen 80;
server_name localhost;
client_max_body_size 50M;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
location /api/v1/ {
proxy_pass http://backend:8000/api/v1/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
c.修改nginx用户对frontend目录及其内容的读取权限
若完成上述步骤后,访问服务发现页面打不开,报下面错误,可能是权限问题

修改权限:
chmod -R 755 frontend/ (755后面跟frontend目录位置)
14.6 一些问题解答
a.docker-compose.yml前端服务的配置
ports: - "9980:80"- 端口映射:将主机的
9980端口映射到容器的80端口(Nginx 默认 HTTP 端口)。 - 效果:访问
http://localhost:9980即可访问容器内的 Nginx 服务。
- 端口映射:将主机的
volumes:(卷挂载配置)- 挂载两个路径到容器内:
./frontend:/usr/share/nginx/html:ro:将主机当前目录下的frontend文件夹挂载到容器的/usr/share/nginx/html,权限为只读(ro)。./nginx.conf:/etc/nginx/conf.d/default.conf:ro:将主机上的nginx.conf文件挂载到容器的 Nginx 配置目录,替换默认配置,权限为只读。
- 挂载两个路径到容器内:
depends_on: - backend- 依赖关系:确保
backend服务先启动,再启动frontend服务。
- 依赖关系:确保
restart: unless-stopped- 重启策略:容器退出时自动重启,除非用户手动停止它。
b.为什么使用卷挂载
- 持久化数据 容器内的数据是临时的,容器删除后数据会丢失。卷挂载可将数据存储到主机文件系统,实现持久化。
- 配置管理 将配置文件(如
nginx.conf)挂载到容器,便于版本控制和灵活修改配置,无需进入容器内部。
c.卷挂载的本质是什么
卷挂载的本质是:将主机文件系统的一部分映射到容器内部的文件系统。
- 它可以是目录或文件。
- 挂载后,容器内的对应路径会直接读写主机上的文件。
- 类型包括:
- 绑定挂载(Bind Mount):直接挂载主机上的特定路径(如示例中的
./frontend)。 - 命名卷(Named Volume):由 Docker 管理的存储卷,适用于数据库数据等。
- 绑定挂载(Bind Mount):直接挂载主机上的特定路径(如示例中的
- 底层通过 Linux 的挂载命名空间(Mount Namespace) 实现,对容器透明。
d./usr/share/nginx/html 目录是什么
- 位置 这是 Nginx 容器内部的默认静态网站根目录。
- 作用 Nginx 作为 Web 服务器时,会从此目录读取 HTML、CSS、JavaScript 等静态文件,并响应给客户端。
- 在配置中的意义
- 将本地的
./frontend目录(包含前端构建产物)挂载到此路径,使 Nginx 直接使用主机上的前端文件。 - 这样,前端代码更新时,只需更新主机上的
./frontend目录,无需重新构建或重启容器(Nginx 会自动提供最新文件)。
- 将本地的
Q:/usr/share/nginx/html这个html是一个目录吗,我将frontend目录挂到这个上面,是不是等于html目录下的内容都是本机上frontend目录下的内容了
-
/usr/share/nginx/html确实是一个目录-
这是 Nginx 官方镜像中预定义的一个目录,专门用于存放静态网站文件。
-
您可以通过进入容器来验证:
docker exec -it <container_name> ls -la /usr/share/nginx/html会看到容器内默认可能有
index.html、50x.html等文件。
-
-
挂载效果:覆盖替换 当您执行
./frontend:/usr/share/nginx/html:ro时:- 覆盖关系:主机的
frontend目录完全覆盖容器内的/usr/share/nginx/html目录。 - 内容同步:容器内
html目录下的内容将完全变成您主机上frontend目录的内容。 - 原始内容被隐藏:容器镜像中原本在
/usr/share/nginx/html下的文件会被隐藏,只能看到挂载的内容。
假设主机文件结构:
项目目录/ ├── docker-compose.yml ├── frontend/ │ ├── index.html │ ├── style.css │ └── app.js └── nginx.conf挂载后,容器内部看到的:
/usr/share/nginx/html/ ├── index.html # 实际上是主机的 ./frontend/index.html ├── style.css # 实际上是主机的 ./frontend/style.css └── app.js # 实际上是主机的 ./frontend/app.js重要特性:
- 双向同步:在主机
frontend目录中添加/删除文件,容器内的html目录会立即反映这些变化。 - 实时性:前端文件修改后,刷新浏览器即可看到更新(对于静态文件)。
- 只读限制:由于加了
:ro(read-only),容器内不能修改这些文件,只能从主机修改。
注意事项
- 如果主机
frontend目录是空的,容器内的html目录也会是空的,Nginx 会返回 403 或 404 错误。 - 确保
frontend目录下有index.html或其他默认文档,否则需要配置 Nginx 的index指令。
e.完整响应流程
- 用户发起请求:用户在浏览器输入:
http://localhost:9980 - 请求到达主机:请求到达主机的9980端口,由于端口映射为9980:80,请求被转发到docker容器的80端口
- docker网络层: 主机网络层接收 → Docker 网络转发 → 容器网络接收,docker的docker-proxy进程处理端口转发,请求进入frontend容器的网络命名空间
- Nginx接收请求,Nginx 主进程(master process)接收连接,创建工作进程(worker process)处理该请求
- 读取文件系统,当 Nginx 需要提供
/或/index.html时,Nginx 进程请求读取/usr/share/nginx/html/index.html,由于卷挂载,该路径实际指向主机的./frontend目录,Linux 内核通过挂载点将请求重定向到主机文件系统 - 文件系统访问路径:容器内路径访问 → 内核重定向 → 主机文件系统 → 返回文件内容
- Nginx构建响应:Nginx 获取文件内容后,先添加HTTP响应头,将文件内容作为响应体
- 响应返回路径:容器内Nginx → 容器网络接口 → Docker网络 → 主机网络接口 → 用户浏览器
- 覆盖关系:主机的
14.7 查看docker日志
# 查看实时日志
docker logs -f <容器名或容器ID>
# 查看最近的100行日志
docker logs --tail 100 <容器名>
# 查看特定时间段的日志
docker logs --since 10m <容器名>
# 查看完整日志
docker logs <容器名> > fastapi.log
进入容器内部查看
# 进入容器shell
docker exec -it <容器名> /bin/bash
# 查看应用日志文件(如果FastAPI配置了文件日志)
cd /app
ls -la logs/
cat logs/app.log
# 查看系统日志
cat /var/log/syslog | grep fastapi
评论区 0