前端在 SSO 单点登录及权限共享 中的角色、流程和关键实现点。
首先要明确一个核心理念:SSO 主要解决的是身份认证(Authentication)问题,即“你是谁”。而 权限共享(Authorization) 是在此基础上,解决“你能干什么”的问题。前端是实现这两者无缝衔接给用户的关键环节。
一、核心概念与流程
1. SSO 单点登录流程(以最经典的 OAuth 2.0 / OIDC 授权码模式为例)
这个流程涉及三方:前端应用(Client)、认证中心(SSO Server / Identity Provider)、后端服务(Resource Server)。
graph TD
A[用户访问前端应用] --> B{前端检查本地Token};
B -->|Token无效/不存在| C[重定向到SSO认证中心];
C --> D{用户在SSO登录?};
D -->|未登录| E[SSO登录页面];
E --> F[用户输入凭证];
F --> G[验证成功, SSO生成授权码];
G --> H[重定向回前端并携带授权码];
D -->|已登录| H;
H --> I[前端用授权码向SSO后台交换Token];
I --> J[SSO返回 Access_Token, Id_Token, Refresh_Token];
J --> K[前端存储Token];
K --> L[前端携带Token请求后端API];
L --> M[后端验证Token, 返回受保护数据/资源];
M --> N[前端渲染有权限的界面];
前端的关键动作:
- 检查登录状态:检查本地(如 localStorage、cookie、内存)是否存在有效的 Token。
- 发起认证:若无有效 Token,构造参数并重定向到 SSO 认证中心。
- 处理回调:在重定向回来的回调页面,从 URL 中提取授权码。
- 交换令牌:在后台(安全地)用授权码向 SSO 的 Token 端点请求令牌。切勿在前端公开 Client Secret。
- 安全存储:将获得的 Access Token、Refresh Token 安全存储。
- 携带令牌:在后续请求 API 时,将 Token 放入 HTTP 请求头(通常是
Authorization: Bearer <access_token>)。
- 处理过期:监听 401 状态码,使用 Refresh Token 静默刷新或引导用户重新登录。
2. 权限共享的实现
认证完成后,多个前端应用(app1.example.com, app2.example.com)如何共享用户的登录状态和权限信息?
| 机制 |
描述 |
前端注意事项 |
|---|
| 1. Token 共享 |
多个应用从同一个 SSO 域获取 Token。 |
关键:Token 存储位置。 • 同域应用:可共享 localStorage 或 cookie(设置相同的一级域,如 .example.com)。 • 跨域应用:无法直接共享存储。需每个应用独立向 SSO 发起认证流程。SSO 因会话已存在会跳过登录,直接返回 Token。这是主要的“共享”方式——共享的是认证状态,而非物理 Token。 |
| 2. 用户/权限信息获取 |
应用从 Token 或用户信息端点获取权限数据。 |
• Access Token 负载 (JWT): 可以将角色、权限等声明(Claims)直接编码在 Token 中。前端解析 JWT 即可获取(注意不要放敏感信息)。 • 调用用户信息端点 (/userinfo): 使用 Access Token 请求 SSO 的用户信息接口,获取完整的用户资料和权限列表。 |
| 3. 前端路由与控权 |
根据权限动态控制菜单、路由、按钮。 |
• 路由守卫: 在进入页面前,校验用户权限(对比路由元信息 meta.roles 与用户角色)。 • 组件级权限: 封装权限指令或高阶组件,如 v-permission="'user:add'" 或 <AuthButton code="delete" />。 • API 级权限: 最终由后端接口验证 Token 和权限,前端需优雅处理 403 错误。 |
二、前端关键实现细节与代码示例
1. Token 管理与存储策略
// auth.js - 认证管理类
class AuthService {
// 存储 Token(考虑同域共享,可优先使用 localStorage)
static setTokens(accessToken, refreshToken) {
localStorage.setItem('access_token', accessToken);
localStorage.setItem('refresh_token', refreshToken);
// 如果是跨域,且主应用需要通知子应用,可考虑使用 window.postMessage
}
// 获取 Token,用于 API 请求
static getAccessToken() {
return localStorage.getItem('access_token');
}
// 检查是否登录(简单检查有无Token,更完善需检查过期)
static isLoggedIn() {
const token = this.getAccessToken();
return !!token && !this.isTokenExpired(token); // 需要实现 isTokenExpired
}
// 静默刷新 Token
static async refreshAccessToken() {
const refreshToken = localStorage.getItem('refresh_token');
try {
const response = await fetch(`${SSO_SERVER}/oauth/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: CLIENT_ID,
}),
});
const data = await response.json();
this.setTokens(data.access_token, data.refresh_token);
return data.access_token;
} catch (error) {
// 刷新失败,登出
this.logout();
throw error;
}
}
// 登出
static logout() {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
// 重定向到 SSO 的统一登出端点,清除 SSO 会话
window.location.href = `${SSO_SERVER}/logout?redirect_uri=${FRONTEND_URL}`;
}
}
// 在请求拦截器中统一处理 Token
axios.interceptors.request.use(config => {
const token = AuthService.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
const newToken = await AuthService.refreshAccessToken();
originalRequest.headers.Authorization = `Bearer ${newToken}`;
return axios(originalRequest);
} catch (refreshError) {
AuthService.logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
}
);
2. 权限控制示例(以 Vue.js 为例)
// 1. 路由守卫中检查权限
router.beforeEach(async (to, from, next) => {
// 检查是否需要认证
if (to.meta.requiresAuth) {
if (AuthService.isLoggedIn()) {
// 已登录,检查是否有访问该页面的角色
const userRoles = getUserRolesFromToken(); // 从JWT解析或store获取
const requiredRoles = to.meta.roles; // 路由元信息中定义的所需角色
if (requiredRoles && requiredRoles.length > 0) {
const hasRole = requiredRoles.some(role => userRoles.includes(role));
hasRole ? next() : next('/403'); // 无权限,跳转到403页面
} else {
next(); // 无需特定角色
}
} else {
// 未登录,重定向到SSO登录页,带上当前路由作为重定向参数
const redirectUrl = encodeURIComponent(window.location.origin + to.fullPath);
window.location.href = `${SSO_SERVER}/login?redirect_uri=${redirectUrl}`;
}
} else {
next(); // 公共页面
}
});
// 2. 自定义权限指令(按钮级控制)
const permissionDirective = {
mounted(el, binding) {
const { value } = binding; // 期望的权限码,如 'user:delete'
const userPermissions = store.getters.permissions; // 从全局状态获取权限列表
if (value && value instanceof Array && value.length > 0) {
const hasPermission = userPermissions.some(perm => value.includes(perm));
if (!hasPermission) {
el.parentNode && el.parentNode.removeChild(el); // 无权限则移除元素
}
} else {
throw new Error(`需要指定权限码,如 v-permission="['user:delete']"`);
}
}
};
// Vue 应用中使用
app.directive('permission', permissionDirective);
// 模板中使用:<button v-permission="['user:delete']">删除</button>
三、跨域/微前端场景下的权限共享
父子应用(同域):最简单,可共享
localStorage 或
cookie。
完全独立跨域应用:
- 每个应用独立完成 SSO 流程。
- SSO 服务器通过 共享会话 Cookie(通常设置在 SSO 域下) 来确保用户只需登录一次。
- 用户信息(角色、权限)由每个应用分别通过 Token 向 SSO 或各自的后端获取。
微前端架构(如 qiankun、single-spa):
- 主应用统一认证:主应用负责完成 SSO 登录,获取 Token。
- Token 下发:通过全局状态、自定义事件或 props 将 Token 传递给子应用。
- 子应用独立验证:子应用接收 Token 后,仍需用它来请求自己的后端 API,后端负责验证 Token 有效性。
- 权限模型统一:最好由主应用或后端统一提供权限数据,子应用消费。
四、安全最佳实践
使用 HTTPS:全程 HTTPS,防止 Token 被窃取。
安全的 Token 存储:
- 避免在
localStorage 中存储非常敏感的信息(易受 XSS 攻击)。
- 考虑使用
httpOnly、Secure、SameSite=Strict/Lax 的 Cookie 来存储 Refresh Token(防 XSS,但需处理 CSRF)。
- Access Token 可存于内存或
localStorage,但需有完善的 XSS 防御和自动刷新机制。
正确的重定向与状态参数:在 OAuth 流程中使用
state 参数防止 CSRF 攻击。
限制 Token 有效期:使用短期的 Access Token(如15分钟)和长期的 Refresh Token,并定期刷新。
后端最终校验:前端权限控制仅为用户体验优化,
所有 API 的权限校验必须在后端严格执行。
总结
前端在 SSO 权限共享中的核心工作是:
引导:将用户引导至 SSO 进行认证。
交换与存储:安全地处理 OAuth/OIDC 流程,获取并存储 Token。
传递:在每次 API 请求中正确携带 Token。
解析与展示:根据 Token 或用户信息端点返回的数据,解析用户角色/权限,并据此控制 UI 的展示。
管理生命周期:处理 Token 的刷新、过期和统一登出。
通过这套组合拳,用户只需在任意一个接入 SSO 的应用中登录一次,即可在所有互信的应用间无缝切换,并看到符合其权限的个性化界面。