登录
前言
Uniboot 模板项目提供了两套针对不同部署场景设计的登录鉴权机制:Simple 模板(本地表单自主登录模式) 与 Auth 模板(Java 后端统一授权服务中心模式)。两套方案在控制层、页面结构和 Token 换取逻辑上存在显著差异。
登录模块系统设计
系统根据是否引入统一认证中心(SSO / OAuth2.0),划分了不同的认证流:
- Simple 模板:适用于独立的单体或微服务应用。系统前端提供本地登录页面
src/views/account/login.vue,在当前应用内搜集用户名和密码,并直接发起登录请求获取 Token。 - Auth 模板:适用于多系统集成或需要统一单点登录(SSO)的应用。前端不包含/不展示本地登录页面,完全依赖 Java 后端“授权服务中心”提供的登录页进行身份认证。
Simple 模板登录流 (本地登录模式)
Simple 模板项目使用标准的 SPA 本地表单登录方案,全套逻辑均在前端应用内流转。
1. 登录组件与基础属性
Simple 模板使用组件库封装好的 LoginPage 组件构建登录表单,挂载在 src/views/account/login.vue:
vue
<template>
<login-page
:logo="config.web_logo"
:illustration="config.login_image"
:title="$t('userLogin')"
:submit-text="$t('login')"
:loading="isLock"
@finish="lockLogin"
>
<!-- 其他插槽 -->
</login-page>
</template>2. 请求控制与防重复提交
登录操作在脚本中自主处理:
- 通过
useLockFn包装真实登录请求函数handleLogin,防止网络延迟时用户重复点击。 - 在
handleLogin内,显式调用 Pinia store 中的userStore.login行为以发送 API 请求。 - 登录成功获取令牌后,根据
route.query.redirect跳转回拦截前的页面或重定向至系统首页。
Auth 模板登录流 (统一认证授权模式)
Auth 模板则采用了一种经典且安全的 OAuth 2.0 授权码(Authorization Code)模式。当用户访问受保护的页面且前端没有检测到 Token 时,鉴权守卫 src/permission.ts 将实施拦截并引导至后端授权页,具体步骤如下:
- 访问受限页面:用户尝试访问系统内部受限路由(如
/dashboard)。 - 路由前置校验:路由守卫
beforeEach拦截本次跳转,检测到本地不存在accessToken(未登录)且目标并非免登录白名单。 - 记忆返回地址:守卫调用
rememberOAuthReturnPath(to.fullPath)提取当前的完整 URL,并安全写入sessionStorage,以便授权成功后跳回。 - 发起网关重定向:守卫调用
redirectToAuthServiceLogin()并执行next(false)中断本路由跳转。浏览器直接执行window.location.href = buildOAuthAuthorizeHref(...)将页面强行引导重定向至 Java 统一授权服务中心。 - 展示登录表单:授权中心接收请求,展示 Java 后端统一提供的多应用登录验证页面。
在守卫中的具体实现逻辑为:
ts
// 当检测到无 token 且未命中免登录白名单时
if (config.useAuthServiceLogin && config.authServiceUrl?.trim()) {
// 1. 在 sessionStorage 中记录当前尝试访问的地址,用于登录成功后跳回
rememberOAuthReturnPath(to.fullPath)
NProgress.done()
// 2. 更改 window.location.href 强行重定向跳转至后端统一授权服务中心
redirectToAuthServiceLogin()
// 3. 中断当前的单页路由跳转
next(false)
return
}用户在 Java 后端的统一授权登录页面完成账号验证后,授权服务中心会携带授权码 code 和防伪验证标识 state 重定向回调至前端站点,具体换票步骤如下:
- 授权完成回调:用户在 Java 授权中心成功验证身份后,授权中心携带随机生成的授权码
code和防伪校验状态state重定向回调到前端配置的 Callback 地址(例如:http://localhost:3000/?code=xxx&state=yyy)。 - 捕获授权状态:前端路由守卫
beforeEach拦截到页面 URL 中的code查询参数,且此时检测到!userStore.token。 - 状态安全性防伪校验:守卫提取
state参数,并与 sessionStorage 中期望的校验值进行校对,确认无误以防御 CSRF 跨站请求伪造攻击。 - 调用接口换取令牌:守卫调用异步请求函数
exchangeAuthorizationCode(oauthCode)。前端向后端的 Token 终结点发送授权码,后端解密校验后返回accessToken访问令牌。 - 写入状态并原路跳回:前端将获取到的
accessToken手动保存至 Pinia Store 的userStore.token并写入本地缓存。随后调用takeAndClearOAuthReturnPath()读取先前记忆的受限制页面地址,重新导航到最初请求的路由位置,用户无缝进入系统。
在 src/permission.ts 中,这一阶段的集中换票逻辑在守卫的顶部实施截获:
ts
const oauthCode = typeof to.query.code === 'string' ? to.query.code : ''
// 当无 Token 但路由 query 中携带有 code 参数时
if (!userStore.token && config.useAuthServiceLogin && oauthCode) {
// 1. 防伪状态校验,防御 CSRF 跨站请求伪造攻击
const stateParam = to.query.state
const expected = sessionStorage.getItem(OAUTH_LOGIN_STATE_KEY)
if (typeof stateParam !== 'string' || stateParam !== expected) {
feedback.msgError('授权状态校验失败,请重试')
next({ path: loginPath, replace: true })
return
}
try {
// 2. 调用 API 将 code 异步换取系统的访问令牌 accessToken
const accessToken = await exchangeAuthorizationCode(oauthCode)
clearOAuthLoginSessionMarkers()
// 3. 手动保存 Token 进 Pinia 与持久化 LocalStorage 缓存
userStore.token = accessToken
appStorage.setItem(TOKEN_KEY, accessToken)
// 4. 读取此前缓存的返回路径并跳回
const dest = takeAndClearOAuthReturnPath(PageEnum.INDEX)
NProgress.done()
next({ path: dest, replace: true })
} catch (e) {
clearOAuthLoginSessionMarkers()
next({ path: loginPath, replace: true })
}
return
}通过这种守卫前置拦截、集中式换票的设计,整个授权与跳转过程在几百毫秒内即可无缝完成,而用户甚至不会感知到前端没有“登录页”这一事实,最大化地利用了单点登录的统一优势。