平顶山市文章资讯

前端权限之SSO单点登录权限共享

2026-03-30 17:49:01 浏览次数:0
详细信息

前端在 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[前端渲染有权限的界面];

前端的关键动作:

2. 权限共享的实现

认证完成后,多个前端应用(app1.example.comapp2.example.com)如何共享用户的登录状态和权限信息?

机制 描述 前端注意事项
1. Token 共享 多个应用从同一个 SSO 域获取 Token。 关键:Token 存储位置。
同域应用:可共享 localStoragecookie(设置相同的一级域,如 .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>

三、跨域/微前端场景下的权限共享

父子应用(同域):最简单,可共享 localStoragecookie完全独立跨域应用 微前端架构(如 qiankun、single-spa)

四、安全最佳实践

使用 HTTPS:全程 HTTPS,防止 Token 被窃取。 安全的 Token 存储 正确的重定向与状态参数:在 OAuth 流程中使用 state 参数防止 CSRF 攻击。 限制 Token 有效期:使用短期的 Access Token(如15分钟)和长期的 Refresh Token,并定期刷新。 后端最终校验:前端权限控制仅为用户体验优化,所有 API 的权限校验必须在后端严格执行

总结

前端在 SSO 权限共享中的核心工作是:

引导:将用户引导至 SSO 进行认证。 交换与存储:安全地处理 OAuth/OIDC 流程,获取并存储 Token。 传递:在每次 API 请求中正确携带 Token。 解析与展示:根据 Token 或用户信息端点返回的数据,解析用户角色/权限,并据此控制 UI 的展示。 管理生命周期:处理 Token 的刷新、过期和统一登出。

通过这套组合拳,用户只需在任意一个接入 SSO 的应用中登录一次,即可在所有互信的应用间无缝切换,并看到符合其权限的个性化界面。

相关推荐