打通sso到wayne登陆的全流程
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
.cache/
|
.cache/
|
||||||
.env
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.tar
|
*.tar
|
||||||
bin/
|
bin/
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ go run ./cmd/server
|
|||||||
发布脚本会在本地完成:
|
发布脚本会在本地完成:
|
||||||
|
|
||||||
- 构建后端和前端 Docker 镜像
|
- 构建后端和前端 Docker 镜像
|
||||||
- 读取本地 `.env`,生成并应用线上 `ConfigMap`/`Secret`
|
- 读取本地 `.env.server`,生成并应用线上 `ConfigMap`/`Secret`
|
||||||
- 导出 `authserver-images-<version>.tar`
|
- 导出 `authserver-images-<version>.tar`
|
||||||
- 上传到服务器
|
- 上传到服务器
|
||||||
- 导入 RKE2 使用的 `containerd`
|
- 导入 RKE2 使用的 `containerd`
|
||||||
@@ -102,7 +102,7 @@ go run ./cmd/server
|
|||||||
- 远端目录:`/authserver`
|
- 远端目录:`/authserver`
|
||||||
- containerd socket:`/run/k3s/containerd/containerd.sock`
|
- containerd socket:`/run/k3s/containerd/containerd.sock`
|
||||||
|
|
||||||
发布脚本默认把仓库根目录的 `.env` 当作线上配置源。也就是说,发版前需要先把 `.env` 改成目标环境的实际值;脚本会在发版时重新生成并应用 `authserver-config` 和 `authserver-secret`。
|
发布脚本默认把仓库根目录的 `.env.server` 当作服务器配置源;脚本会在发版时重新生成并应用 `authserver-config` 和 `authserver-secret`。本地开发用的 `.env` 不会被默认发布。
|
||||||
|
|
||||||
脚本默认不会拦截测试环境配置。如果你要对正式环境启用更严格的保护,可以显式打开:
|
脚本默认不会拦截测试环境配置。如果你要对正式环境启用更严格的保护,可以显式打开:
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ bash scripts/release.sh 1.0.5
|
|||||||
如果需要覆盖默认值,可以传环境变量:
|
如果需要覆盖默认值,可以传环境变量:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ENV_FILE=.env \
|
ENV_FILE=.env.server \
|
||||||
DEPLOY_HOST=218.11.5.223 \
|
DEPLOY_HOST=218.11.5.223 \
|
||||||
DEPLOY_KEY=/path/to/key.pem \
|
DEPLOY_KEY=/path/to/key.pem \
|
||||||
REMOTE_DIR=/authserver \
|
REMOTE_DIR=/authserver \
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
|||||||
if email == "" {
|
if email == "" {
|
||||||
return nil, ErrWayenEmailMissing
|
return nil, ErrWayenEmailMissing
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(s.cfg.OAuthRedirectURI) != "" && strings.TrimSpace(s.cfg.WayenTargetURL) != "" {
|
||||||
|
target, err := s.oauthLoginURL(s.cfg.OAuthRedirectURI, s.cfg.WayenTargetURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &WayenLoginResult{TargetURL: target}, nil
|
||||||
|
}
|
||||||
if strings.TrimSpace(s.cfg.WayenLoginURL) == "" || strings.TrimSpace(s.cfg.WayenTargetURL) == "" {
|
if strings.TrimSpace(s.cfg.WayenLoginURL) == "" || strings.TrimSpace(s.cfg.WayenTargetURL) == "" {
|
||||||
return nil, ErrWayenNotConfigured
|
return nil, ErrWayenNotConfigured
|
||||||
}
|
}
|
||||||
@@ -100,6 +107,28 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, error) {
|
||||||
|
parsed, err := url.Parse(strings.TrimSpace(redirectURI))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
next, err := url.Parse(strings.TrimSpace(targetURL))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if next.Path == "" || next.Path == "/" {
|
||||||
|
next.Path = "/sign-in"
|
||||||
|
}
|
||||||
|
values := next.Query()
|
||||||
|
values.Set("ref", "oauth")
|
||||||
|
next.RawQuery = values.Encode()
|
||||||
|
|
||||||
|
query := parsed.Query()
|
||||||
|
query.Set("next", next.String())
|
||||||
|
parsed.RawQuery = query.Encode()
|
||||||
|
return parsed.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *WayenService) loginRequest(loginURL, email, password string) (string, io.Reader, string, error) {
|
func (s *WayenService) loginRequest(loginURL, email, password string) (string, io.Reader, string, error) {
|
||||||
usernameKey := defaultConfigValue(s.cfg.WayenUsernameKey, "email")
|
usernameKey := defaultConfigValue(s.cfg.WayenUsernameKey, "email")
|
||||||
passwordKey := defaultConfigValue(s.cfg.WayenPasswordKey, "password")
|
passwordKey := defaultConfigValue(s.cfg.WayenPasswordKey, "password")
|
||||||
|
|||||||
@@ -41,6 +41,18 @@ server {
|
|||||||
proxy_read_timeout 60s;
|
proxy_read_timeout 60s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /auth/oauth/ {
|
||||||
|
proxy_pass http://authserver-backend:8083;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
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;
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
location /auth/ {
|
location /auth/ {
|
||||||
try_files $uri $uri/ /auth/index.html;
|
try_files $uri $uri/ /auth/index.html;
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-61
@@ -1,24 +1,16 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const TOKEN_KEY = 'authserver_token'
|
const TOKEN_KEY = 'authserver_token'
|
||||||
const SSO_LOGIN_PATH = '/auth/api/v1/login/internal-sso'
|
const SSO_LOGIN_PATH = '/auth/api/v1/login/internal-sso'
|
||||||
const WAYEN_LOGIN_PATH = '/auth/api/v1/wayen/login'
|
const WAYEN_LOGIN_PATH = '/auth/api/v1/wayen/login'
|
||||||
const WAYEN_CREDENTIAL_PATH = '/auth/api/v1/wayen/credential'
|
|
||||||
|
|
||||||
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
|
const token = ref(localStorage.getItem(TOKEN_KEY) || '')
|
||||||
const currentUser = ref(null)
|
const currentUser = ref(null)
|
||||||
const message = ref('')
|
const message = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const ssoLoading = ref(false)
|
const redirectingToSSO = ref(false)
|
||||||
const wayenLoading = ref(false)
|
const wayenLoading = ref(false)
|
||||||
const wayenCredentialLoading = ref(false)
|
|
||||||
const wayenConfigured = ref(false)
|
|
||||||
|
|
||||||
const wayenForm = reactive({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAuthed = computed(() => Boolean(token.value))
|
const isAuthed = computed(() => Boolean(token.value))
|
||||||
const currentUserLabel = computed(() => {
|
const currentUserLabel = computed(() => {
|
||||||
@@ -34,10 +26,20 @@ function clearAuth() {
|
|||||||
localStorage.removeItem(TOKEN_KEY)
|
localStorage.removeItem(TOKEN_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ssoLogin() {
|
function relayState(openWayen = false) {
|
||||||
const relayState = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/'
|
const url = new URL(window.location.href)
|
||||||
ssoLoading.value = true
|
if (openWayen) {
|
||||||
window.location.assign(`${SSO_LOGIN_PATH}?relay_state=${encodeURIComponent(relayState)}`)
|
url.searchParams.set('open_wayen', '1')
|
||||||
|
}
|
||||||
|
return `${url.pathname}${url.search}${url.hash}` || '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ssoLogin({ openWayen = false } = {}) {
|
||||||
|
if (redirectingToSSO.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
redirectingToSSO.value = true
|
||||||
|
window.location.assign(`${SSO_LOGIN_PATH}?relay_state=${encodeURIComponent(relayState(openWayen))}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
@@ -53,6 +55,7 @@ async function api(path, options = {}) {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
clearAuth()
|
clearAuth()
|
||||||
|
ssoLogin({ openWayen: true })
|
||||||
}
|
}
|
||||||
const error = new Error(data.error || `HTTP ${response.status}`)
|
const error = new Error(data.error || `HTTP ${response.status}`)
|
||||||
error.status = response.status
|
error.status = response.status
|
||||||
@@ -78,41 +81,13 @@ async function run(task, successText = '操作完成') {
|
|||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
clearAuth()
|
clearAuth()
|
||||||
message.value = '已退出'
|
ssoLogin()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadMe() {
|
async function loadMe() {
|
||||||
currentUser.value = await api('/auth/api/v1/users/me')
|
currentUser.value = await api('/auth/api/v1/users/me')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadWayenCredential() {
|
|
||||||
const result = await api(WAYEN_CREDENTIAL_PATH)
|
|
||||||
wayenForm.email = result.email || (currentUser.value && currentUser.value.email) || ''
|
|
||||||
wayenConfigured.value = Boolean(result.configured)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveWayenCredential() {
|
|
||||||
wayenCredentialLoading.value = true
|
|
||||||
message.value = ''
|
|
||||||
try {
|
|
||||||
const result = await api(WAYEN_CREDENTIAL_PATH, {
|
|
||||||
method: 'PUT',
|
|
||||||
body: JSON.stringify(wayenForm),
|
|
||||||
})
|
|
||||||
wayenForm.email = result.email || wayenForm.email
|
|
||||||
wayenForm.password = ''
|
|
||||||
wayenConfigured.value = Boolean(result.configured)
|
|
||||||
if (currentUser.value) {
|
|
||||||
currentUser.value.email = wayenForm.email
|
|
||||||
}
|
|
||||||
message.value = 'Wayen 凭据已保存'
|
|
||||||
} catch (error) {
|
|
||||||
message.value = error.message
|
|
||||||
} finally {
|
|
||||||
wayenCredentialLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openWayen() {
|
async function openWayen() {
|
||||||
wayenLoading.value = true
|
wayenLoading.value = true
|
||||||
message.value = ''
|
message.value = ''
|
||||||
@@ -128,6 +103,7 @@ async function openWayen() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
clearAuth()
|
clearAuth()
|
||||||
|
ssoLogin({ openWayen: true })
|
||||||
}
|
}
|
||||||
throw new Error(data.error || `HTTP ${response.status}`)
|
throw new Error(data.error || `HTTP ${response.status}`)
|
||||||
}
|
}
|
||||||
@@ -145,19 +121,24 @@ async function openWayen() {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
const ssoToken = url.searchParams.get('sso_token')
|
const ssoToken = url.searchParams.get('sso_token')
|
||||||
|
const shouldOpenWayen = url.searchParams.get('open_wayen') === '1'
|
||||||
if (ssoToken) {
|
if (ssoToken) {
|
||||||
token.value = ssoToken
|
token.value = ssoToken
|
||||||
localStorage.setItem(TOKEN_KEY, ssoToken)
|
localStorage.setItem(TOKEN_KEY, ssoToken)
|
||||||
url.searchParams.delete('sso_token')
|
url.searchParams.delete('sso_token')
|
||||||
|
url.searchParams.delete('open_wayen')
|
||||||
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
|
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
|
||||||
}
|
}
|
||||||
if (!token.value) {
|
if (!token.value) {
|
||||||
|
ssoLogin({ openWayen: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await run(async () => {
|
await run(async () => {
|
||||||
await loadMe()
|
await loadMe()
|
||||||
await loadWayenCredential()
|
|
||||||
}, '已就绪')
|
}, '已就绪')
|
||||||
|
if (shouldOpenWayen) {
|
||||||
|
await openWayen()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -168,9 +149,7 @@ onMounted(async () => {
|
|||||||
<p class="eyebrow">AuthServer</p>
|
<p class="eyebrow">AuthServer</p>
|
||||||
<h1>Wayen 登录入口</h1>
|
<h1>Wayen 登录入口</h1>
|
||||||
</div>
|
</div>
|
||||||
<p class="message">当前仅保留 SSO 登录和 Wayen 单点跳转。</p>
|
<p class="message">正在跳转到 SSO 登录...</p>
|
||||||
<button class="primary" type="button" :disabled="ssoLoading" @click="ssoLogin">SSO 登录</button>
|
|
||||||
<p v-if="message" class="message">{{ message }}</p>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -191,20 +170,6 @@ onMounted(async () => {
|
|||||||
<span class="wayen-icon" aria-hidden="true">W</span>
|
<span class="wayen-icon" aria-hidden="true">W</span>
|
||||||
<span class="wayen-title">Wayen</span>
|
<span class="wayen-title">Wayen</span>
|
||||||
</button>
|
</button>
|
||||||
<form class="wayen-config form stack" @submit.prevent="saveWayenCredential">
|
|
||||||
<p class="config-title">Wayen 凭据</p>
|
|
||||||
<label>
|
|
||||||
<span>Wayen 账号</span>
|
|
||||||
<input v-model="wayenForm.email" autocomplete="email" />
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
<span>Wayen 密码</span>
|
|
||||||
<input v-model="wayenForm.password" type="password" autocomplete="current-password" />
|
|
||||||
</label>
|
|
||||||
<button class="secondary" type="submit" :disabled="wayenCredentialLoading">
|
|
||||||
{{ wayenConfigured ? '更新凭据' : '保存凭据' }}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<p v-if="message" class="message">{{ message }}</p>
|
<p v-if="message" class="message">{{ message }}</p>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -70,22 +70,6 @@ h1 {
|
|||||||
box-shadow: 0 18px 50px rgb(43 57 84 / 10%);
|
box-shadow: 0 18px 50px rgb(43 57 84 / 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form {
|
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stack {
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form label span {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
color: #526174;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.primary {
|
.primary {
|
||||||
border-color: #1b64d8;
|
border-color: #1b64d8;
|
||||||
background: #1b64d8;
|
background: #1b64d8;
|
||||||
@@ -190,21 +174,6 @@ h1 {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wayen-config {
|
|
||||||
width: min(360px, 100%);
|
|
||||||
padding: 22px;
|
|
||||||
border: 1px solid #dbe3ef;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #ffffff;
|
|
||||||
box-shadow: 0 18px 50px rgb(43 57 84 / 10%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
color: #172033;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) {
|
||||||
.wayen-header {
|
.wayen-header {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|||||||
Reference in New Issue
Block a user