Pārlūkot izejas kodu

登录30天修复

guarantee-lsq 3 dienas atpakaļ
vecāks
revīzija
17af1548fd
3 mainītis faili ar 271 papildinājumiem un 229 dzēšanām
  1. 216 196
      .cool/service/index.ts
  2. 48 33
      .cool/store/user.ts
  3. 7 0
      services/user.ts

+ 216 - 196
.cool/service/index.ts

@@ -1,197 +1,217 @@
-import { isDev, ignoreTokens, config } from "@/config";
-import { locale, t } from "../locale";
-import { isNull, isObject, parse, storage } from "../utils";
-import { useStore } from "../store";
-import { useUi } from "@/uni_modules/cool-ui";
-// 请求参数类型定义
-export type RequestOptions = {
-	url: string; // 请求地址
-	method?: RequestMethod; // 请求方法
-	data?: any; // 请求体数据
-	params?: any; // URL参数
-	header?: any; // 请求头
-	timeout?: number; // 超时时间
-	withCredentials?: boolean; // 是否携带凭证
-	firstIpv4?: boolean; // 是否优先使用IPv4
-	enableChunked?: boolean; // 是否启用分块传输
-};
-
-// 响应数据类型定义
-export type Response = {
-	code: number;
-	message: string;
-	data: any;
-};
-const ui = useUi();
-// 请求队列(用于等待token刷新后继续请求)
-let requests: ((token: string) => void)[] = [];
-
-// 标记token是否正在刷新
-let isRefreshing = false;
-
-// 判断当前url是否忽略token校验
-const isIgnoreToken = (url: string) => {
-	return ignoreTokens.some((e) => {
-		const pattern = e.replace(/\*/g, ".*");
-		return new RegExp(pattern).test(url);
-	});
-};
-
-/**
- * 通用请求方法
- * @param options 请求参数
- * @returns Promise<T>
- */
-export function request(options: RequestOptions): Promise<any | null> {
-	let { url, method = "GET", data = {}, header = {}, timeout = 60000 } = options;
-
-	const { user } = useStore();
-
-
-
-	// 拼接基础url
-	if (!url.startsWith("http")) {
-		url = config.baseUrl + url;
-	}
-	// 开发环境下打印请求信息
-	if (isDev) {
-		console.log(`[${method}] ${url}`);
-	}
-	// 获取当前token
-	let Authorization: string | null = user.token;
-
-	// 如果是忽略token的接口,则不携带token
-	if (isIgnoreToken(url)) {
-		Authorization = null;
-	}
-
-	return new Promise((resolve, reject) => {
-		// 发起请求的实际函数
-		const next = () => {
-			uni.request({
-				url,
-				method,
-				data,
-				header: {
-					Authorization,
-					language: locale.value,
-					...(header as UTSJSONObject)
-				},
-				timeout,
-
-				success(res) {
-					if (isNull(res.data)) {
-						res.data = {};
-					}
-					const { code, message, data } = parse<Response>(
-						(res.data as Response)
-					)!
-
-					// 401 无权限
-					if (res.statusCode == 401) {
-						user.logout();
-						ui.showToast({
-							message: "无权限",
-							type: "warn",
-						})
-						reject({ message: t("无权限") } as Response);
-					}
-
-					// 502 服务异常
-					else if (res.statusCode == 502 || res.statusCode == 405) {
-						ui.showToast({
-							message: message || "服务异常",
-							type: "warn",
-						})
-						reject({
-							message: message || t("服务异常")
-						} as Response);
-					}
-
-					// 404 未找到
-					else if (res.statusCode == 404) {
-						ui.showToast({
-							message: `[404] ${url}`,
-							type: "warn",
-						})
-						return reject({
-							message: `[404] ${url}`
-						} as Response);
-					}
-
-					// 200 正常响应
-					else if (res.statusCode == 200) {
-						if (res.data == null) {
-							resolve(null);
-						} else if (!isObject(res.data as any)) {
-							resolve(res.data);
-						} else {
-							// 解析响应数据
-							if (code == 200 || code == 0) {
-								resolve(data);
-							} else if (code == 1) {
-								reject(res.data);
-							}
-							else {
-								ui.showToast({
-									message: message ?? t("服务异常"),
-									type: "warn",
-								})
-								reject({ message: message } as Response);
-							}
-						}
-					} else {
-						ui.showToast({
-							message: message || "服务异常",
-							type: "warn",
-						})
-						reject({ message: message || "服务异常" } as Response);
-					}
-				},
-
-				// 网络请求失败
-				fail(err) {
-					reject({ message: err.errMsg } as Response);
-				}
-			});
-		};
-
-		next();
-	});
-}
-export function useGet(url: string, data?: any, config?: RequestOptions): Promise<any> {
-	const options: RequestOptions = {
-		url,
-		data,
-		method: 'GET',
-		...config,
-	}
-	return request(options)
-}
-export function usePost(url: string, data?: any, config?: RequestOptions): Promise<any> {
-	const options: RequestOptions = {
-		url,
-		data,
-		method: 'POST',
-		...config,
-	}
-	return request(options)
-}
-export function usePut(url: string, data?: any, config?: RequestOptions): Promise<any> {
-	const options: RequestOptions = {
-		url,
-		data,
-		method: 'PUT',
-		...config,
-	}
-	return request(options)
-}
-export function useDelete(url: string, data?: any, config?: RequestOptions): Promise<any> {
-	const options: RequestOptions = {
-		url,
-		data,
-		method: 'DELETE',
-		...config,
-	}
-	return request(options)
+import { isDev, ignoreTokens, config } from "@/config";
+import { locale, t } from "../locale";
+import { isNull, isObject, parse, storage } from "../utils";
+import { useStore } from "../store";
+import { useUi } from "@/uni_modules/cool-ui";
+// 请求参数类型定义
+export type RequestOptions = {
+	url: string; // 请求地址
+	method?: RequestMethod; // 请求方法
+	data?: any; // 请求体数据
+	params?: any; // URL参数
+	header?: any; // 请求头
+	timeout?: number; // 超时时间
+	withCredentials?: boolean; // 是否携带凭证
+	firstIpv4?: boolean; // 是否优先使用IPv4
+	enableChunked?: boolean; // 是否启用分块传输
+};
+
+// 响应数据类型定义
+export type Response = {
+	code: number;
+	message: string;
+	data: any;
+};
+const ui = useUi();
+// 请求队列(用于等待token刷新后继续请求)
+let requests: ((token: string) => void)[] = [];
+
+// 标记token是否正在刷新
+let isRefreshing = false;
+
+// 判断当前url是否忽略token校验
+const isIgnoreToken = (url: string) => {
+	return ignoreTokens.some((e) => {
+		const pattern = e.replace(/\*/g, ".*");
+		return new RegExp(pattern).test(url);
+	});
+};
+
+/**
+ * 通用请求方法
+ * @param options 请求参数
+ * @returns Promise<T>
+ */
+export function request(options: RequestOptions): Promise<any | null> {
+	let { url, method = "GET", data = {}, header = {}, timeout = 60000 } = options;
+
+	const { user } = useStore();
+
+
+
+	// 拼接基础url
+	if (!url.startsWith("http")) {
+		url = config.baseUrl + url;
+	}
+	// 开发环境下打印请求信息
+	if (isDev) {
+		console.log(`[${method}] ${url}`);
+	}
+	// 获取当前token
+	let Authorization: string | null = user.token;
+
+	// 如果是忽略token的接口,则不携带token
+	if (isIgnoreToken(url)) {
+		Authorization = null;
+	}
+
+	return new Promise((resolve, reject) => {
+			let isRetry = false;
+		// 发起请求的实际函数
+		function next() {
+			uni.request({
+				url,
+				method,
+				data,
+				header: {
+					Authorization,
+					language: locale.value,
+					...(header as UTSJSONObject)
+				},
+				timeout,
+
+				success(res) {
+					if (isNull(res.data)) {
+						res.data = {};
+					}
+					const { code, message, data } = parse<Response>(
+						(res.data as Response)
+					)!
+
+					// 401 无权限
+					if (res.statusCode == 401) {
+						// 不对token接口本身做刷新,防止死循环
+						if (!isRetry && url.indexOf('/oauth/token') === -1) {
+							isRetry = true;
+							user.refreshToken()
+								.then(() => {
+									Authorization = user.token;
+									next();
+								})
+								.catch(() => {
+									user.logout();
+									ui.showToast({
+										message: "无权限",
+										type: "warn",
+									})
+									reject({ message: t("无权限") } as Response);
+								});
+							return;
+						}
+						// 刷新后仍然401,强制登出
+						user.logout();
+						ui.showToast({
+							message: "无权限",
+							type: "warn",
+						})
+						reject({ message: t("无权限") } as Response);
+					}
+
+					// 502 服务异常
+					else if (res.statusCode == 502 || res.statusCode == 405) {
+						ui.showToast({
+							message: message || "服务异常",
+							type: "warn",
+						})
+						reject({
+							message: message || t("服务异常")
+						} as Response);
+					}
+
+					// 404 未找到
+					else if (res.statusCode == 404) {
+						ui.showToast({
+							message: `[404] ${url}`,
+							type: "warn",
+						})
+						return reject({
+							message: `[404] ${url}`
+						} as Response);
+					}
+
+					// 200 正常响应
+					else if (res.statusCode == 200) {
+						if (res.data == null) {
+							resolve(null);
+						} else if (!isObject(res.data as any)) {
+							resolve(res.data);
+						} else {
+							// 解析响应数据
+							if (code == 200 || code == 0) {
+								resolve(data);
+							} else if (code == 1) {
+								reject(res.data);
+							}
+							else {
+								ui.showToast({
+									message: message ?? t("服务异常"),
+									type: "warn",
+								})
+								reject({ message: message } as Response);
+							}
+						}
+					} else {
+						ui.showToast({
+							message: message || "服务异常",
+							type: "warn",
+						})
+						reject({ message: message || "服务异常" } as Response);
+					}
+				},
+
+				// 网络请求失败
+				fail(err) {
+					reject({ message: err.errMsg } as Response);
+				}
+			});
+		};
+
+		next();
+	});
+}
+export function useGet(url: string, data?: any, config?: RequestOptions): Promise<any> {
+	const options: RequestOptions = {
+		url,
+		data,
+		method: 'GET',
+		...config,
+	}
+	return request(options)
+}
+export function usePost(url: string, data?: any, config?: RequestOptions): Promise<any> {
+	const options: RequestOptions = {
+		url,
+		data,
+		method: 'POST',
+		...config,
+	}
+	return request(options)
+}
+export function usePut(url: string, data?: any, config?: RequestOptions): Promise<any> {
+	const options: RequestOptions = {
+		url,
+		data,
+		method: 'PUT',
+		...config,
+	}
+	return request(options)
+}
+export function useDelete(url: string, data?: any, config?: RequestOptions): Promise<any> {
+	const options: RequestOptions = {
+		url,
+		data,
+		method: 'DELETE',
+		...config,
+	}
+	return request(options)
 }

+ 48 - 33
.cool/store/user.ts

@@ -3,7 +3,7 @@ import { forInObject, isNull, isObject, parse, storage } from "../utils";
 import { router } from "../router";
 import { request } from "../service";
 import type { UserInfo } from "../types";
-import { type LoginData, getUsersInfo } from "@/services/user";
+import { type LoginData, getUsersInfo, refreshTokenApi } from "@/services/user";
 import { dict } from "./dict";
 export type Token = {
 	token: string; // 访问token
@@ -18,6 +18,10 @@ export class User {
 	 */
 	info = ref<UserInfo | null>(null);
 	/**
+	 * token刷新中的Promise,防止并发刷新
+	 */
+	private refreshing: Promise<string> | null = null;
+	/**
 	 * 用户信息,响应式对象
 	 */
 	courseId = ref<string | null>(null);
@@ -57,7 +61,7 @@ export class User {
 				this.set(res);
 			}
 		} catch (err) {
-			this.logout();
+			console.warn("获取用户信息失败:", err);
 		}
 	}
 
@@ -123,7 +127,7 @@ export class User {
 	clear() {
 		storage.remove("userInfo");
 		storage.remove("token");
-		// storage.remove("refreshToken");
+		storage.remove("refreshToken");
 		this.token = 'Basic ZW5kOmVuZA==';
 		this.remove();
 		this.info.value = null;
@@ -149,39 +153,50 @@ export class User {
 		storage.set("token", this.token, 7 * 24 * 60 * 60 - 5);
 		this.get();
 		dict.refresh();
+		
+		// 存储refresh_token(服务端默认30天有效期)
+		if (data != null && data.refresh_token != null) {
+			storage.set("refreshToken", data.refresh_token, 30 * 24 * 60 * 60 - 5);
+		}
 
-		// 刷新token,提前5秒过期
-		// storage.set("refreshToken", data.refresh_token, data.refreshExpire - 5);
 	}
 
-	// /**
-	//  * 刷新token(调用服务端接口,自动更新本地token)
-	//  * @returns Promise<string> 新的token
-	//  */
-	// refreshToken(): Promise<string> {
-	// 	return new Promise((resolve, reject) => {
-	// 		request({
-	// 			url: "/app/user/login/refreshToken",
-	// 			method: "POST",
-	// 			data: {
-	// 				refreshToken: storage.get("refreshToken")
-	// 			}
-	// 		})
-	// 			.then((res) => {
-	// 				if (res != null) {
-	// 					const token = parse<Token>(res);
-
-	// 					if (token != null) {
-	// 						this.setToken(token);
-	// 						resolve(token.token);
-	// 					}
-	// 				}
-	// 			})
-	// 			.catch((err) => {
-	// 				reject(err);
-	// 			});
-	// 	});
-	// }
+
+	/**
+	 * 刷新token(调用服务端OAuth2接口,自动更新本地token)
+	 * @returns Promise<string> 新的token
+	 */
+	async refreshToken(): Promise<string> {
+		// 防止并发刷新
+		if (this.refreshing != null) {
+			return this.refreshing;
+		}
+
+		const storedRefreshToken = storage.get("refreshToken") as string;
+		if (storedRefreshToken == '' || storedRefreshToken.length == 0) {
+			return Promise.reject(new Error("No refresh token available"));
+		}
+
+		this.refreshing = (async () => {
+			try {
+				const res = await refreshTokenApi(storedRefreshToken);
+				if (res != null && res.data != null) {
+					const respData = res.data as UTSJSONObject;
+					const loginData: LoginData = {
+						access_token: respData["access_token"] as string,
+						refresh_token: respData["refresh_token"] as string,
+					};
+					this.setToken(loginData);
+					return this.token;
+				}
+				throw new Error("Refresh token response invalid");
+			} finally {
+				this.refreshing = null;
+			}
+		})();
+
+		return this.refreshing;
+	}
 }
 
 /**

+ 7 - 0
services/user.ts

@@ -43,6 +43,13 @@ export interface DICT_DATA {
 export function loginApi(params) {
   return usePost(`/oauth/token?${stringify(params)}`) as Promise<LoginData>
 }
+export function refreshTokenApi(refreshToken: string) {
+  return usePost(
+    `/oauth/token?grant_type=refresh_token&refresh_token=${refreshToken}`,
+    null,
+    { header: { Authorization: 'Basic ZW5kOmVuZA==' } }
+  ) as Promise<LoginData>
+}
 export function sendSmsCode(params) {
   return usePost(`/verify/sms/code/send`, params) as Promise<string>
 }