基于Node.js的SSO技术的实现

使用客户端服务器体系结构(使用HTTP作为通信协议)创建Web应用程序。 HTTP是无状态协议。每次浏览器向服务器发送请求时,服务器都会独立于其他请求处理该请求,并且不会将其与来自同一浏览器的先前或后续请求关联。这意味着,除其他外,这意味着任何人都可以访问不受任何方式保护的服务器资源。如果需要保护某些服务器资源不受外界的侵扰,则意味着您需要以某种方式限制浏览器可以从服务器请求的内容。也就是说,您需要对请求进行身份验证,并且仅对通过测试的请求做出响应,而忽略未通过测试的请求。要对请求进行身份验证,您需要了解有关请求的一些信息,存储在浏览器端。由于HTTP协议不存储请求状态,因此我们需要一些其他机制来允许服务器和浏览器共同管理连接状态。这些机制包括使用cookie,会话,JWT。







如果我们在谈论单个Web项目,则使用客户端登录时的用户身份验证可以轻松维护有关客户端和服务器之间特定交互会话状态的信息。但是,如果这样一个独立的系统发展成为几个系统,那么开发人员将面临维护有关每个独立系统状态信息的问题。在实践中,这个问题看起来像这样:“这些系统的用户是否必须分别输入每个系统并退出系统?”



有一个很好的经验法则,那就是随着时间的流逝,系统变得越来越复杂,这些系统如何与用户交互。即,解决与项目架构的复杂性相关的问题的负担落在系统上,而不是系统用户身上。 Web项目的内部机制有多复杂都没有关系。对用户来说,它应该看起来像一个统一的系统。换句话说,使用由许多组件组成的Web系统的用户应该感知正在发生的事情,就像他正在使用一个系统一样。特别是,我们正在谈论使用SSO(单点登录)(一种单点登录技术)的此类系统中的身份验证。



如何创建使用SSO的系统?您可能在这里想到了基于Cookie的旧解决方案,但是该解决方案受到限制。限制适用于从中设置Cookie的域。只能通过在一个顶级域上收集Web应用程序所有子系统的所有域名来规避此问题。



在当今的环境中,微服务架构的广泛采用阻碍了此类解决方案的发展。当在开发Web项目中使用不同的技术,并且有时在不同的域上托管不同的服务时,会话管理变得更加复杂。另外,曾经用Java编写的Web服务开始使用Node.js平台的功能来编写。这使得使用cookie变得更加困难。事实证明,会话现在不那么容易管理。



这些困难导致开发了登录系统的新方法,尤其是我们正在谈论的单点登录技术。



单点登录技术



单点登录技术所基于的基本原理是,用户可以登录到一个由多个系统组成的项目的一个系统中,并在所有其他系统中得到授权,而无需再次登录。同时,我们正在谈论从所有系统集中退出。



出于教育目的,我们将在Node.js平台上实施SSO技术。



应当指出的是,在企业范围内实施该技术将需要比投入培训系统开发更多的精力。这就是为什么有专门为大型项目设计的SSO解决方案的原因。



SSO登录如何组织?



SSO实现的核心是一个独立的身份验证服务器,该服务器可以接受信息以对用户进行身份验证。例如-电子邮件地址,用户名,密码。其他系统没有为用户提供直接的登录机制。他们通过从身份验证服务器接收有关用户的信息来间接授权用户。间接授权机制是使用令牌实现的。



这是simple-sso项目的代码存储库,我将在这里描述其实现。我正在使用Node.js框架,但是可以使用不同的东西来实现相同的东西。让我们逐步分析使用该系统的用户的操作以及构成该系统的机制。



第1步



用户正在尝试访问系统上的受保护资源(我们将此资源称为“ SSO使用者”,“ sso消费者”)。SSO使用者发现用户尚未登录,并使用其自己的地址作为查询参数将用户重定向到“ SSO服务器”(“ sso服务器”)。成功通过身份验证的用户将被重定向到该地址。此机制由Express中间件提供:



