iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Modern Web

對零開始,三十工備份社交網路(台文書寫)系列 第 8

簡單通用,抑是安全複雜?

  • 分享至 

  • xImage
  •  

簡單通用,抑是安全複雜?

前端 + A.I. = 這是啥物碗糕

因為以早有實做過 OAuth2.0 毋過彼當陣前端是用別人規畫好的筐仔,所以責任無踮家己身上,但是這馬愛考慮著前端的實做,所以咧寫程式愛細膩。

嘛因為家己無啥 Vue, Nuxt 的經驗,所以家己是交予 A.I. 處理 middleware 兼 composable 的初始化。

然後就得著這兩个物件

// composables/useAuthState.ts
import { computed, ref } from 'vue'

type AuthState = 'unknown' | 'auth' | 'unauth'
interface SessionPayload {
  result: string            // "0000" means OK (adjust to your API)
  roles?: string[]
  user?: { id: string; name?: string }
}

interface Snapshot {
  state: AuthState
  roles: string[]
  user?: { id: string; name?: string }
  updatedAt: number | null
}

const HARD_TTL_MS = 15_000   // ≤ hard TTL → trust cache, no request
const SOFT_TTL_MS = 120_000  // ≤ soft TTL → allow now, refresh in background

const snap = ref<Snapshot>({ state: 'unknown', roles: [], user: undefined, updatedAt: null })
let inflight: Promise<void> | null = null

function ageMs(): number {
  return snap.value.updatedAt ? Date.now() - snap.value.updatedAt : Number.POSITIVE_INFINITY
}

function setSnapshot(next: Partial<Snapshot>) {
  snap.value = { ...snap.value, ...next, updatedAt: next.updatedAt ?? Date.now() }
  // Optional non-sensitive hint for hard reloads
  try {
    sessionStorage.setItem('auth_hint', JSON.stringify({
      state: snap.value.state, roles: snap.value.roles, updatedAt: snap.value.updatedAt
    }))
  } catch { /* ignore */ }
}

// Bootstrap from hint (optional)
try {
  if (snap.value.state === 'unknown') {
    const hint = sessionStorage.getItem('auth_hint')
    if (hint) {
      const h = JSON.parse(hint) as Partial<Snapshot>
      setSnapshot({ state: (h.state as AuthState) ?? 'unknown', roles: h.roles ?? [], updatedAt: h.updatedAt ?? null })
    }
  }
} catch { /* ignore */ }

export function useAuthState() {
  const isAuthenticated = computed(() => snap.value.state === 'auth')
  const roles = computed(() => snap.value.roles)
  const snapshot = snap

  function ttlStatus() {
    const a = ageMs()
    if (a <= HARD_TTL_MS) return 'hard-fresh' as const
    if (a <= SOFT_TTL_MS) return 'soft-fresh' as const
    return 'stale' as const
  }

  async function doFetch(): Promise<void> {
    const { $api } = useNuxtApp()
    const res = await $api<SessionPayload>('/auth/state', { credentials: 'include' })
    if (res?.result === '0000') {
      setSnapshot({ state: 'auth', roles: Array.isArray(res.roles) ? res.roles : [], user: res.user })
    } else {
      setSnapshot({ state: 'unauth', roles: [], user: undefined })
    }
  }

  async function refresh(mode: 'foreground' | 'background' = 'foreground', force = false) {
    const freshness = ttlStatus()
    // Decide necessity based on TTL and mode
    if (!force) {
      if (mode === 'foreground' && freshness !== 'stale') return
      if (mode === 'background' && (freshness === 'hard-fresh' || inflight)) return
    }

    if (inflight) return inflight // single-flight

    inflight = doFetch()
      .catch((e) => {
        // Network/other errors: keep last known state but mark time to avoid spin
        setSnapshot({ updatedAt: Date.now() })
        if (mode === 'foreground') throw e
      })
      .finally(() => { inflight = null })

    return inflight
  }

  function invalidate() {
    // Force next check to be foreground (treat as stale)
    snap.value.updatedAt = null
  }

  function backgroundRefresh() {
    return refresh('background')
  }

  return { isAuthenticated, roles, snapshot, ttlStatus, refresh, backgroundRefresh, invalidate }
}

// middleware/auth.global.ts
export default defineNuxtRouteMiddleware(async (to) => {
  // Public routes (avoid loops)
  const publicPaths = new Set(['/login', '/oauth/callback', '/help'])
  if (to.meta.public === true || publicPaths.has(to.path)) return

  const { isAuthenticated, roles, ttlStatus, refresh, backgroundRefresh } = useAuthState()

  // TTL policy: minimize blocking calls
  const freshness = ttlStatus()
  if (freshness === 'hard-fresh') {
    // trust cache
  } else if (freshness === 'soft-fresh') {
    // allow now; quietly revalidate
    void backgroundRefresh()
  } else {
    // stale/unknown → one foreground revalidation
    try { await refresh('foreground') } catch { /* last-known state will drive decision */ }
  }

  // Auth decision
  if (!isAuthenticated.value) {
    const redirect = encodeURIComponent(to.fullPath)
    return navigateTo(`/login?redirect=${redirect}`, { replace: true })
  }

  // Optional: role gating via route meta
  const need = (to.meta.roles as string[] | undefined) ?? []
  if (need.length && !need.some(r => roles.value.includes(r))) {
    return navigateTo('/forbidden', { replace: true })
  }
})

小等一下……

我只是想欲接後端 session ,轉來判斷前端敢有權限入去特定頁面。

  1. 因為效能問題,上好會使共 session 囥踮前端做簡單的參考。
  2. 因為安全問題,這个 session 有性命週期,時間到需要向後端更新,若猶未到就毋免重 request。
  3. 無想欲傷複雜,因為家己經驗無應該傷複雜。

結果哪遮複雜……

害我開始慒心是毋是家己傷菜,致使減考慮傷濟物件……

斟酌看過了後,A.I. 開誠濟段落咧處理「時間問題」,毋過可能畢竟毋是家己設計的,伊就是足「A.I.」化的段落。

然後欠缺「工程化」的結構,這實在予我足齷齪!

我感覺最近實在食緊挵破碗,緊炊無好粿,過去一直掛意的 software engineering, design pattern, system design, 最近一直攏無做到,就因為家己感覺咧走 SCRUM 結果一直感覺無需要詳細規畫,這馬顛倒一直脫箠,毋是用著錯誤的套件,就是任務、結構分割袂開,透濫結規毬,完全毋知咧創啥。

這禮拜閣是繼續燃燒性命,毋過嘛有機會靜心思考,應該會先冷靜處理一下仔未來一禮拜的規畫。


上一篇
設計比實做重要
下一篇
我佮世界的連結
系列文
對零開始,三十工備份社交網路(台文書寫)10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言