Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions apps/daas/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import App from '@/App.vue'
import { installOEM } from '@/oem'
import { installAllPlugins } from '@/plugins'
import { initRequestClient } from '@/plugins/axios'
import { configUser, getUrlSearch, signOut } from '@/utils/util'
import {
configUser,
getUrlSearch,
removeUrlParams,
signOut,
} from '@/utils/util'
import store from '@/vuex' // 引入全局数据控制
import { installDirectives } from './directives'
import i18n from './i18n'
Expand Down Expand Up @@ -71,6 +76,7 @@ const URL_LANG = getUrlSearch('lang')

if (TOKEN) {
Cookie.set('access_token', TOKEN)
removeUrlParams('token')
}

const token = Cookie.get('access_token')
Expand Down Expand Up @@ -100,9 +106,8 @@ const init = () => {

app.config.globalProperties.$ws = new WSClient(wsUrl, undefined, {
getQuery: () => {
return {
access_token: Cookie.get('access_token'),
}
// Same-origin WS sends the access_token cookie; do not put the token in the URL (TAP-11883).
return {}
},
})

Expand Down
55 changes: 35 additions & 20 deletions apps/daas/src/plugins/axios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { signOut } from '../utils/util'

type AxiosRequestConfigPro = AxiosRequestConfig & {
silenceMessage?: boolean
skipAuthExpire?: boolean
}

// Pending requests map for cancellation
Expand Down Expand Up @@ -60,6 +61,9 @@ const errorCallback = (error: any): Promise<any> => {
switch (rsp.status) {
// 用户无权限访问接口
case 401: {
if ((error.config as AxiosRequestConfigPro)?.skipAuthExpire) {
break
}
const isSingleSession = getSettingByKey('login.single.session', 'open')

signOut()
Expand Down Expand Up @@ -114,6 +118,35 @@ const errorCallback = (error: any): Promise<any> => {
return Promise.reject(error)
}

function stripUrlAccessToken(url?: string) {
if (!url) {
return url
}
let next = url.replaceAll(/([?&])access_token=[^&]*/gi, '$1')
next = next.replaceAll('?&', '?').replaceAll('&&', '&')
if (next.endsWith('?') || next.endsWith('&')) {
next = next.slice(0, -1)
}
return next
}

function applyBearerAuth(config: InternalAxiosRequestConfig) {
const accessToken = Cookie.get('access_token')
if (accessToken) {
config.headers = config.headers || {}
if (!config.headers.Authorization) {
config.headers.Authorization = `Bearer ${accessToken}`
}
}
if (config.url) {
config.url = stripUrlAccessToken(config.url)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 这里只清理 Axios 请求。SAML 登出通过 window.location.href = getSamlLogoutUrl(accessToken) 导航,packages/api/src/core/sso.ts:38 仍会生成 /logout?access_token=...,所以该凭证绕过此拦截器并进入请求 URL。请一并调整登出调用和服务端契约,确保 URL 不再携带 token。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

已修复(2f688ddfa),并同步调整了服务端契约:tapdata/tapdata#3516。

  • 前端:getSamlLogoutUrl(token) 改为 startSamlLogout(token),以 POST /api/sso/saml/logout + Authorization: Bearer 发起登出,后端返回 redirectUrl(IdP SLO 地址或登出后页面),signOut() 再跳转;请求失败时回到 #/login。登出 URL 不再携带 token。
  • 后端(#3516):新增 POST /logout,通过 AccessTokenResolver 从请求头解析 token;GET /logout 保留兼容旧前端并标记 deprecated,url-token-mode=REJECT 时忽略 URL token。

注意发布顺序:本 PR 依赖 #3516,需在后端合并部署后(或同时)再合并,否则 SAML 登出只会回到登录页而不会终止服务端会话 / 触发 IdP SLO。

}
if (config.params && typeof config.params === 'object') {
delete (config.params as Record<string, unknown>).access_token
}
return config
}

axios.interceptors.request.use(function (
config: InternalAxiosRequestConfig,
): InternalAxiosRequestConfig {
Expand All @@ -123,16 +156,7 @@ axios.interceptors.request.use(function (
encoder: (str) => window.encodeURIComponent(str),
})
}
const accessToken = Cookie.get('access_token')
if (accessToken && config.url) {
if (~config.url.indexOf('?')) {
if (!~config.url.indexOf('access_token')) {
config.url = `${config.url}&access_token=${accessToken}`
}
} else {
config.url = `${config.url}?access_token=${accessToken}`
}
}
applyBearerAuth(config)
if (config.headers) {
config.headers['x-requested-with'] = 'XMLHttpRequest'
}
Expand Down Expand Up @@ -170,16 +194,7 @@ export function initRequestClient() {
requestClient.addRequestInterceptor({
synchronous: true,
fulfilled: (config) => {
const accessToken = Cookie.get('access_token')
if (accessToken) {
if (config.url?.indexOf?.('?') !== -1) {
if (config.url?.indexOf?.('access_token') === -1) {
config.url = `${config.url}&access_token=${accessToken}`
}
} else {
config.url = `${config.url}?access_token=${accessToken}`
}
}
applyBearerAuth(config)
if (isPassiveScope()) {
config.headers = config.headers || {}
config.headers['X-User-Activity'] = '0'
Expand Down
48 changes: 43 additions & 5 deletions apps/daas/src/utils/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getSamlLogoutUrl } from '@tap/api/src/core/sso'
import { startSamlLogout } from '@tap/api/src/core/sso'
import { getUserInfoByToken } from '@tap/api/src/core/users'
import Cookie from '@tap/shared/src/cookie'
import dayjs from 'dayjs'
Expand Down Expand Up @@ -160,17 +160,55 @@ export function signOut() {
Cookie.remove('user_id')
Cookie.remove('auth_method')
clearPermissions()
const loginHref = `${location.href.split('#')[0]}#/login`
if (isSamlSession) {
// The backend terminates the local session, then redirects to the IdP SLO
// endpoint (or straight back to the login page when SLO is not configured).
window.location.href = getSamlLogoutUrl(accessToken)
// The backend terminates the local session, then returns the IdP SLO
// endpoint (or the login page when SLO is not configured) to navigate to.
startSamlLogout(accessToken)
.then(({ redirectUrl }) => {
location.href = redirectUrl || loginHref
})
.catch(() => {
location.href = loginHref
})
return null
}
sessionStorage.setItem('lastLocationHref', location.href)
location.href = `${location.href.split('#')[0]}#/login`
location.href = loginHref
return null
}

/**
* Drop credential params from the address bar without adding a history entry,
* so the token is not kept in history or later copied into lastLocationHref
* (TAP-11883).
*/
export function removeUrlParams(...names: string[]) {
const search = new URLSearchParams(location.search)
const [hashPath, hashQuery = ''] = location.hash.split('?')
const hashParams = new URLSearchParams(hashQuery)
if (!names.some((name) => search.has(name) || hashParams.has(name))) {
return
}
for (const name of names) {
search.delete(name)
hashParams.delete(name)
}
const nextSearch = search.toString() ? `?${search}` : ''
const nextHash = hashParams.toString()
? `${hashPath}?${hashParams}`
: hashPath
// vue-router keeps the full route in history.state.current; drop it there too.
const state = history.state?.current
? { ...history.state, current: nextHash.slice(1) }
: history.state
history.replaceState(
state,
'',
`${location.pathname}${nextSearch}${nextHash}`,
)
}

export function getUrlSearch(name: string) {
// 未传参,返回空
if (!name) return null
Expand Down
8 changes: 2 additions & 6 deletions apps/daas/src/views/api-page/ApiServer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import {
import PageContainer from '@tap/business/src/components/PageContainer.vue'
import TablePage from '@tap/business/src/components/TablePage.vue'
import { useHas } from '@tap/business/src/composables'
import { downloadAuthenticated } from '@tap/api/src/download'
import i18n from '@tap/i18n'
import Cookie from '@tap/shared/src/cookie'
import { ElMessage, ElMessageBox } from 'element-plus'
import { escapeRegExp } from 'lodash-es'
import { h, nextTick, reactive, ref } from 'vue'
Expand Down Expand Up @@ -113,11 +113,7 @@ const remove = (item: Record<string, any>) => {

// 下载api配置文件
const downloadConfig = (item: Record<string, any>) => {
const token = Cookie.get('access_token')
window.open(
`${API_SERVER_BASE_URL}/download/${item.id}?access_token=${token}`,
'_blank',
)
downloadAuthenticated(`${API_SERVER_BASE_URL}/download/${item.id}`)
}

// 保存
Expand Down
5 changes: 4 additions & 1 deletion apps/daas/src/views/login/SsoCallback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetchSettings } from '@tap/api/core/settings'
import { getUserInfoByToken } from '@tap/api/src/core/users'
import Cookie from '@tap/shared/src/cookie'
import { setSettings } from '@tap/shared/src/settings'
import { configUser, getUrlSearch } from '@/utils/util'
import { configUser, getUrlSearch, removeUrlParams } from '@/utils/util'
import LoginPage from './LoginPage.vue'

export default {
Expand All @@ -25,6 +25,9 @@ export default {
// to the hash route. Convert that URL token into a cookie and run the same
// post-login bootstrap as the password path before the router guard runs.
const accessToken = getUrlSearch('access_token')
// Strip the token before any await so it never stays in the address bar
// or history, nor ends up in a later location.href (TAP-11883).
removeUrlParams('access_token')
if (!accessToken) {
this.redirectToLogin()
return
Expand Down
9 changes: 4 additions & 5 deletions packages/api/src/JavascriptFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from './download'
import Http from './Http'

export default class JavascriptFunctions extends Http {
constructor() {
super('/api/Javascript_functions')
}
export(ids) {
const href = `${
this.url
}/batch/load?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(
`${this.url}/batch/load?id=${ids.join('&id=')}`,
)
}
}
export { JavascriptFunctions }
16 changes: 7 additions & 9 deletions packages/api/src/Modules.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from './download'
import Http from './Http'

export default class Modules extends Http {
Expand Down Expand Up @@ -50,17 +50,15 @@ export default class Modules extends Http {
}

export(ids) {
const href = `${
this.url
}/batch/load?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(
`${this.url}/batch/load?id=${ids.join('&id=')}`,
)
}

apiExport(ids, ip) {
const href = `${
this.url
}/api/export?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}&ip=${ip}`
window.open(href)
return downloadAuthenticated(
`${this.url}/api/export?id=${ids.join('&id=')}&ip=${ip}`,
)
}

updatePermissions(data) {
Expand Down
9 changes: 4 additions & 5 deletions packages/api/src/PythonFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from './download'
import Http from './Http'

export default class PythonFunctions extends Http {
constructor() {
super('/api/python-functions')
}
export(ids) {
const href = `${
this.url
}/batch/load?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(
`${this.url}/batch/load?id=${ids.join('&id=')}`,
)
}
}
export { PythonFunctions }
9 changes: 4 additions & 5 deletions packages/api/src/Task.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isPlainObj } from '@tap/shared'
import Cookie from '@tap/shared/src/cookie'
import qs from 'qs'
import { downloadAuthenticated } from './download'
import Http from './Http'

export default class Task extends Http {
Expand Down Expand Up @@ -81,10 +81,9 @@ export default class Task extends Http {
}

export(ids) {
const href = `${
this.url
}/batch/load?taskId=${ids.join('&taskId=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(
`${this.url}/batch/load?taskId=${ids.join('&taskId=')}`,
)
}
checkRun(id) {
return this.axios.get(`${this.url}/checkRun/${id}`)
Expand Down
4 changes: 3 additions & 1 deletion packages/api/src/Users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export default class Users extends Http {
}

newResetPassword(token) {
return this.axios.post(`${this.url}/newResetPassword?access_token=${token}`)
return this.axios.post(`${this.url}/newResetPassword`, undefined, {
headers: { Authorization: `Bearer ${token}` },
})
}

confirm(id, token) {
Expand Down
7 changes: 4 additions & 3 deletions packages/api/src/core/connections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from '../download'
import { requestClient, type Filter } from '../request'

const BASE_URL = '/api/Connections'
Expand Down Expand Up @@ -115,8 +115,9 @@ export function findOneConnection(params: any) {
}

export function exportConnections(ids: string[]) {
const href = `${BASE_URL}/batch/load?connectionId=${ids.join('&connectionId=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(
`${BASE_URL}/batch/load?connectionId=${ids.join('&connectionId=')}`,
)
}

export function uploadConnections(data: any) {
Expand Down
5 changes: 2 additions & 3 deletions packages/api/src/core/function.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from '../download'
import { requestClient } from '../request'

const BASE_URL = '/api/Javascript_functions'
Expand Down Expand Up @@ -53,8 +53,7 @@ export function updateFunction(where: any, attributes: any) {
}

export function exportFunctions(ids: string[]) {
const href = `${BASE_URL}/batch/load?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(`${BASE_URL}/batch/load?id=${ids.join('&id=')}`)
}

export function uploadFunctions(data: any) {
Expand Down
5 changes: 2 additions & 3 deletions packages/api/src/core/javascript-functions.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from '../download'

const BASE_URL = '/api/Javascript_functions'

export function exportJavascriptFunctions(ids: string[]) {
const href = `${BASE_URL}/batch/load?id=${ids.join('&id=')}&access_token=${Cookie.get('access_token')}`
window.open(href)
return downloadAuthenticated(`${BASE_URL}/batch/load?id=${ids.join('&id=')}`)
}
7 changes: 3 additions & 4 deletions packages/api/src/core/metadata-instances.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Cookie from '@tap/shared/src/cookie'
import { downloadAuthenticated } from '../download'
import { requestClient, type PageFetchResult } from '../request'

const BASE_URL = '/api/MetadataInstances'
Expand All @@ -17,9 +17,8 @@ export function updateMetadataInstanceClassification(params: any) {

export function downloadMetadataInstance(where: any, type: string) {
if (typeof where === 'object') where = JSON.stringify(where)
const accessToken = Cookie.get('access_token') as string | undefined
window.open(
`${BASE_URL}/download?where=${encodeURIComponent(where)}&type=${type}&access_token=${encodeURIComponent(accessToken || '')}`,
return downloadAuthenticated(
`${BASE_URL}/download?where=${encodeURIComponent(where)}&type=${type}`,
)
}

Expand Down
Loading