00:00

文章目录

加载目录中...

FastAPI总结文档(下)

11.用户认证

11.1 登录认证概述

登录是很多系统的基本功能, 有些页面需要登录之后才能进行访问. 实现这一功能的方案大体为:

  1. 首先进行登录, 登录成功后, 给前端(浏览器)返回一个值"xxxx"(session或者token)
  2. 前端(浏览器)去访问需要登录的页面(如用户信息页面)时, 会带上上面值"xxxx"
  3. (后端)服务器根据传入的值"xxxx"获取到这个值对应的用户是哪一个, 那么就返回这个用户的信息

上面的方案再根据 返回值"xxx" 的方式不同, 可以细分为两种:

  • session:
  1. 用户登录后, 在服务器这一方会生成一个随机值session_id, 把session_id和用户的唯一标识(如user_id)映射起来保存, 可以保存在数据库中或者内存中, 然后给前端返回这个session_id
  2. 前端下次请求时, 带上这个session_id, 后端可以根据这个session_id找到对应的user_id, 即可知道请求的是哪个用户的信息了

但是这种方式由于需要服务器端保存这个session_id, 因此会衍生出一些问题, 如:

a. 保存session_id需要耗费服务器资源

b. 当业务量比较大需要多台机器进行负载均衡, 统一提供服务时, 多台机器的session_id需要进行同步, 否则跨机器访问时就获取不到用户了

于是就慢慢发展出了第二种方式: token

  • token:
  1. 用户登录后, 服务器端根据user_id和秘钥进行签名加密, 直接返回给浏览器, 不进行保存操作
  2. 前端下次请求时, 带上这个token, 服务器端对这个token进行解密, 获取到解密后的user_id, 即可知道请求的是哪个用户的信息了

基于token的登录认证解决了基于session方式带来的问题, 已经是现在的首选方案了。

11.2 OAuth2

既然采用了上述的token方案, 那么如何进行token的加密和解密? 后端返回token的格式是什么? 前端访问时token是放在url参数中, 还是请求头中或者请求体中? 后端如何去提取请求头中的token?

OAuth2就是对上述具体问题的一套规范的解决方案。可以使用OAuth2PasswordBearer类来实现OAuth2的功能, 使用的是OAuth2中的一种认证方案, 通过bearer token来携带token, 具体做法就是:

在请求头中添加参数Authorization, 其值为Bearertoken中间使用空格连接形成的字符串, 如Bearer token_string, 注意, AuthorizationBearer都是规范中固定的写法, 不可修改

a.oauth2_scheme(提取token)

在 FastAPI 里,oauth2_scheme 是一个 OAuth2PasswordBearer类的 实例,它干了两件核心的事情:

  1. 告诉框架: “本接口(乃至整个依赖链)期望请求在 Authorization: Bearer 头里带一只 JWT。”
  2. 自动把 <token> 提取出来并返回给依赖函数,如果没带或格式不对,直接抛 401 + WWW-Authenticate 头,省得自己再写一堆校验。
  3. 代码层面:
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)):
    ...

执行顺序:

  1. 请求进来 →
  2. FastAPI 先调用 oauth2_scheme(request)
  3. 它检查请求头有没有 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 认证核心概念

  1. token字符串组成 Header.Payload.Signature
    • Header:声明类型与签名算法(如 HS256)
    • Payload:业务声明(sub、exp、iat、自定义字段等)
    • Signature:用服务端密钥对「Header+Payload」签名,防篡改
  2. 典型认证流程(无刷新令牌场景) ① 用户拿用户名/密码 → ② 服务端校验 → ③ 签发 AccessToken → ④ 客户端在后续请求 Header 中携带 Authorization: Bearer
  3. 刷新令牌(RefreshToken)场景 AccessToken 有效期短(如 30 min),RefreshToken 长(如 7 天)。 当 AccessToken 过期,客户端用 RefreshToken 请求 /refresh,服务端验证后颁发新 AccessToken,无需再次登录。
  4. 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)

整体流程是这样的:

  1. 访问受保护的接口的请求到来
  2. 会先执行注入的get_current_user函数,因为get_current_user函数依赖了verify_token函数,所以会先拿到请求头中的token进行验证。
  3. 若验证未通过,直接报错,不会执行业务函数,若通过,在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.自动续期流程(用户无感知)

  1. 用户首次登录 → 后端返回

    { accessToken: "aaa", refreshToken: "bbb" }
    
  2. 客户端在内存里保存 accessToken,每次请求带 Authorization: Bearer aaa

  3. 5 分钟后 aaa 过期,接口返回 401。

  4. 客户端检测到 401,自动refreshToken 调用

    POST /refresh
    Body: { refreshToken "bbb" }
    
  5. 后端验证 bbb → 生成新 accessToken(+ 可选新 refreshToken)→ 返回

    { accessToken: "new-aaa", refreshToken: "new-bbb" }
    
  6. 客户端用新令牌重放刚才失败的请求,用户毫无感知

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 天?

  1. 泄露窗口 accessToken 随 每一次业务请求 都带在头里,被代理、日志、CDN 蹭到的概率远高于只偶尔出现的 refresh_token。 5 分钟泄露 → 最多被滥用 5 分钟;7 天泄露 → 用户要倒霉一周。
  2. 无法提前撤销 JWT 无状态,access_token 一旦签出去,在过期前任何拥有密钥的人都不敢说它是无效的(除非加黑名单,那就失去无状态优势)。 refresh_token 因为使用频率低,可以存数据库 / Redis,想踢就踢
  3. 刷新令牌可绑定设备、加指纹 后端保存 refresh_token 时可同时记录 UA、IP、设备 ID,发现异常直接吊销; access_token 要在高并发接口里反复验,做这些成本太高。
  4. 符合 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 应用时,它会依次通过你添加的所有中间件,形成一个“中间件栈”或“处理链”。整个流程可以分为两个明确的阶段:

  1. 请求阶段: 请求从客户端发出,依次通过各个中间件,最终到达你的路径操作函数。
  2. 响应阶段: 从路径操作函数返回的响应,会以相反的顺序再次通过中间件栈,最终返回给客户端。