const isAuthenticated = (req, res, next) => {
  //   ,   ,
  //     -     SSO-     
  //    URL  URL,     
  // ,   
  const redirectURL = `${req.protocol}://${req.headers.host}${req.path}`;
  if (req.session.user == null) {
    return res.redirect(
      `http://sso.ankuranand.com:3010/simplesso/login?serviceURL=${redirectURL}`
    );
  }
  next();
};

module.exports = isAuthenticated;


第2步



SSO服务器发现用户尚未登录,并将其重定向到登录页面:



const login = (req, res, next) => {
  //  req.query  url,      
  //    ,     sso-.
  //        
  //     
  const { serviceURL } = req.query;
  //         URL.
  if (serviceURL != null) {
    const url = new URL(serviceURL);
    if (alloweOrigin[url.origin] !== true) {
      return res
        .status(400)
        .json({ message: "Your are not allowed to access the sso-server" });
    }
  }
  if (req.session.user != null && serviceURL == null) {
    return res.redirect("/");
  }
  //          -  
  //   
  if (req.session.user != null && serviceURL != null) {
    const url = new URL(serviceURL);
    const intrmid = encodedId();
    storeApplicationInCache(url.origin, req.session.user, intrmid);
    return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
  }

  return res.render("login", {
    title: "SSO-Server | Login"
  });
};


我将在此处针对安全性发表一些评论。



我们检查serviceURL,以请求参数的形式发送到SSO服务器。这使我们能够确定此URL是否已在系统中注册,以及它表示的服务是否可以使用SSO服务器的服务。



允许使用SSO服务器的服务的URL列表如下所示:



const alloweOrigin = {
"http://consumer.ankuranand.in:3020": true,
"http://consumertwo.ankuranand.in:3030": true,
"http://test.tangledvibes.com:3080": true,
"http://blog.tangledvibes.com:3080": fasle,
};


第三步



用户在登录请求中输入发送到SSO服务器的用户名和密码。





登录页面



第4步



SSO身份验证服务器验证用户的信息,并在其自身和用户之间创建会话。这就是所谓的“全局会话”。授权令牌将立即创建。令牌是一串随机字符。此字符串的确切生成方式无关紧要。最主要的是,相似的行不会为不同的用户重复,并且这样的行将很难伪造。



第5步



SSO服务器获取授权令牌,并将其传递给新登录用户的来源(即,它将令牌传递给SSO使用者)。



const doLogin = (req, res, next) => {
  //         .
  //         , 
  // userDB -   ,   ,   
  const { email, password } = req.body;
  if (!(userDB[email] && password === userDB[email].password)) {
    return res.status(404).json({ message: "Invalid email and password" });
  }

  //     
  const { serviceURL } = req.query;
  const id = encodedId();
  req.session.user = id;
  sessionUser[id] = email;
  if (serviceURL == null) {
    return res.redirect("/");
  }
  const url = new URL(serviceURL);
  const intrmid = encodedId();
  storeApplicationInCache(url.origin, id, intrmid);
  return res.redirect(`${serviceURL}?ssoToken=${intrmid}`);
};


同样,一些安全说明:



  • 此令牌应始终被视为一种中间机制,它用于获取另一个令牌。
  • 如果您将JWT用作中间令牌,请尝试不要在其中包含秘密。


第6步



SSO使用者接收令牌,并与SSO服务器联系以验证令牌。服务器检查令牌,并返回另一个包含用户信息的令牌。SSO使用者使用此令牌与用户建立会话。该会话称为本地会话。



这是基于Express的SSO使用者中使用的中间件代码:



const ssoRedirect = () => {
  return async function(req, res, next) {
    // ,    req queryParameter,  ssoToken,
    //  ,    .
    const { ssoToken } = req.query;
    if (ssoToken != null) {
      //   ssoToken   ,  .
      const redirectURL = url.parse(req.url).pathname;
      try {
        const response = await axios.get(
          `${ssoServerJWTURL}?ssoToken=${ssoToken}`,
          {
            headers: {
              Authorization: "Bearer l1Q7zkOL59cRqWBkQ12ZiGVW2DBL"
            }
          }
        );
        const { token } = response.data;
        const decoded = await verifyJwtToken(token);
        //      jwt,  
        // global-session-id  id ,  
        //         .
        req.session.user = decoded;
      } catch (err) {
        return next(err);
      }

      return res.redirect(`${redirectURL}`);
    }

    return next();
  };
};


