Python登录接口实战:从密码加密到JWT鉴权全解析

📅 发布时间:2026/10/4 5:06:56
Python登录接口实战:从密码加密到JWT鉴权全解析
做这个登录接口项目其实是源于我在学 Python 过程中的一个念头平时打开各种网站输入用户名密码就进去了但背后到底发生了什么尤其是看到携程登陆这种带验证码、带风控的真实登录场景总觉得离自己很远。后来想明白了与其去看别人怎么逆向别人的登录不如先老老实实自己动手写一个完整的用户登录接口。只有自己把注册、密码加密、Token 签发、鉴权这一整套流程走通了回头再去看任何网站的登录机制心里才算真正有底。这篇博文就围绕用 Python 实现用户登录接口来展开带你把整个接口从零到一写出来。文章会覆盖技术选型、数据库设计、密码加密、JWT 认证、接口测试和常见坑位适合刚学完 Python 基础语法、想找一个完整练手项目的读者。如果你正准备找 Python 相关的工作或者想系统理解 Web 后端里登录这件事这篇应该能帮上忙。我会尽可能讲清楚每个设计决策背后的原因不是那种复制粘贴就能跑、但跑完啥也没学会的代码。1. 先搞清楚登录接口到底在做什么1.1 一个再常见不过的场景背后是什么先别急着写代码我们把登录这个动作拆开看。一个用户打开你的网站输入用户名和密码点击登录然后系统返回一个凭证之后用户再访问其他页面这个凭证会自动带在请求里服务器一看凭证有效就知道哦这个人是刚才那个登录过的用户于是放行。这个过程拆成技术语言其实就是两个接口的事注册接口接收用户名和密码把密码安全地存进数据库。登录接口接收用户名和密码验证通过后返回一个 Token令牌。再加上一个受保护接口比如获取用户资料用来验证 Token 是否有效。就这么几件事兜住了几乎所有 Web 应用的登录骨架。像我们平时看到的携程登陆这类场景本质上也是同一套逻辑无非是外面多套了验证码、短信校验、滑块拖动、设备指纹这些防护壳。壳子再花哨核心认证部分依然是拿凭证、验凭证、发令牌三步。提到这个是想说练习项目最好挑能触类旁通的。你花半天时间把用户登录接口写扎实了后面理解 OAuth、单点登录、扫码登录都会顺很多因为它们解决的都是同一个问题——如何确认你是你。1.2 技术选型为什么用 Flask SQLite JWT选型这件事练习项目和商业项目要分开看。如果是线上项目你多半要上 Spring Boot、Go、或者 Django数据库也得是 MySQL/PostgreSQL 这类还得配 Redis 做会话缓存。但这是学习练习目标只有一个用最少的成本把核心逻辑跑通同时不留下坏习惯。我选的是 Flask SQLite JWT 这套组合。Flask 是 Python 里最轻量的 Web 框架之一。它不像 Django 那样自带全套脚手架你需要什么自己装什么这对理解 HTTP 请求处理、路由、装饰器这些概念特别有帮助。用一个 Flask 接口你基本就理解了 Python Web 后端的主干逻辑后面上 FastAPI 或 Django 都是顺水推舟的事。SQLite 是 Python 内置的轻量数据库一个文件就是一个库不需要单独安装数据库服务。在这个项目里我们只需要存用户表用它完全够还能让你专注在接口逻辑上。等你以后需要上 MySQL把 SQL 语句换一换、连接方式改一改就基本能迁移。JWTJSON Web Token是目前前后端分离场景下最主流的认证方案。登录成功后服务端签发一个经过签名的 Token 给前端前端每次请求带上它服务端验证签名即可。它最大的好处是无状态——服务器不需要存 Session天然适合分布式部署和移动端接入。提示如果你是零基础建议先装好 Python 环境再装 Flask 和 PyJWT 这两个库就行。命令随手给一下pip install flask PyJWT。装完用python -c import flask验证一下不报错就是装好了。2. 动手前必须想清楚的三个关键设计2.1 密码绝不能明文存——哈希加密为什么是底线很多刚入门的朋友会在这一步犯迷糊不就是把密码存进数据库吗一个字段的事为什么要搞什么加密。我先给结论密码明文存储跟裸奔没有区别一旦数据库泄露所有用户的账号就全部暴露了这是最严重的安全事故没有之一。正确的做法是哈希加密。哈希Hash是一个单向过程把任意长度的密码变成一串固定长度的字符串而且这个过程不可逆。你只能拿原密码去算哈希值然后跟数据库里存的哈希值比对比对成功就说明密码正确但你永远没法从数据库里反推出原密码。这里要特别提醒一下直接哈希还是不够安全的。因为很多人密码简单黑客可以用彩虹表预计算好的哈希对照表暴力匹配。所以业界通用做法是加盐Salt就是在密码后拼接一段随机字符串再一起做哈希。这样即使两个用户的密码一样因为盐不同哈希结果也不同。好在 Flask 的werkzeug.security模块已经把这些细节封装好了直接用generate_password_hash和check_password_hash两个函数就行底层用的是安全哈希算法还自动帮你处理了盐。from werkzeug.security import generate_password_hash, check_password_hash # 注册时 password_hash generate_password_hash(123456) # 得到的值是类似 pbkdf2:sha256:260000$xxxx$xxxx 的字符串 # 登录校验时 check_password_hash(password_hash, 123456) # 返回 True注意到没有存储字段叫password_hash不是password。这个命名习惯是在提醒自己数据库里根本没有密码只有密码的哈希值。代码里也禁止出现把原密码塞进 SQL 的操作。2.2 登录后怎么记得你——Token 认证机制密码验证通过之后登录这一步其实还没完。你还需要决定用户登录成功后后续请求怎么证明自己的身份传统的方案是 Session 记录服务器存一份会话 ID发给客户端存 Cookie每次请求时带上。缺点是服务器要维护会话状态分布式部署还要专门做 Session 共享。现在前后端分离和移动端盛行JWT 成了更主流的方案。JWT 的模型可以理解成一张签名盖章的通行证。用户登录成功后服务器生成一个 Token里面包含了用户 ID、用户名、过期时间这些基本信息再用服务器私钥签名。之后用户请求带着这个 Token服务器验签通过就信任里面的信息绝不查数据库。一个 JWT 由三部分组成用点号分隔Header声明加密算法和令牌类型。Payload放用户 ID、过期时间等业务信息。Signature用服务器密钥对前两部分签名。三部分各自做 Base64 编码后拼在一起就得到了形如eyJhbGciOiJIUzI1NiIs...的一长串字符串也就是我们接口里返回给前端的 Token。用PyJWT库实现非常简单import jwt import datetime SECRET_KEY your-secret-key # 签发 Token设置24小时过期 token jwt.encode( { user_id: 1, username: testuser, exp: datetime.datetime.utcnow() datetime.timedelta(hours24) }, SECRET_KEY, algorithmHS256 ) # 验证 Token payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) print(payload[username]) # 输出 testuser有几个设计要点值得记一下。第一Token 里的信息只有服务器才能伪造因为签名用的密钥在服务器手里。第二Token 一旦签发在过期之前是没法中途作废的所以过期时间要设得合理一般业务场景 2 到 24 小时比较常见。第三SECRET_KEY 要足够随机且绝不能写死在代码里提交到 Git 仓库练习项目可以用环境变量管理养成好习惯。2.3 用户表怎么建——字段设计和数据库初始化登录系统核心就一张用户表但字段设计上要想清楚。最基础的几个字段id主键自增整数业务上用来标识用户。username用户名必须加唯一约束防止重复注册。password_hash密码哈希值注意是哈希不是明文。created_at注册时间排查问题时会用到。SQLite 建表的 SQL 语句如下CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL );UNIQUE约束保证了用户名唯一注册时如果插入重复的用户名SQLite 会抛出IntegrityError异常我们在代码里捕获这个异常就能实现用户名已存在的提示。这个设计比先查一遍再插入更安全因为查和插之间存在时间窗口并发情况下可能重复插入。为了方便调用我在项目里单独写了一个数据库初始化函数import sqlite3 DB_PATH users.db def init_db(): conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, created_at TEXT NOT NULL ) ) conn.commit() conn.close()这段代码放在程序启动时执行一次即可CREATE TABLE IF NOT EXISTS意味着重复执行也不会有副作用。数据库文件users.db会自动生成在当前目录整体项目非常轻量。3. 完整实现注册、登录、Token 校验3.1 搭建环境和项目结构先说项目结构。虽然是练习项目目录结构最好也稍微规范一点不要所有代码堆一个文件里。我的建议是最小化分两层login_demo/ ├── app.py # Flask 主程序所有接口 ├── test_client.py # 接口测试脚本 └── users.db # 数据库文件运行 init_db 后自动生成其实接口和测试脚本分开放就够了app.py 里面按逻辑顺序组织函数工具函数、数据库初始化、业务接口、主入口。等以后项目规模大了再拆成包练习阶段这个粒度最舒服。接下来是安装依赖。需要的东西很少pip install flask PyJWT requestsrequests是后面测试阶段用的如果你顺手的话也可以全程用浏览器插件比如 Postman、Apifox来测但脚本能帮你自动化验证还能反复跑我觉得更实在。3.2 注册接口校验参数 密码哈希入库注册接口的逻辑是按顺序做的解析 JSON 请求体、校验参数是否齐全、哈希密码、插入数据库、捕获唯一约束异常、返回结果。from flask import Flask, request, jsonify from werkzeug.security import generate_password_hash, check_password_hash import sqlite3 import jwt import datetime from functools import wraps app Flask(__name__) app.config[SECRET_KEY] please-change-me-to-a-random-secret DB_PATH users.db def get_db(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row return conn app.route(/register, methods[POST]) def register(): data request.get_json(silentTrue) or {} username (data.get(username) or ).strip() password data.get(password) or if not username or not password: return jsonify({code: 400, msg: 用户名和密码不能为空}), 400 if len(password) 6: return jsonify({code: 400, msg: 密码长度不能少于6位}), 400 password_hash generate_password_hash(password) conn get_db() try: conn.execute( INSERT INTO users (username, password_hash, created_at) VALUES (?, ?, ?), (username, password_hash, datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S)) ) conn.commit() return jsonify({code: 200, msg: 注册成功}) except sqlite3.IntegrityError: return jsonify({code: 400, msg: 用户名已存在}), 400 finally: conn.close()几个容易忽略的细节说一下。request.get_json(silentTrue) or {}如果请求头没带Content-Type: application/json或者请求体解析失败get_json()会返回None加or {}能避免后面的.get抛异常。username (data.get(username) or ).strip()先处理None再去除首尾空格防止用户填了一堆空格当用户名存进数据库才发现是个空串。密码长度校验只是最基础的防线真正的安全性由哈希保证。只要密码不短于 6 位哈希后存储是安全的业务上再复杂的策略大小写混合、特殊字符后面可以自己加。插入数据库时用一个 try 包住IntegrityError这是处理用户重名的标准姿势。前提是你给username字段建了唯一索引或UNIQUE约束否则这里永远捕获不到异常就会出现两个同名用户后续登录逻辑就乱了。3.3 登录接口验证密码 签发 Token登录接口和注册接口最大的区别在于它要按用户名查出用户记录再校验密码哈希校验通过后签发 Token。app.route(/login, methods[POST]) def login(): data request.get_json(silentTrue) or {} username (data.get(username) or ).strip() password data.get(password) or if not username or not password: return jsonify({code: 400, msg: 用户名和密码不能为空}), 400 conn get_db() user conn.execute( SELECT * FROM users WHERE username ?, (username,) ).fetchone() conn.close() if not user or not check_password_hash(user[password_hash], password): return jsonify({code: 401, msg: 用户名或密码错误}), 401 token jwt.encode( { user_id: user[id], username: user[username], exp: datetime.datetime.utcnow() datetime.timedelta(hours24) }, app.config[SECRET_KEY], algorithmHS256 ) return jsonify({code: 200, msg: 登录成功, token: token, username: user[username]})校验密码时用了werkzeug.security的check_password_hash它内部会从存储的哈希值里提取盐和算法参数然后用相同参数重新计算传入的密码哈希再比对结果。所以即使以后换了哈希算法它能读旧数据非常实用。登录失败时统一返回用户名或密码错误没有区分用户名不存在和密码错误。这是有讲究的——如果接口能区分就相当于给攻击者提供了用户名是否已注册的探测通道方便他们做定向撞库。统一返回降低信息泄露风险是接口设计的常规做法。Session 不太懂的朋友可能想问为啥不直接返回一个 user_id 就行因为 user_id 是整数任何人拿到都能伪造别人的身份等于没登录系统。Token 的价值在于签名——前端无法修改 Token 里的任何内容改一个字符验签就会失败。这就是签名的意义所在。3.4 受保护接口Token 校验装饰器有了 Token用户的每个后续请求都要带上。受保护接口在执行业务逻辑前先校验请求头里有没有合法的 Token没通过就直接拒绝。def token_required(f): wraps(f) def decorated(*args, **kwargs): token request.headers.get(Authorization, ) # 支持 Bearer token 和裸 token 两种格式 if token.startswith(Bearer ): token token.split( )[1] if not token: return jsonify({code: 401, msg: 缺少Token}), 401 try: payload jwt.decode(token, app.config[SECRET_KEY], algorithms[HS256]) request.user payload except jwt.ExpiredSignatureError: return jsonify({code: 401, msg: Token已过期}), 401 except jwt.InvalidTokenError: return jsonify({code: 401, msg: Token无效}), 401 return f(*args, **kwargs) return decorated app.route(/profile, methods[GET]) token_required def profile(): return jsonify({code: 200, data: {user_id: request.user[user_id], username: request.user[username]}})这段代码用装饰器实现鉴权是 Python 里非常经典的一个用法。token_required套在任意接口函数上面接口内部只需要通过request.user获取当前登录用户信息完全不用关心鉴权逻辑后续做新接口只要加一行装饰器就完成了。Token 的常见携带方式是放在 HTTP 请求头Authorization字段里有两种风格裸 TokenAuthorization: eyJhbGciOiJIUzI1NiIs...Bearer 格式Authorization: Bearer eyJhbGciOiJIUzI1NiIs...代码里两种都兼容了这算个小细节因为不少前端框架或 API 文档默认用 Bearer 格式兼容一下能少踩很多坑。到这里一个最小可用的登录接口系统就齐了注册存哈希、登录发 Token、受保护接口验 Token。把 app.py 跑起来用测试脚本调一遍整个链路就通了。4. 接口写完了怎么测、怎么调4.1 用 requests 写一个最简单的测试脚本接口是给别人调用的没测过就等于没写完。我习惯写一个独立的测试脚本把正常流程和几个典型异常流程都跑一遍。import requests BASE_URL http://127.0.0.1:5000 def test_register(): # 正常注册 resp requests.post(f{BASE_URL}/register, json{username: alice, password: 123456}) print(正常注册:, resp.json()) # 重复注册 resp requests.post(f{BASE_URL}/register, json{username: alice, password: 654321}) print(重复注册:, resp.json()) # 密码太短 resp requests.post(f{BASE_URL}/register, json{username: bob, password: 123}) print(密码太短:, resp.json()) def test_login(): # 正常登录 resp requests.post(f{BASE_URL}/login, json{username: alice, password: 123456}) print(正常登录:, resp.json()) token resp.json().get(token) # 错误密码 resp requests.post(f{BASE_URL}/login, json{username: alice, password: wrong}) print(错误密码:, resp.json()) # 访问受保护接口 resp requests.get(f{BASE_URL}/profile, headers{Authorization: fBearer {token}}) print(带Token访问:, resp.json()) # 不带Token访问 resp requests.get(f{BASE_URL}/profile) print(无Token访问:, resp.json()) if __name__ __main__: test_register() test_login()先运行python app.py启动服务再开一个终端窗口运行python test_client.py看到类似这样的日志就说明全流程通了正常注册: {code: 200, msg: 注册成功} 重复注册: {code: 400, msg: 用户名已存在} 密码太短: {code: 400, msg: 密码长度不能少于6位} 正常登录: {code: 200, msg: 登录成功, token: eyJ...} 错误密码: {code: 401, msg: 用户名或密码错误} 带Token访问: {code: 200, data: {user_id: 1, username: alice}} 无Token访问: {code: 401, msg: 缺少Token}这组测试还顺手验证了一个隐藏逻辑——注册的密码是123456登录也传123456但接口内部比对的是哈希值不是明文。你存进数据库里看到的那串pbkdf2:sha256:260000$...字符串永远不可能反推出123456来。把这句话记住你在任何场合说到密码存储都是内行水准。4.2 模拟登录场景的延伸为什么懂了原理看第三方登录就不怵了写到这里有朋友可能会问标题里不是有携程登陆吗你写的这个和携程登录有什么关系这是个好问题。我说说我的理解。携程这种大型网站的登录从用户视角来看是输入手机号、收验证码、点滑块、人脸识别复杂得很但落到后端最后一步一定还是服务器确认请求者身份合法然后签发一个会话凭证跟咱们实现的login接口是同一件事。不同的是它前面多了几层风控验证码防机器批量注册和撞库。滑块判断是真人还是脚本。设备指纹识别常用设备异常登录时触发二次验证。风控策略结合 IP、行为轨迹决定要不要加强校验。咱们练习项目不需要搞这些但一旦你理解了登录的本质再看这些防护手段就不会觉得神秘了。它们都是在确认是你这条主线上加的各种检查关卡围绕的始终是身份认证和凭证管理。以后再遇到用 Python 模拟第三方登录的需求你至少知道突破口在哪也明白哪些能做、哪些不该做。从学习路径的角度说我强烈建议先把自建用户登录接口写到熟练再去碰第三方网站的各种登录。直接上手模拟别人的登录会让你一头扎进加密算法和风控对抗的泥潭完全没有建立起主干认知容易劝退。4.3 接口调试的三个高频坑测试脚本跑一遍容易但真到了自己动手写、动手调的时候有几个坑几乎是每个人都会踩的。第一个坑是请求体格式对不上。前后端用application/x-www-form-urlencoded提交表单和用application/json提交 JSONFlask 里接收方式完全不同。我这个项目统一用 JSON前端必须显式设置Content-Type: application/json并传合法的 JSON 字符串。如果你在调试时发现data一直是None十有八九是这里出了问题。用requests时直接写json{...}会自动设置好请求头手动构造请求时要留意。第二个坑是 Token 过期时间设置。代码里用的是datetime.datetime.utcnow()注意不能用datetime.datetime.now()不然服务器时区不在 UTC 时签发时间和过期时间的计算会乱掉。验签时如果偶尔报Signature has expired先检查一下是不是这个原因。第三个坑是跨域问题。如果你的前端页面跑在 5500 端口比如用 VSCode 的 Live Server后端接口在 5000 端口浏览器出于同源策略会拦截跨域请求。这不是后端逻辑错而是没配 CORS跨域资源共享响应头。练习阶段最简单的办法是用 requests 脚本测或者装一个允许跨域的浏览器插件要正式处理给 Flask 加上flask-cors库再配置允许的域名即可。5. 常见问题与排查技巧实录5.1 常见问题速查表一个项目写下来我顺手记录了几个典型的报错和排查思路。列成表格方便大家直接对照。现象可能原因排查与解决注册时报OperationalError: no such table: users忘记调用init_db()启动 app.py 前先执行初始化函数或者在if __name__ __main__里调用重复用户名注册不报错建表时没给 username 加UNIQUE约束删掉 users.db 重建表或在 SQL 的 username 字段加上UNIQUE登录接口报 500 错误请求体不是合法 JSON打印request.get_json()的结果确认为 None 时换个方式提交JWT 解码报Signature has expiredToken 过期时间设太短或服务器时钟不准检查签发代码里的exp确认用的是utcnow而非now浏览器里请求接口报 CORS 错误没有配置跨域响应头开发环境用 requests 测正式处理用 flask-cors数据库文件被占用Windows 下无法删除有进程还持着连接确保所有conn.close()都执行了必要时重启 Python 进程部署到服务器后访问 5000 端口失败防火墙或监听地址绑定了 127.0.0.1练习项目本机跑没问题公网部署需改app.run(host0.0.0.0)并配合 Nginx这个表格里的问题我基本都亲手踩过尤其是UNIQUE约束那条最开始我建表时没注意导致重复注册时数据库插入成功两个同名的账户把登录逻辑搞得乌烟瘴气。查了半小时才发现是表结构少了个约束。5.2 我的几个实操心得第一个心得跟数据库连接有关。Python 的sqlite3模块每次连接都是独立的用完了必须close()。我在写注册接口时用try/finally保证无论成功还是异常连接都会关闭。别小看这个细节开发时连开几个连接不关Windows 下数据库文件会一直被占用你想删都删不掉只能强行结束 Python 进程。想要更省心可以学习用with语句管理连接但try/finally是最直观的写法。第二个心得是日志要打得比想象中多。接口联调时最烦的就是前端说报错了你问他报什么错他说就是报错了。所以我在每个接口的入口处都打印请求参数密码除外关键分支也打印走向。密码不能打印这是原则问题也是自由裁量的事——为了排查问题可以把是否打印密码做成环境变量控制但默认永远是关的。第三个心得是版本管理要趁早。哪怕是自己一个人练手也建议从第一天就git init每个阶段改完代码提交一次。不要觉得我还没写好呢等写完再提交。等你真的改出了 bug没有版本管理就只能靠肉眼比对代码极其痛苦。我经历过一次把能跑的版本覆盖掉、找不回来的惨案从那以后就再也不偷懒了。第四个心得也可能是最有价值的一个这个练习项目做完后你可以试着给自己加需求。比如支持手机号登录、支持刷新 Token、支持修改密码、支持注销登录后端把 Token 加入黑名单每加一个需求你对登录体系的理解就会深一层。我后来写过一个带验证码的登录接口就是在这个项目基础上改的前前后后只花了一天半很多概念因为基础已经打牢上手特别顺。跟着这篇文章把代码敲一遍你大概需要三十分钟到一小时。如果中间有报错别急着复制答案先自己看报错信息把问题拆开查能查明白的感觉特别有成就感。另外做完之后你自己试试改一个需求比如把密码最短长度从 6 位改成 8 位或者让 Token 的有效期变成 7 天看看代码要动几处。这种小改动会帮你把所有逻辑串联得更牢靠学到的就不仅仅是一个接口了。