基于OAuth2.0和JWT规范实现安全易用的用户认证

devtools/2025/1/8 6:06:42/

文章目录

    • 预备知识
      • OAuth2.0
      • JWT
    • 基本思路
    • 详细步骤
      • 1. 客户端提交认证请求
      • 2. 服务端验证用户登录
      • 3. 服务端颁发JWT Token
      • 4. 客户端管理 JWT token
      • 5. 客户端后续请求
      • 6. 服务端验证客户端的token
    • 总结
    • 查看完整代码


遵循OAuth2.0JWT规范实现用户认证,不但具有很好的实用性,还能提供很不错的安全保障。
本文结合实用的代码讲述了基于OAuth2.0JWT,在前后端分离的系统中,实现用户使用方便而又安全可靠的用户认证的基本思路。

预备知识

OAuth2.0

OAuth2.0 是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用。比如:微信登录、Facebook,Google,Twitter,GitHub等。
OAuth2.0规范要求使用密码流时,客户端或用户必须以表单数据形式发送 usernamepassword 字段。这两个字段必须命名为 usernamepassword ,不能使用 user-nameemail 等其它名称。
该规范要求必须以表单数据形式发送 username 和 password,因此,不能使用 JSON 对象。
当然,前端仍可以显示终端用户所需的名称,数据库模型也可以使用自己定义的名称。

关于OAuth的更多知识,您可以参考:理解OAuth2.0

JWT

JWT 即JSON 网络令牌(JSON Web Tokens),是目前最流行的跨域认证解决方案。
它在服务端将用户信息进行签名(如果有保密信息也可以加密后再签名),这样可以防止它被篡改。
每次客户端提交请求时,都附带 JWT 内容,这样服务端可以直接读取用户信息,而不用必须从服务器端的会话中获取。

关于JWT的更多知识,可以参考:JSON Web Token 入门教程

基本思路

以下是基本的实现思路:

  1. 客户端以表单方式,携带usernamepassword向后台接口发起登录认证请求
  2. 服务端接口验证账号和密码后,生成JWT格式的token,其中放置加密信息,签名后发放给客户端
  3. 客户端再访问服务端接口时,在HTTP Header中携带token即可

详细步骤

这些步骤中包含的代码片段的目的是能把过程讲得更清楚,如果想查看详细的代码并且自己动手实践,可参见本文最后的代码下载地址。

以下代码片段前端使用javascript和vue3,后端使用的是python

1. 客户端提交认证请求

前端javascript发送form请求:

import { ref } from "vue";
import axios from "axios";
import { jwtDecode } from "jwt-decode";import { setToken } from "@/assets/js/auth";const login_url = "http://127.0.0.1:8000/token";//表单数据
const form_data = ref({username: "",password: "",remember: false,
});const isLoading = ref(false);
const error = ref(false);
const error_msg = ref("");// 获取路由实例
import { useRoute ,useRouter } from "vue-router";
const route = useRoute();
const router = useRouter();const emits = defineEmits(["login"]);//提交
async function submit() {if (form_data.value.username === "" || form_data.value.password === "") {return;}isLoading.value = true;error.value = false;error_msg.value = "";const formData = new FormData();formData.append("username", form_data.value.username);formData.append("password", form_data.value.password);formData.append("remember", form_data.value.remember);try {const response = await axios.post(login_url, formData, {headers: {"Content-Type": "multipart/form-data", // 指定使用 form-data 格式},});const token = response.data.access_token;//console.log(token);// 解析 JWT Token 内容const decodedToken = jwtDecode(token);console.log("解析后的 Token 内容:", decodedToken);// 存储tokensetToken(token);let userid =  decodedToken["sub"];emits("login",userid);    if (route.query && route.query['redirect']) {router.push({ path: route.query['redirect']});} else {router.push({ path: "/" });}} catch (error) {if (error.code == "ERR_NETWORK") {error_msg.value = "网络错误,无法连接到服务器。";} else if (error.code == "ERR_BAD_REQUEST") {if (error.response.status == 401) {error_msg.value = "用户名或密码错误。";}} else {error_msg.value = error.message;}}isLoading.value = false;if (error_msg.value != "") {error.value = true;}
}

JWT字符串可以解密
后台颁发的JWT token是经过签名的,但是签名的目的是防篡改,客户端收到这个token后,是可以解析出签名前的JWT token原文的,但是由于不知道签名JWT使用的密钥,修改后无法再签名并提交给服务端。

2. 服务端验证用户登录

以下代码基于FastAPI

# 登录方法
@app.post("/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(),remember: bool|None=Body(None),log_details: None = Depends(log_request_details))-> Token:'''OAuth2PasswordRequestForm 是用以下几项内容声明表单请求体的类依赖项:usernamepasswordscope、grant_type、client_id等可选字段。'''user = authenticate_user(form_data.username, form_data.password)if not user:raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="用户名或者密码错误",headers={"WWW-Authenticate": "Bearer"},)m = 0if remember:m = ACCESS_TOKEN_EXPIRE_MINUTES# 在JWT 规范中,sub 键的值是令牌的主题。access_token = create_access_token(data={"sub": user.username},encrypted_text=user.userid, expire_minutes=m)# 响应返回的内容应该包含 token_type。本例中用的是BearerToken,因此, Token 类型应为bearer。return Token(access_token=access_token, token_type="bearer")

增加remember参数实现30天内免登录
上述代码中,除了接收客户端的usernamepassword以外,还额外接收一个remember请求,remember参数用于实现30天内免登录,如果客户端选择30天内免登录,服务端将会颁发有效期为30天的JWT token

3. 服务端颁发JWT Token

JWT Token 的主要结构是:

  • sub 即主题,包含 用户昵称
  • sign 一段加密的信息,里面包含 userId和exp
  • exp 时间戳,单位为:秒

一个颁发后的token长这样子:
在这里插入图片描述

将userId加密后放在token里面的好处
每次服务器收到客户端的请求时,可以解析出来userId,这样可以很方便的通过它查询用户相关的信息返回给客户端;由于userId具有唯一性,所以用它处理会话也更方便。

separator = "\u2016"# 用于JWT签名。
SECRET_KEY = config['secret']["jwt_key"]
'''
注意,不要使用本例所示的密钥,因为它不安全。
'''# 对JWT编码解码的算法。JWT不加密,任何人都能用它恢复原始信息。
ALGORITHM = "HS256"DEFAULT_TOKEN_EXPIRE_MINUTES = config['token']["default_expires_time"]   #默认token过期时间# 加密签名:userid + timestamp
def get_sign(encrypted_text,access_token_expires):t = str(math.floor(access_token_expires.timestamp()))#print(f"access_token_expires is {t}")src_text = encrypted_text + separator + treturn encrypt(src_text) from datetime import datetime, timedelta, timezone
import jwt
from jwt.exceptions import InvalidTokenError,ExpiredSignatureError# 生成JWT
def create_access_token(data: dict,encrypted_text: str=None, expire_minutes: int | None = None):to_encode = data.copy()if expire_minutes is not None and expire_minutes > 0:expires_delta = timedelta(minutes=expire_minutes) expire = datetime.now(timezone.utc) + expires_deltaelse:expire = datetime.now(timezone.utc) + timedelta(minutes=DEFAULT_TOKEN_EXPIRE_MINUTES)if encrypted_text is not None and encrypted_text != "": #加密签名sign = get_sign(encrypted_text,expire)to_encode.update({"sign": sign})to_encode.update({"exp": expire})encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)return encoded_jwt

4. 客户端管理 JWT token

客户端收到服务端颁发的 JWT token 后,可以存储在本地存储和会话中。
在本次会话,即未关闭浏览器之前,后续的请求都使用会话中存储的token。
以后再打开此系统时,可以从会话中取出token直接用。在这种情况下,客户端不必向服务端发送任何请求,也可以更新登录状态,显示 用户昵称。
当然,客户端在使用本地存储的token时,也应该校验一下是否过期。即便校验错误也没关系,因为服务端也会再次校验。

下面是客户端使用 JWT Token 的主要代码:

import { reactive } from 'vue';
import { jwtDecode } from "jwt-decode";const state = reactive({token: null,
});const token_name = 'token_liupras';const setToken = (token) => { state.token = token;localStorage.setItem(token_name, token);
};const getToken = () => {if (!state.token) {state.token = localStorage.getItem(token_name);}return state.token;
};const getUserId =() => {const token = getToken();if (token) {const decodedToken = jwtDecode(token);// 校验过期时间let exp_str = decodedToken['exp'];    let exp = parseInt(exp_str);   let now = parseInt(Date.now()/1000);    //转换为秒if (exp > now) {let diffDays = Math.floor((exp-now) / (60 * 60 * 24));console.log("token还有"+diffDays+"天过期。");return decodedToken.sub;}else{return null;}    }else{return null;}
};const logout = () => {state.token = null;localStorage.removeItem(token_name);
};const checkLoggedIn = () => {return !!getUserId();
};export {setToken, getToken, logout, checkLoggedIn,getUserId};

上述代码实现了客户端会话的管理功能

5. 客户端后续请求

在拥有了JWT Token 以后,客户端在发请求时,将它放在请求的Header中就可以了,我们可以在通用的请求方法中自动把它加上。

import axios from 'axios';
import {getToken,logout} from '@/assets/js/auth';const instance = axios.create({baseURL: 'http://localhost:8000',timeout: 90000,
});// 请求拦截器:添加 Authorization 头
instance.interceptors.request.use((config) => {const token = getToken();if (token) {config.headers.Authorization = `Bearer ${token}`;}return config;},(error) => Promise.reject(error)
);// 响应拦截器:处理 401 错误
instance.interceptors.response.use((response) => response,(error) => {if (error.response && error.response.status === 401) {logout();}return Promise.reject(error);}
);export default instance;

6. 服务端验证客户端的token

使用 FastAPI 处理客户端请求时,可以使用依赖注入自动进行token验证。

# 根据token获取当前登录的用户信息
# 该函数接收 str 类型的令牌,并返回 Pydantic 的 User 模型
async def get_current_user(token: str = Depends(oauth2_scheme)):'''安全和依赖注入的代码只需要写一次。各个端点可以使用同一个安全系统。'''credentials_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,detail="Could not validate credentials",headers={"WWW-Authenticate": "Bearer"},)try:userid = decode_access_token(token)if userid is None or userid == "":raise credentials_exceptionuser = get_user(username=userid)if user is None:raise credentials_exceptionreturn userexcept Exception:raise credentials_exception# 获取当前登录用户信息,并检查是否禁用
async def get_current_active_user(current_user: User = Depends(get_current_user)):'''在端点中,只有当用户存在、通过身份验证、且状态为激活时,才能获得该用户信息。'''if current_user.disabled:raise HTTPException(status_code=400, detail="Inactive user")return current_user# 获取用户信息
@app.get("/users/me")
async def read_users_me(current_user: Annotated[User, Depends(get_current_active_user)]):'''Depends 在依赖注入系统中处理安全机制。此处把 current_user 的类型声明为 Pydantic 的 User 模型,这有助于在函数内部使用代码补全和类型检查。get_current_user 依赖项从子依赖项 oauth2_scheme 中接收 str 类型的 toke。FastAPI 校验请求中的 Authorization 请求头,核对请求头的值是不是由 Bearer + 令牌组成, 并返回令牌字符串;如果没有找到 Authorization 请求头,或请求头的值不是 Bearer + 令牌。FastAPI 直接返回 401 错误状态码(UNAUTHORIZED)。'''return current_user

总结

以上的文字和代码阐述了如何基于OAuth2.0JWT实现安全完善的用户认证,欢迎指正。

查看完整代码

  • gitee
  • github
  • gitcode

🪐祝好运🪐


http://www.ppmy.cn/devtools/148386.html

相关文章

渗透测试实战-DC-1

firewall-cmd –reload DC-1 靶机实战 打开测试靶机DC-1 查看网络配置,及网卡 靶机使用NAT 模式,得到其MAC地址 使用nmap 工具扫描内网网段 nmap -sP 192.168.1.144/24 -oN nmap.Sp MAC 对照得到其IP地址 对其详细进行扫描 nmap -A 192.168.1.158 -p …

利用 NineData 实现 PostgreSQL 到 Kafka 的高效数据同步

记录一次 PostgreSQL 到 Kafka 的数据迁移实践。前段时间,NineData 的某个客户在一个项目中需要将 PostgreSQL 的数据实时同步到 Kafka。需求明确且普遍: PostgreSQL 中的交易数据,需要实时推送到 Kafka,供下游多个系统消费&#…

应用程序越权漏洞安全测试总结体会

应用程序越权漏洞安全测试总结体会 一、 越权漏洞简介 越权漏洞顾名思议超越了自身的权限去访问一些资源,在OWASP TOP10 2021中归类为A01:Broken Access Control,其本质原因为对访问用户的权限未进行校验或者校验不严谨。在一个特定的系统或…

django vue3实现大文件分段续传(断点续传)

前端环境准备及目录结构: npm create vue 并取名为big-file-upload-fontend 通过 npm i 安装以下内容"dependencies": {"axios": "^1.7.9","element-plus": "^2.9.1","js-sha256": "^0.11.0&quo…

解决 :VS code右键没有go to definition选项(转到定义选项)

问题背景: VScode 右键没有“go to definition”选项了,情况如图所示: 问题解决办法: 第一步:先检查没有先安装C/C插件,没有安装就先安装下。 第二步: 打开VS CODE设置界面:文件->…

【网络】HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3比对

HTTP/3是HTTP协议的最新版本,它基于QUIC协议,而QUIC最初由Google开发,后来被IETF标准化。以下是包含HTTP/1.0、HTTP/1.1、HTTP/2和HTTP/3(基于QUIC)的对比表格: 特性HTTP/1.0HTTP/1.1HTTP/2HTTP/3 (基于QU…

【HarmonyOS】:DevEco Studio安装与应用工程创建指南

前言 本文旨在为初涉 HarmonyOS 开发的开发者提供一份详尽的入门指南,涵盖从安装最新版 DevEco Studio 到使用该 IDE 创建首个应用工程的具体步骤。通过遵循本指南,您将能够顺利搭建起自己的开发环境,并迈出构建HarmonyOS应用的第一步。 一、…

Visual Studio 中增加的AI功能

前言: 人工智能的发展,在现在,编程技术的IDE里面也融合了AI的基本操做。本例,以微软的Visual Studio中的人工智能的功能介绍例子。 本例的环境: Visual Studio 17.12 1 AI 智能变量检测: 上图展示了一…