欢迎光临殡葬网
详情描述

前端在 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.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完全独立跨域应用
  • 每个应用独立完成 SSO 流程。
  • SSO 服务器通过 共享会话 Cookie(通常设置在 SSO 域下) 来确保用户只需登录一次。
  • 用户信息(角色、权限)由每个应用分别通过 Token 向 SSO 或各自的后端获取。
微前端架构(如 qiankun、single-spa)
  • 主应用统一认证:主应用负责完成 SSO 登录,获取 Token。
  • Token 下发:通过全局状态、自定义事件或 props 将 Token 传递给子应用。
  • 子应用独立验证:子应用接收 Token 后,仍需用它来请求自己的后端 API,后端负责验证 Token 有效性。
  • 权限模型统一:最好由主应用或后端统一提供权限数据,子应用消费。

四、安全最佳实践

使用 HTTPS:全程 HTTPS,防止 Token 被窃取。 安全的 Token 存储
  • 避免在 localStorage 中存储非常敏感的信息(易受 XSS 攻击)。
  • 考虑使用 httpOnlySecureSameSite=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 的应用中登录一次,即可在所有互信的应用间无缝切换,并看到符合其权限的个性化界面。

相关帖子
草原旅游日益火热,如何在发展旅游业的同时有效保护脆弱的草原环境?
草原旅游日益火热,如何在发展旅游业的同时有效保护脆弱的草原环境?
平顶山市殡葬服务一站式办理|丧事服务一条龙,丧葬拍摄
平顶山市殡葬服务一站式办理|丧事服务一条龙,丧葬拍摄
在符合年龄要求后,何时提出高龄津贴申请最为合适是否有时间限制?
在符合年龄要求后,何时提出高龄津贴申请最为合适是否有时间限制?
初中最后一年,如何科学制定一份有效的长期备考复习规划?
初中最后一年,如何科学制定一份有效的长期备考复习规划?
变更工作或住址后如何进行居住证信息的及时变更登记?
变更工作或住址后如何进行居住证信息的及时变更登记?
在食用当季蔬果时,有哪些有效的家庭处理方法可以减少农残疑虑?
在食用当季蔬果时,有哪些有效的家庭处理方法可以减少农残疑虑?
因手机信号或系统问题导致无法按时核销,会被误判为爽约吗?
因手机信号或系统问题导致无法按时核销,会被误判为爽约吗?
2026年中考综合素质评价越来越重要,小学阶段该怎么帮孩子攒素材?
2026年中考综合素质评价越来越重要,小学阶段该怎么帮孩子攒素材?
当有意外收入时,将其转为储蓄而非消费的心理技巧有哪些?
当有意外收入时,将其转为储蓄而非消费的心理技巧有哪些?
对于刚参加工作不久的年轻人,租房提取住房公积金有哪些便利条件与渠道?
对于刚参加工作不久的年轻人,租房提取住房公积金有哪些便利条件与渠道?
近年有哪些新兴的行业或赛道获得了更多的创业政策倾斜?
近年有哪些新兴的行业或赛道获得了更多的创业政策倾斜?
过桥垫资费用如何核算?结合计费规则看懂单日收费与总成本计算
过桥垫资费用如何核算?结合计费规则看懂单日收费与总成本计算
房产抵押贷款相关法规解读,学法懂法保障抵押借贷行为合法合规
房产抵押贷款相关法规解读,学法懂法保障抵押借贷行为合法合规
台风来临前窗外突然变得安静,真的是风暴已经过去了吗?
台风来临前窗外突然变得安静,真的是风暴已经过去了吗?
押车贷款办理流程详解,签订借据时借款用途怎么填写?
押车贷款办理流程详解,签订借据时借款用途怎么填写?
大暑三候
大暑三候"腐草为萤、土润溽暑、大雨时行"该怎么理解?
解读相关法律法规对“最终解释权归本店所有”这类单方声明的具体规定
解读相关法律法规对“最终解释权归本店所有”这类单方声明的具体规定
2026年有没有更智能的个人收款码账单管理与分析工具?
2026年有没有更智能的个人收款码账单管理与分析工具?
房产证红本抵押贷款办理流程全解析,从申请到放款只需七步
房产证红本抵押贷款办理流程全解析,从申请到放款只需七步
头伏饺子二伏面三伏烙饼摊鸡蛋,这套吃法背后是哪里的民俗习惯?
头伏饺子二伏面三伏烙饼摊鸡蛋,这套吃法背后是哪里的民俗习惯?