下图展示了这一完整流程:

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,且接收 requestcall_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主机上

整个流程是这样的:

  1. 基于Dockerfile构建本地项目镜像
  2. 基于本地镜像使用docker save命令构建tar文件
  3. 使用scp将tar文件传到主机上
  4. 使用docker load命令将主机上的tar文件转成项目镜像并保存在本地镜像仓库
  5. 编写docker-compose.yml文件
  6. 编写.env文件
  7. 使用docker compose up -d命令运行docker-compose.yml文件完成容器的编排启动
  8. 进入项目容器使用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前端服务的配置

  1. ports: - "9980:80"
    • 端口映射:将主机的 9980 端口映射到容器的 80 端口(Nginx 默认 HTTP 端口)。
    • 效果:访问 http://localhost:9980 即可访问容器内的 Nginx 服务。
  2. 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 配置目录,替换默认配置,权限为只读
  3. depends_on: - backend
    • 依赖关系:确保 backend 服务先启动,再启动 frontend 服务。
  4. restart: unless-stopped
    • 重启策略:容器退出时自动重启,除非用户手动停止它。

b.为什么使用卷挂载

  1. 持久化数据 容器内的数据是临时的,容器删除后数据会丢失。卷挂载可将数据存储到主机文件系统,实现持久化。
  2. 配置管理 将配置文件(如 nginx.conf)挂载到容器,便于版本控制和灵活修改配置,无需进入容器内部。

c.卷挂载的本质是什么

卷挂载的本质是:将主机文件系统的一部分映射到容器内部的文件系统。

  • 它可以是目录文件
  • 挂载后,容器内的对应路径会直接读写主机上的文件
  • 类型包括:
    • 绑定挂载(Bind Mount):直接挂载主机上的特定路径(如示例中的 ./frontend)。
    • 命名卷(Named Volume):由 Docker 管理的存储卷,适用于数据库数据等。
  • 底层通过 Linux 的挂载命名空间(Mount Namespace) 实现,对容器透明。

d./usr/share/nginx/html 目录是什么

  1. 位置 这是 Nginx 容器内部的默认静态网站根目录
  2. 作用 Nginx 作为 Web 服务器时,会从此目录读取 HTML、CSS、JavaScript 等静态文件,并响应给客户端。
  3. 在配置中的意义
    • 将本地的 ./frontend 目录(包含前端构建产物)挂载到此路径,使 Nginx 直接使用主机上的前端文件。
    • 这样,前端代码更新时,只需更新主机上的 ./frontend 目录,无需重新构建或重启容器(Nginx 会自动提供最新文件)。

Q:/usr/share/nginx/html这个html是一个目录吗,我将frontend目录挂到这个上面,是不是等于html目录下的内容都是本机上frontend目录下的内容了

  1. /usr/share/nginx/html 确实是一个目录

    • 这是 Nginx 官方镜像中预定义的一个目录,专门用于存放静态网站文件。

    • 您可以通过进入容器来验证:

      docker exec -it <container_name> ls -la /usr/share/nginx/html
      

      会看到容器内默认可能有 index.html50x.html 等文件。

  2. 挂载效果:覆盖替换 当您执行 ./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.完整响应流程

    1. 用户发起请求:用户在浏览器输入:http://localhost:9980
    2. 请求到达主机:请求到达主机的9980端口,由于端口映射为9980:80,请求被转发到docker容器的80端口
    3. docker网络层: 主机网络层接收 → Docker 网络转发 → 容器网络接收,docker的docker-proxy进程处理端口转发,请求进入frontend容器的网络命名空间
    4. Nginx接收请求,Nginx 主进程(master process)接收连接,创建工作进程(worker process)处理该请求
    5. 读取文件系统,当 Nginx 需要提供 //index.html 时,Nginx 进程请求读取 /usr/share/nginx/html/index.html,由于卷挂载,该路径实际指向主机的 ./frontend 目录,Linux 内核通过挂载点将请求重定向到主机文件系统
    6. 文件系统访问路径:容器内路径访问 → 内核重定向 → 主机文件系统 → 返回文件内容
    7. Nginx构建响应:Nginx 获取文件内容后,先添加HTTP响应头,将文件内容作为响应体
    8. 响应返回路径:容器内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