From fd1175f59fee610cb04a7a23fe832cc037479d2f Mon Sep 17 00:00:00 2001 From: overtrue Date: Sat, 11 Jan 2025 15:25:46 +0800 Subject: [PATCH] feat: login --- .env.example | 11 +- .gitignore | 2 + composables/useAuth.ts | 53 ++++++++++ lib/api-client.ts | 29 ++++-- lib/sts.ts | 37 +++++++ middleware/auth.global.ts | 24 +++-- nuxt.config.ts | 12 ++- package-lock.json | 213 ++++++++++++++++++++++++++++++++++++++ package.json | 6 ++ pages/auth/login.vue | 44 ++++---- plugins/api.ts | 49 +++++---- plugins/s3.ts | 12 ++- 12 files changed, 414 insertions(+), 78 deletions(-) create mode 100644 composables/useAuth.ts create mode 100644 lib/sts.ts diff --git a/.env.example b/.env.example index bbc698c..e51baab 100644 --- a/.env.example +++ b/.env.example @@ -1,12 +1,13 @@ APP_NAME=RustFS -APP_DESCRIPTION=RustFS is a distributed file system written in Rust. +# Description +APP_DESCRIPTION="RustFS is a distributed file system written in Rust." -# console api base url +# Admin API base URL API_BASE_URL=http://localhost:61307 -# 测试 S3 凭证,控制台创建,仅本地调试使用 +# S3 API base URL S3_ENDPOINT=http://localhost:9000 + +# default region S3_REGION=us-east-1 -S3_ACCESS_KEY_ID= -S3_SECRET_ACCESS_KEY= diff --git a/.gitignore b/.gitignore index 96a858b..d056894 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ logs .env .env.* !.env.example + +pages/test diff --git a/composables/useAuth.ts b/composables/useAuth.ts new file mode 100644 index 0000000..278b79f --- /dev/null +++ b/composables/useAuth.ts @@ -0,0 +1,53 @@ +import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types"; +import { getStsToken } from "~/lib/sts"; + +interface Credentials { + AccessKeyId?: string; + SecretAccessKey?: string; + SessionToken?: string; + Expiration?: string; +} + +export function useAuth() { + const store = useLocalStorage('auth.credentials', {}) + + const setCredentials = (credentials: Credentials) => { + store.value = credentials + } + + const getCredentials = () => { + if (!isValiedCredentials(store.value)) { + return + } + + return store.value + } + + const isExpired = (expiration: string) => expiration ? new Date(expiration) < new Date() : false + + const isValiedCredentials = (credentials: Credentials) => { + return !!credentials?.AccessKeyId && !!credentials?.SecretAccessKey && !!credentials?.SessionToken && credentials?.Expiration && !isExpired(credentials.Expiration) + } + + const login = async (credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider) => { + const credentialsResponse = await getStsToken(credentials, 'arn:aws:iam::*:role/Admin') + + setCredentials({ + ...credentialsResponse, + Expiration: credentialsResponse.Expiration?.toISOString() + }) + + return credentialsResponse + } + + const logout = () => { + store.value = {} + } + + return { + login, + logout, + credentials: ref(getCredentials()), + isAuthenticated: ref(isValiedCredentials(store.value)), + } +} diff --git a/lib/api-client.ts b/lib/api-client.ts index 206d5f4..69566c1 100644 --- a/lib/api-client.ts +++ b/lib/api-client.ts @@ -1,42 +1,51 @@ +import type { AwsClient } from "aws4fetch" +import { joinURL } from "ufo" -type FetchType = typeof $fetch class ApiClient { private $api: any + private config?: { baseUrl?: string, headers?: Record } - constructor(api: FetchType) { + constructor(api: AwsClient, options?: any) { this.$api = api + this.config = options + } + + async request(url: string, options?: any) { + url = this.config?.baseUrl ? joinURL(this.config?.baseUrl, url) : url + options.headers = { ...this.config?.headers, ...options.headers } + return this.$api.fetch(url, options) } async get(url: string, options?: any) { - return this.$api(url, { method: 'GET', ...options }) + return this.request(url, { method: 'GET', ...options }) } async post(url: string, body: any, options?: any) { - return this.$api(url, { method: 'POST', body, ...options }) + return this.request(url, { method: 'POST', body, ...options }) } async delete(url: string, options?: any) { - return this.$api(url, { method: 'DELETE', ...options }) + return this.request(url, { method: 'DELETE', ...options }) } async put(url: string, body: any, options?: any) { - return this.$api(url, { method: 'PUT', body, ...options }) + return this.request(url, { method: 'PUT', body, ...options }) } async patch(url: string, body: any, options?: any) { - return this.$api(url, { method: 'PATCH', body, ...options }) + return this.request(url, { method: 'PATCH', body, ...options }) } async head(url: string, options?: any) { - return this.$api(url, { method: 'HEAD', ...options }) + return this.request(url, { method: 'HEAD', ...options }) } async options(url: string, options?: any) { - return this.$api(url, { method: 'OPTIONS', ...options }) + return this.request(url, { method: 'OPTIONS', ...options }) } async trace(url: string, options?: any) { - return this.$api(url, { method: 'TRACE', ...options }) + return this.request(url, { method: 'TRACE', ...options }) } } diff --git a/lib/sts.ts b/lib/sts.ts new file mode 100644 index 0000000..6fc6020 --- /dev/null +++ b/lib/sts.ts @@ -0,0 +1,37 @@ +import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts"; +import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from "@aws-sdk/types"; + +/** + * 获取 STS 临时凭证并返回 + * @param {string} ak - 主账号 AccessKey + * @param {string} sk - 主账号 SecretKey + * @param {string} roleArn - 需要扮演的角色 ARN + * @returns {Promise} + */ +export async function getStsToken(credentials: AwsCredentialIdentity | AwsCredentialIdentityProvider, roleArn: string) { + const runtimeConfig = useRuntimeConfig().public; + console.log('S3 runtimeConfig', runtimeConfig); + + // 1. 创建 STS 客户端 + const stsClient = new STSClient({ + endpoint: runtimeConfig.s3.endpoint, + region: runtimeConfig.s3.region || 'us-east-1', + credentials: credentials + }); + + // 2. 构建 AssumeRole 请求 + // 你可以根据需要额外添加 DurationSeconds、Policy 等参数 + const command = new AssumeRoleCommand({ + RoleArn: roleArn, + RoleSessionName: "console", // 自定义角色会话名称 + DurationSeconds: runtimeConfig.session.durationSeconds || 3600 * 12, // 临时凭证有效期 + }); + + const response = await stsClient.send(command); + + if (!response.Credentials) { + throw new Error("Failed to retrieve credentials"); + } + + return response.Credentials; +} diff --git a/middleware/auth.global.ts b/middleware/auth.global.ts index d90610d..3657e73 100644 --- a/middleware/auth.global.ts +++ b/middleware/auth.global.ts @@ -1,17 +1,19 @@ + + export default defineNuxtRouteMiddleware((to, from) => { - // const token = useLocalStorage('auth.token', undefined) + const isAuthenticated = useAuth().isAuthenticated.value - // console.log('Auth middleware', to.path, token.value); + console.debug('Auth middleware', to.path, isAuthenticated); - // if (to.path === '/auth/login') { - // if (token.value) { - // return navigateTo('/') - // } + if (to.path === '/auth/login') { + if (isAuthenticated) { + return navigateTo('/') + } - // return - // } + return + } - // if (!token.value) { - // return navigateTo('/auth/login') - // } + if (!isAuthenticated) { + return navigateTo('/auth/login') + } }) diff --git a/nuxt.config.ts b/nuxt.config.ts index dea8f75..dba3c89 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -23,16 +23,20 @@ export default defineNuxtConfig({ ], runtimeConfig: { public: { + session: { + // 临时凭证有效期 + durationSeconds: Number(process.env.SESSION_DURATION_SECONDS) || 3600 * 12 + }, + + // API 请求基础 URL api: { baseURL: process.env.API_BASE_URL || '' }, + // 临时配置,后续登录后从本地存储中获取 s3: { region: process.env.S3_REGION || 'us-east-1', - endpoint: process.env.S3_ENDPOINT || process.env.API_BASE_URL || '', - accessKeyId: process.env.S3_ACCESS_KEY_ID || '', - secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || '', - sessionToken: process.env.S3_SESSION_TOKEN || '' + endpoint: process.env.S3_ENDPOINT || process.env.API_BASE_URL || '' } } }, diff --git a/package-lock.json b/package-lock.json index cf949d9..5440371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,11 @@ "@pinia/nuxt": "^0.9.0", "@vueuse/integrations": "^12.0.0", "@vueuse/nuxt": "^12.0.0", + "aws-sdk": "^2.1692.0", + "aws4": "^1.13.2", + "aws4-axios": "^3.3.14", + "aws4fetch": "^1.0.20", + "axios": "^1.7.9", "json-editor-vue": "^0.17.3", "lodash": "^4.17.21", "rustfs": "^8.0.2", @@ -29,6 +34,7 @@ }, "devDependencies": { "@iconify-json/ri": "^1.2.3", + "@types/aws4": "^1.11.6", "naive-ui": "^2.40.3", "typescript": "^5.7.2", "unplugin-auto-import": "^0.18.6", @@ -4967,6 +4973,16 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aws4": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@types/aws4/-/aws4-1.11.6.tgz", + "integrity": "sha512-5CnVUkHNyLGpD9AnOcK66YyP0qvIh6nhJJoeK8zSl5YKikUcUbdB7SlHevUYVqicgeh6j5AJa1qa/h08dSZHoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -6158,6 +6174,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -6219,6 +6241,111 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/aws-sdk/node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "license": "MIT", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/aws-sdk/node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", + "license": "BSD-3-Clause" + }, + "node_modules/aws-sdk/node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", + "license": "ISC" + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" + }, + "node_modules/aws4-axios": { + "version": "3.3.14", + "resolved": "https://registry.npmjs.org/aws4-axios/-/aws4-axios-3.3.14.tgz", + "integrity": "sha512-M/JL9ISNtYeyGOtcFI1lPFPX5tziL9Ininbm0k6FxxjvWhmA8NdC3nMJhL2SuRNZcfzrtJo037ZwPf60r+DfGA==", + "license": "MIT", + "workspaces": [ + "infra" + ], + "dependencies": { + "@aws-sdk/client-sts": "^3.4.1", + "aws4": "^1.12.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "axios": ">=1.6.0" + } + }, + "node_modules/aws4fetch": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/aws4fetch/-/aws4fetch-1.0.20.tgz", + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/axobject-query/-/axobject-query-4.1.0.tgz", @@ -6824,6 +6951,18 @@ "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -7433,6 +7572,15 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -8319,6 +8467,26 @@ "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8356,6 +8524,20 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -12115,6 +12297,12 @@ "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", "license": "MIT" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12143,6 +12331,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -14387,6 +14584,22 @@ "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", "license": "MIT" }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "license": "MIT", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", + "license": "MIT" + }, "node_modules/urlpattern-polyfill": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", diff --git a/package.json b/package.json index 5e09698..a7be433 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,11 @@ "@pinia/nuxt": "^0.9.0", "@vueuse/integrations": "^12.0.0", "@vueuse/nuxt": "^12.0.0", + "aws-sdk": "^2.1692.0", + "aws4": "^1.13.2", + "aws4-axios": "^3.3.14", + "aws4fetch": "^1.0.20", + "axios": "^1.7.9", "json-editor-vue": "^0.17.3", "lodash": "^4.17.21", "rustfs": "^8.0.2", @@ -32,6 +37,7 @@ }, "devDependencies": { "@iconify-json/ri": "^1.2.3", + "@types/aws4": "^1.11.6", "naive-ui": "^2.40.3", "typescript": "^5.7.2", "unplugin-auto-import": "^0.18.6", diff --git a/pages/auth/login.vue b/pages/auth/login.vue index c68cd57..0657579 100644 --- a/pages/auth/login.vue +++ b/pages/auth/login.vue @@ -1,34 +1,32 @@