在收到来自SSO使用者的请求之后,服务器将检查令牌的存在和到期日期。经验证的令牌被视为有效。



在我们的示例中,SSO服务器在成功验证令牌后,将返回带有用户信息的签名JWT。



const verifySsoToken = async (req, res, next) => {
  const appToken = appTokenFromRequest(req);
  const { ssoToken } = req.query;
  //        ssoToken .
  //  ssoToken    - ,   .
  if (
    appToken == null ||
    ssoToken == null ||
    intrmTokenCache[ssoToken] == null
  ) {
    return res.status(400).json({ message: "badRequest" });
  }

  //  appToken  -     
  const appName = intrmTokenCache[ssoToken][1];
  const globalSessionToken = intrmTokenCache[ssoToken][0];
  //  appToken   ,   SSO-        
  if (
    appToken !== appTokenDB[appName] ||
    sessionApp[globalSessionToken][appName] !== true
  ) {
    return res.status(403).json({ message: "Unauthorized" });
  }
  // ,     
  const payload = generatePayload(ssoToken);

  const token = await genJwtToken(payload);
  //    ,     
  delete intrmTokenCache[ssoToken];
  return res.status(200).json({ token });
};


这里有一些安全注意事项。



  • 将使用此服务器进行身份验证的所有应用程序都必须在SSO服务器中注册。需要为它们分配代码,以便在它们向服务器发出请求时用于对其进行验证。在SSO服务器和SSO使用者之间进行通信时,这可以提供更高的安全性。
  • 可以为每个应用程序生成不同的“私有”和“公共” rsa文件,并让每个应用程序使用各自的公共密钥在内部验证其JWT。


此外,您可以定义应用程序级安全策略并组织其集中式存储:



const userDB = {
  "info@ankuranand.com": {
    password: "test",
    userId: encodedId(), //   ,         .
    appPolicy: {
      sso_consumer: { role: "admin", shareEmail: true },
      simple_sso_consumer: { role: "user", shareEmail: false }
    }
  }
};


用户成功登录系统后,将在他与SSO服务器之间以及他与每个子系统之间创建会话。用户与SSO服务器之间建立的会话称为全局会话。用户与为用户提供某些服务的子系统之间建立的会话称为本地会话。建立本地会话后,用户将能够使用对外部资源封闭的子系统资源。





设置本地和全球会话



SSO使用者和SSO服务器快速浏览



让我们快速浏览一下SSO使用者和SSO服务器功能。



SO SSO消费者



  1. SSO使用者子系统不会通过将用户重定向到SSO服务器来认证用户。
  2. 该子系统接收SSO服务器传递给它的令牌。
  3. 它与服务器交互以验证令牌的有效性。
  4. 她收到JWT并使用公钥验证此令牌。
  5. 该子系统建立本地会话。


SOSSO服务器



  1. SSO服务器验证用户登录信息。
  2. 服务器创建一个全局会话。
  3. 它创建一个授权令牌。
  4. 授权令牌发送到SSO使用者。
  5. 服务器验证SSO使用者传递给它的令牌的有效性。
  6. 服务器将SSO JWT与用户信息一起发送给使用者。


组织集中注销



与实施SSO的方式类似,您可以实施SSO技术。在这里,您只需要考虑以下注意事项:



  1. 如果存在本地会话,则也必须存在全局会话。
  2. 如果存在全局会话,则不一定意味着存在本地会话。
  3. 如果本地会话被破坏,则全局会话也必须被破坏。


结果



结果,可以注意到,有许多现成的单点登录技术实现,可以将它们集成到系统中。它们都有自己的优点和缺点。从头开始独立开发这样的系统是一个迭代过程,在此过程中,您需要分析每个系统的特性。这包括登录方法,用户信息存储,数据同步等。



您的项目是否使用SSO机制?






All Articles