WHAT - 用户登录系列(二)- 单点登录 SSO

目录

  • 一、认证机制
    • 1.1 基于会话的认证(Session-based Authentication)
      • 1. 介绍
      • 2. 基本流程
    • 1.2 JSON Web Tokens (JWT)
      • 1. 介绍
      • 2. jwt 组成
      • 3. 基本流程
      • 4. 阻止列表
      • 5. 刷新令牌
  • 二、单点登录:SSO
    • 2.1 单系统登录
    • 2.2 SSO 介绍
    • 2.3 SSO 登录
    • 2.4 SSO 注销
    • 2.5 同域 SSO
    • 2.6 跨域 SSO
      • 1. 介绍
      • 2. 核心原理
      • 3. 基于 OAuth 2.0 的跨域 SSO
      • 4. 示例代码
        • 身份认证服务器(auth.example.com)
        • 应用服务器(app1.com)
        • 应用服务器(app2.com)
        • 后端直接写 Cookie 存在的跨域限制

本文主要详细介绍单点登录。一般来说,一个完整的登录方案,包括:

  1. 认证机制。如基于会话的认证(Session-based Authentication)、JSON Web Tokens (JWT)。
  2. 单点登录。支持跨多个子系统的登录方案。

一、认证机制

1.1 基于会话的认证(Session-based Authentication)

1. 介绍

会话基本上是一种在客户端(通常是浏览器)和服务器之间维护状态的方法。

在会话认证中,用户通过提供凭据(如用户名和密码)来登录,并且在登录成功后,服务器会创建一个会话,并为用户分配一个唯一的会话标识符(通常称为会话ID)。这个会话ID通常存储在客户端的Cookie中,或者通过其他方式(如URL参数)发送到客户端,并在之后的每个请求中被客户端发送回服务器。

2. 基本流程

以下是会话认证的基本流程:

  1. 用户提供凭据: 用户通过提供用户名和密码来登录。这些凭据通常通过一个登录表单或者其他身份验证界面提交给服务器。

  2. 服务器验证凭据: 服务器收到用户提供的凭据后,会验证其有效性。这通常涉及到将用户提供的凭据与服务器中存储的相应凭据进行比对,以确保用户提供的凭据是有效的。

  3. 创建会话: 如果提供的凭据是有效的,服务器会创建一个会话,并为用户分配一个唯一的会话ID。

  4. 将会话ID发送到客户端: 一旦会话被创建,服务器将会话ID发送到客户端。这通常通过在响应中设置一个Cookie,其中包含会话ID,或者通过其他方式(如URL参数)将其发送到客户端。

  5. 客户端发送会话ID: 客户端在之后的每个请求中都会发送会话ID到服务器。这通常通过在请求的Cookie中包含会话ID,或者通过其他方式(如URL参数)将其发送到服务器。

  6. 服务器验证会话ID: 服务器在接收到客户端的请求时,会检查请求中是否包含有效的会话ID。如果会话ID是有效的,服务器将允许请求继续处理;否则,可能需要用户重新进行身份验证。

在该环节,涉及到的具体原理:

  1. 会话ID的存储: 当服务器创建会话时,会为该会话生成一个唯一的会话ID,并将其与用户的身份信息(如用户ID、角色等)关联起来。服务器通常会将这些会话信息存储在内存(适合轻量级应用或短暂会话,因此在服务器重启时即丢失)、数据库(适合长期或持久性的会话)、缓存(适合需要优化访问速度和性能时,常见解决方案包括 Redis、Memcached)或者其他持久化存储中,以便后续验证用户的身份。
  2. 会话ID的发送: 服务器在响应中将会话ID发送到客户端。这通常通过在响应的Cookie中设置会话ID,或者通过其他方式将其发送到客户端。
  3. 请求中的会话ID验证: 当服务器收到客户端的请求时,它会检查请求中是否包含会话ID。这通常通过检查请求中的Cookie,或者其他地方(如URL参数)来获取会话ID。
  4. 会话ID的验证: 服务器将会话ID与存储的会话信息进行比对,以验证会话ID的有效性。服务器会检查会话ID是否存在,并且是否与已存储的会话信息相关联。如果会话ID有效且与相应的会话信息匹配,则服务器将确认用户的身份,并允许请求继续处理。
  5. 无效会话ID的处理: 如果会话ID无效或者与任何会话信息不匹配,服务器可能会要求用户重新进行身份验证,或者以其他方式处理无效会话ID的情况,例如返回错误信息或重定向到登录页面。
  1. 会话管理: 一旦用户完成了会话,服务器可能会终止会话并销毁相关数据,以便释放资源并提高安全性。

会话认证的原理是建立在服务器和客户端之间共享一个标识符(会话ID)的基础上,以便服务器可以识别特定用户的请求。通过这种方式,服务器可以跟踪用户的身份状态,并在必要时进行身份验证。

1.2 JSON Web Tokens (JWT)

1. 介绍

JSON Web Tokens (JWT) 是一种用于在网络应用间安全传递信息的一种简洁的、自包含的方式。JWT可以通过数字签名来验证数据的完整性和来源。JWT通常用于身份验证和信息交换,特别是在分布式环境中。

2. jwt 组成

在JWT中,签名是放在JWT的第三部分,即签名部分。JWT由三部分组成,它们分别是:

  1. Header(头部):包含了关于JWT的元数据信息,通常包括算法(alg)和令牌类型(typ)等。
  2. Payload(负载):包含了JWT的主要信息,例如用户ID、角色、过期时间等。
  3. Signature(签名):用于验证JWT的完整性和真实性的一段字符串,由将Header和Payload使用指定的算法和密钥(始终存储在服务器上)加密后生成的。

在JWT生成过程中,服务器会将Header和Payload进行Base64编码后,使用约定的加密算法(如HMAC、RSA等)和密钥进行签名生成Signature。最终,将这三部分连接起来形成完整的JWT,通常以.分隔,例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

其中,第一部分是Header,第二部分是Payload,第三部分是Signature。

而在后续服务器拿到前端传过来的JWT的验证过程中,服务器会首先对Header和Payload进行解码,然后使用相同的密钥和算法对Header和Payload重新进行签名,并与JWT中的Signature部分进行比对,来验证JWT的完整性和真实性。

另外,JWT可能包含其他的声明,如发行人(issuer)、受众(audience)等。服务器可以根据应用程序的需求来验证这些声明是否符合预期,以进一步确保JWT的有效性。

3. 基本流程

以下是基于JWT进行用户登录的流程和原理:

  1. 用户提供凭据: 用户通过提供凭据(例如用户名和密码)来尝试登录到应用程序。

  2. 服务器验证凭据: 服务器接收到用户提供的凭据后,会验证其有效性。通常情况下,这涉及到与存储在服务器上的用户凭据进行比对。如果凭据有效,则服务器将继续处理登录请求。

  3. 生成JWT: 一旦服务器验证了用户的凭据,它会为用户生成一个JWT。JWT通常包含一些关键信息,如用户ID、角色、过期时间等。这些信息会被编码到JWT的负载(Payload)中。

  4. 签名JWT: 服务器使用密钥对JWT进行签名,以确保JWT的完整性和真实性。签名的过程使用一种加密算法,如HMAC(用于对称密钥)或RSA(用于非对称密钥)。

  5. 将JWT发送给客户端: 服务器在响应中将生成的JWT发送给客户端。这通常是通过将JWT包含在响应的JSON对象中发送回客户端的方式来完成的。

  6. 客户端存储JWT: 客户端收到JWT后,通常会将其存储在本地,例如在浏览器的本地存储(localStorage)或会话存储(sessionStorage)中。

  7. 将JWT发送到服务器: 客户端在后续的请求中,通常会将JWT包含在请求的头部(通常是Authorization头)中发送回服务器。

  8. 验证JWT: 服务器在接收到请求时,会检查JWT的有效性。这包括验证JWT的签名以确保其完整性,即对头部和负载部分使用相同的加密算法与服务器上存储的密钥进行签名,并与JWT中的签名部分进行比对;并检查JWT的有效期以确保其尚未过期,JWT通常包含一个exp字段,代表有效期,服务器基于此来判断是否需要重新进行身份验证。

  9. 提取JWT中的信息: 如果JWT有效,服务器会解码JWT的负载并提取其中的用户信息,以确定用户的身份和权限。这样服务器就可以基于用户的身份来处理请求。

  10. 处理请求: 如果JWT有效且包含了足够的信息来识别和授权用户,服务器会处理请求并响应相应的数据或操作。

  11. 处理过期的JWT: 如果JWT过期或者不再有效,服务器可能会要求客户端重新进行身份验证,或者以其他方式处理无效的JWT。

JWT 的原理在于它是一个自包含的令牌,其中包含了所需的用户信息,并且通过数字签名来确保令牌的完整性和真实性。这使得服务器无需在本地存储会话信息,从而简化了会话管理和状态维护。同时,JWT 的使用也提高了跨域分布式环境下的身份验证和信息交换的安全性和效率。

4. 阻止列表

有时,服务器可能会维护一个阻止列表,用于存储已经失效的JWT或者被标记为不可信的JWT。在验证JWT时,服务器可能会先检查JWT是否在阻止列表中,如果是,则拒绝该JWT。

5. 刷新令牌

在某些情况下,服务器可能会使用JWT中的某些信息来生成新的JWT,以实现令牌的刷新。它允许客户端在访问令牌过期或失效时获取新的令牌,而无需重新进行身份验证。

这可以用于延长用户的会话期限或者更新令牌中的其他信息。

这个过程可能会涉及到一些特定的流程,例如检查刷新令牌的有效性等。具体来说,刷新令牌的流程如下:

  1. 获取刷新令牌: 在用户登录或者进行身份验证成功后,服务器会生成一对访问令牌(Access Token)和刷新令牌(Refresh Token),并将它们发送给客户端。刷新令牌通常包含在JWT中的Payload中,可以携带一些额外的信息,如用户ID等。

  2. 使用访问令牌访问资源: 客户端使用访问令牌来访问受保护的资源,例如API端点。服务器会验证访问令牌的有效性,并根据其包含的信息来确定用户的身份和权限。

  3. 访问令牌过期或失效: 当访问令牌过期或失效时,客户端可能会收到一个响应,指示访问被拒绝或者令牌过期。此时客户端可以使用刷新令牌来获取新的访问令牌,而无需重新进行用户身份验证。

  4. 使用刷新令牌获取新的访问令牌: 客户端使用刷新令牌发送请求到服务器,请求服务器颁发新的访问令牌。服务器接收到请求后,会验证刷新令牌的有效性,并根据需要对客户端进行身份验证。

  5. 颁发新的访问令牌: 如果刷新令牌有效且合法,服务器会生成一个新的访问令牌,并将其发送给客户端。客户端可以使用这个新的访问令牌来继续访问受保护的资源。

  6. 重复使用新的访问令牌: 客户端可以继续使用新颁发的访问令牌来访问资源,直到新的访问令牌过期或失效,或者直到需要再次刷新令牌。

刷新令牌的机制允许客户端在访问令牌过期或失效时获取新的访问令牌,而无需用户重新进行身份验证,从而提高了用户体验和安全性。

当使用JWT时,刷新令牌(Refresh Token)通常是在用户登录成功后一并生成并发送给客户端的。下面是一个简单的示例代码,演示了如何生成和使用JWT以及刷新令牌:

const jwt = require('jsonwebtoken');// 服务器端的密钥,用于签名和验证JWT
const SECRET_KEY = 'my_secret_key';// 用户登录成功后,生成JWT和刷新令牌
function generateTokens(user_id) {// 生成访问令牌(Access Token)const access_token_payload = {user_id: user_id,exp: Math.floor(Date.now() / 1000) + (15 * 60) // 设置过期时间为15分钟};const access_token = jwt.sign(access_token_payload, SECRET_KEY);// 生成刷新令牌(Refresh Token)const refresh_token_payload = {user_id: user_id};const refresh_token = jwt.sign(refresh_token_payload, SECRET_KEY);return { access_token, refresh_token };
}// 客户端在使用访问令牌访问资源时,验证令牌的有效性
function verifyAccessToken(token) {try {const payload = jwt.verify(token, SECRET_KEY);return payload;} catch (err) {if (err.name === 'TokenExpiredError') {console.log('Access Token 已过期');} else {console.log('无效的 Access Token');}return null;}
}// 当访问令牌过期时,客户端使用刷新令牌获取新的访问令牌
function refreshAccessToken(refresh_token) {try {const payload = jwt.verify(refresh_token, SECRET_KEY);const user_id = payload.user_id;// 在这里进行一些额外的验证操作,如验证用户是否存在等const new_access_token = jwt.sign({ user_id }, SECRET_KEY);return new_access_token;} catch (err) {if (err.name === 'TokenExpiredError') {console.log('Refresh Token 已过期');} else {console.log('无效的 Refresh Token');}return null;}
}// 示例演示
// 用户登录成功后生成JWT和刷新令牌
const { access_token, refresh_token } = generateTokens('123456');// 客户端使用访问令牌访问资源
// 在请求中发送 access_token 给服务器
console.log('访问受保护的资源...');
const access_token_payload = verifyAccessToken(access_token);
if (access_token_payload) {console.log('访问令牌有效,用户ID:', access_token_payload.user_id);
} else {console.log('无效的访问令牌');
}// 当访问令牌过期时,使用刷新令牌获取新的访问令牌
const new_access_token = refreshAccessToken(refresh_token);
if (new_access_token) {console.log('获取新的访问令牌成功:', new_access_token);
} else {console.log('无法获取新的访问令牌');
}

最后需要注意的是,在实际的应用中,刷新令牌通常是由次数限制的,因为刷新令牌的目的是为了避免长时间持有相同的令牌而增加安全风险。通常情况下,刷新令牌会被设计成有一定的有效期,并且只能被使用一次。

在示例代码中,刷新令牌的有效性是通过 jwt.verify() 方法来验证的。一旦刷新令牌被验证过一次后,它就会被标记为已使用,之后再次尝试验证时会触发错误。这样就可以确保刷新令牌只能被使用一次。

在实际应用中,你可以通过记录刷新令牌的使用次数,并设置一个合理的限制,以防止刷新令牌被无限次使用。通常情况下,刷新令牌的使用次数会被限制在一个较小的数量,例如一次或几次。另外,你也可以在刷新令牌的时候进行一些其他的验证操作,例如验证用户的状态、检查访问权限等,以增强安全性和控制访问。

二、单点登录:SSO

2.1 单系统登录

就是在一个系统内进行的登录,不涉及到多个系统之间的登录。在这种情况下,流程相对简单,用户只需要在应用程序的登录页面上进行一次登录操作,就可以在该系统内访问所有的资源和功能。

2.2 SSO 介绍

单点登录(Single Sign-On,简称SSO)是一种身份验证和授权的机制,允许用户在多个应用程序或系统中使用同一组凭据(如用户名和密码)进行登录,而无需在每个应用程序中单独进行登录。

SSO 的基本原理是在一个系统(认证中心)中进行身份验证后,该系统会生成一个令牌(Token),并将用户的身份信息传递给其他相关系统。这样,其他系统就可以使用这个令牌来验证用户的身份,并授权用户访问相应的资源。

  • 认证中心

相比单系统登录,SSO 需要一个独立的认证中心,只有认证中心能接受用户的用户名和密码等安全信息,其他系统则只接受认证中心的间接授权。

  • 令牌

间接授权通过令牌实现,SSO 认证中心验证登录没问题后,会创建授权令牌。在登录成功后的跳转过程中,授权令牌会作为传输传递给子系统,子系统拿到令牌即代表其得到了授权,可借此创建局部会话

  • 全局会话和局部会话

用户登录成功后,与 SSO 认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立后,用户访问子系统上受保护的资源将不再需要通过 SSO 登录中心。

2.3 SSO 登录

请添加图片描述

在上图中,是一个完整的从未登录、到登录、再到令牌生成以及系统1被注册的过程。

而在用户通过系统1登录后,访问系统2时,就可以直接在认证中心验证用户已登录,只需要将令牌返回给系统2,系统2校验后将令牌和系统2地址传递给认证中心,认证中心确认令牌有效,将系统2也进行注册,最后系统2即可和用户创建局部会话,返回受保护资源。

2.4 SSO 注销

请添加图片描述

2.5 同域 SSO

SSO 主流都是基于共享 Cookie 实现的,Cookie 的使用需要考虑跨域问题。

对于同域来说,适用场景:所有子系统都是企业自己的系统,所有系统都使用同一个一级域名,不同在于二级域名。比如 mycompany.com,三个子系统分别是认证中心 sso.mycompany.com、系统1 app1.mycompany.com、系统2 app2.mycompany.com。
请添加图片描述

核心原理:

  1. 认证中心登录成功后会设置 Cookie 的 domain 为一级域名 mycompany.com,如 Set-Cookie: SSO_TOKEN=generated_token; Domain=mycompany.com; Path=/; Secure; HttpOnly ,根据规则,可以共享 Cookie 给所有 xxx.mycompany.com
  2. 使用 Redis、Cookie 等技术让所有系统共享

2.6 跨域 SSO

1. 介绍

假如希望支持第三方系统,由于跨域,不能共享 Cookie 了,因为浏览器安全策略通常禁止跨域请求中的Cookie共享。

对于这种情况,可以通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的Cookie来实现单点登录。

举个例子,app1.com 和 app2.com,基于 UAA 授权中心 sso.com 实现单点登录:

请添加图片描述

2. 核心原理

  1. 访问系统1判断未登录,跳转到UAA系统请求授权
  2. 在UAA系统域名sso.com下的登录页面输入用户名和密码登录成功后,UAA 系统把登录信息存储到Redis中,并在浏览器写入 domain 为 sso.com 的 Cookie。并且会重定向回系统1,带上临时授权码code(一般10s内有效,并且只能校验一次)
  3. 系统1使用授权码code向UAA系统请求令牌(一般会返回访问令牌和ID令牌),系统1拿到后可以利用令牌获取用户信息并创建本地会话
  4. 访问系统2判断未登录,则跳转UAA系统请求授权
  5. 由于是跳转到UAA系统的域名下,可以通过浏览器中UAA存储的Cookie读取到之前的登录信息,判断已登录,同样会重定向回系统2,带上临时授权码code,系统2通过code可以交换令牌并创建会话

在这里插入图片描述

3. 基于 OAuth 2.0 的跨域 SSO

在前面我们介绍过,用户可以通过第三方平台(如 Google、Facebook)进行认证,OAuth 2.0 提供授权框架,OpenID Connect 扩展了 OAuth 2.0,用于用户身份认证。

为了实现跨域SSO,需要采用一些特殊的机制,如OAuth 2.0、OpenID Connect、SAML等协议可以安全地在不同域之间传递身份验证信息。这其实也属于跨域 SSO。

4. 示例代码

以下是如何使用Node.js和Express实现一个简单的跨域SSO示例。

身份认证服务器(auth.example.com)

使用expressjsonwebtoken来模拟认证服务器:

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
const bodyParser = require('body-parser');const SECRET_KEY = 'your_secret_key';
const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const REDIRECT_URI = 'http://app1.com/callback';app.use(bodyParser.json());app.get('/login', (req, res) => {// 模拟登录页面res.send('<form action="/authenticate" method="POST"><input name="username"><button type="submit">Login</button></form>');
});app.post('/authenticate', (req, res) => {const { username } = req.body;if (username) {const authCode = jwt.sign({ username }, SECRET_KEY, { expiresIn: '10m' });res.redirect(`${REDIRECT_URI}?code=${authCode}`);} else {res.status(400).send('Login failed');}
});app.post('/token', (req, res) => {const { code } = req.body;try {const payload = jwt.verify(code, SECRET_KEY);const accessToken = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });const idToken = jwt.sign(payload, SECRET_KEY, { expiresIn: '1h' });res.json({ access_token: accessToken, id_token: idToken });} catch (error) {res.status(400).send('Invalid code');}
});app.listen(3000, () => console.log('Auth server running on port 3000'));
应用服务器(app1.com)

使用expressjsonwebtoken来模拟应用服务器:

const express = require('express');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const cookieParser = require('cookie-parser');
const app = express();const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const AUTH_SERVER_URL = 'http://auth.example.com';
const SECRET_KEY = 'your_secret_key';app.use(cookieParser());
app.use(express.json());app.get('/', (req, res) => {const token = req.cookies.token;if (token) {try {const payload = jwt.verify(token, SECRET_KEY);res.send(`Welcome, ${payload.username}`);} catch (err) {res.redirect('/login');}} else {res.redirect('/login');}
});app.get('/login', (req, res) => {const authUrl = `${AUTH_SERVER_URL}/login?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http://app1.com/callback`;res.redirect(authUrl);
});app.get('/callback', async (req, res) => {const { code } = req.query;try {const response = await axios.post(`${AUTH_SERVER_URL}/token`, {code,client_id: CLIENT_ID,client_secret: CLIENT_SECRET,redirect_uri: 'http://app1.com/callback',});const { access_token, id_token } = response.data;res.cookie('token', access_token, { httpOnly: true });res.redirect('/');} catch (error) {res.status(400).send('Token exchange failed');}
});app.listen(3001, () => console.log('App1 running on port 3001'));
应用服务器(app2.com)

类似于app1.com,但URL和端口不同:

const express = require('express');
const jwt = require('jsonwebtoken');
const axios = require('axios');
const cookieParser = require('cookie-parser');
const app = express();const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const AUTH_SERVER_URL = 'http://auth.example.com';
const SECRET_KEY = 'your_secret_key';app.use(cookieParser());
app.use(express.json());app.get('/', (req, res) => {const token = req.cookies.token;if (token) {try {const payload = jwt.verify(token, SECRET_KEY);res.send(`Welcome, ${payload.username}`);} catch (err) {res.redirect('/login');}} else {res.redirect('/login');}
});app.get('/login', (req, res) => {const authUrl = `${AUTH_SERVER_URL}/login?response_type=code&client_id=${CLIENT_ID}&redirect_uri=http://app2.com/callback`;res.redirect(authUrl);
});app.get('/callback', async (req, res) => {const { code } = req.query;try {const response = await axios.post(`${AUTH_SERVER_URL}/token`, {code,client_id: CLIENT_ID,client_secret: CLIENT_SECRET,redirect_uri: 'http://app2.com/callback',});const { access_token, id_token } = response.data;res.cookie('token', access_token, { httpOnly: true });res.redirect('/');} catch (error) {res.status(400).send('Token exchange failed');}
});app.listen(3002, () => console.log('App2 running on port 3002'));

上述示例展示了如何使用OAuth 2.0和OpenID Connect协议在跨域的情况下实现单点登录。

关键点在于:

  1. 使用授权码和令牌进行身份验证和授权
  2. 通过重定向URL在不同域之间传递身份验证信息
  3. 使用安全的令牌存储和传输机制(如HTTPS和HttpOnly Cookie)

这种方式确保了用户在多个域中的无缝登录体验,同时保证了安全性。

后端直接写 Cookie 存在的跨域限制

注意在上述代码中,后端使用这种方法使得前端刷新页面时,下次请求会自动带上 Cookie。

res.cookie('token', access_token, { httpOnly: true });
res.redirect('/');

请添加图片描述

但默认情况下只能在前端资源和后台服务位于相同的域名或子域时才有效

**假如是前端和后端服务是跨域呢?**因为在当下前后端分离是非常常见的场景。

  • 第一种方案:重定向

通过在服务器端进行重定向或请求代理来设置Cookie。例如,域名A的后台可以重定向用户到域名B的一个特定页面,同时在重定向的过程中通过HTTP响应头设置Cookie。示例:

serviceA.com 后端设置响应头:

HTTP/1.1 302 Found
Location: https://serviceB.com/set-cookie?token=abc123

serviceB.com 接收到请求后,在服务器端设置Cookie:

// 在 `https://serviceB.com/set-cookie` 处理请求
const token = req.query.token;
res.cookie('auth_token', token, { domain: '.serviceB.com', httpOnly: true, secure: true });
res.redirect('/');
  • 第二种方案,由浏览器携带临时授权码code发起请求

请添加图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/19564.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用jquery.mousewheel-3.0.6.pack.js时报错

基于1.12.4版本的jquery.min.js&#xff0c;在使用jquery.mousewheel-3.0.6.pack.js时报错了&#xff1a; 可以如下解决&#xff1a; addEventListener事件里要加上{ passive: false }&#xff0c;这样就可以在使用鼠标滚轮放大缩小图片时&#xff0c;就不会报上述的错误了。 …

解决docker容器: bash: ping: command not found, 并制作镜像

一. 出现原因 从 dockerhub 拉下来的镜像都是最轻量级的, 不会安装各种工具, 所以使用 ping, vim 等命令, 会出现 command not found 二. 解决方式 2.1 安装工具包 进入到一个正在运行的容器内部, 执行命令: apt-get update 之后会发现, 容器正在更新软件包, 不过最终会由…

水工建筑物荷载设计规范的技术关键点

遵循《水工建筑物荷载设计规范》的指引&#xff0c;水工建筑所承载的荷载依据其作用的时间变异性特征&#xff0c;可划分为永久作用荷载、可变作用荷载以及偶然作用荷载三个主要类别。 一、荷载分类及其特点 永久作用荷载&#xff1a;涉及建筑物自身的结构重量&#xff0c;以及…

主线程等待所有线程结束之后再执行

如何让主线程等待所有线程结束之后再执行 1、Future的机制&#xff0c;使用Future.get()阻塞等待结果&#xff08;Future&#xff0c;FutureTask&#xff09; 2、CountDownLatch同步工具类&#xff0c;此类的作用就是一个线程等待所有线程结束之后再执行 3、CompletableFuture …

音视频开发—FFmpeg播放YUV文件,YUV转换为JPEG操作

文章目录 1.使用命令行播放YUV数据1.1命令解析1.2参数说明 2.使用C语言实现将YUV数据转为JPEG图片格式2.1需求分析2.2读取YUV源文件2.3将YUV数据封装为AVFrame2.4将NV12 转换为YUV420平面格式2.5初始化MJPEG编码器2.6将YUV420P编码为JPEG2.7将编码数据写入图片文件2.8完整代码 …

App自动化测试_Python+Appium使用手册

一、Appium的介绍 Appium是一款开源的自动化测试工具&#xff0c;支持模拟器和真机上的原生应用、混合应用、Web应用&#xff1b;基于Selenium二次开发&#xff0c;Appium支持Selenium WebDriver支持的所有语言&#xff08;java、 Object-C 、 JavaScript 、p hp、 Python等&am…

安装flask:后端框架的学习之旅

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一、引言 二、创建虚拟环境 1. 引入虚拟环境的概念 2. 创建Flask虚拟环境 三、安装Flask…

CSRF跨站请求伪造漏洞

CSRF跨站请求伪造漏洞 1.CSRF漏洞概述2.防御CSRF攻击3.CSRF防御绕过CSRF令牌未绑定到用户会话自定义标头令牌绕过绕过Referer检查关键词绕过 4.利用示例使用HTML标签进行GET表单 GET 请求表单POST请求通过 iframe 发送表单 POST 请求Ajax POST 请求 5.CSRF BP 验证方法6.CSRF测…

HTTP协议介绍与TCP协议的区别

1、HTTP介绍 HTTP&#xff08;超文本传输协议&#xff0c;Hypertext Transfer Protocol&#xff09;是一种用于从网络传输超文本到本地浏览器的传输协议。它定义了客户端与服务器之间请求和响应的格式。HTTP 是基于TCP/IP 进行数据的通信&#xff0c;通常使用端口 80/8080。HTT…

软件需求规格说明书(Word原件@配套软件全资料)

软件需求规格说明书编写规范编写规范 1.项目背景 2.项目目标 3.系统架构 4.总体流程 5.名称解释 6.功能模块 软件项目相关全套精华资料包获取方式①&#xff1a;点我获取 获取方式②&#xff1a;本文末个人名片直接获取。 软件资料清单列表部分文档清单&#xff1a;工作安排任…

Optional类

一、概述 泛型类、java8引进的、java.util包里 二、作用 解决空指针异常带来的不便 三、做法 将对象封装为一个Optional对象&#xff0c;如果封装的对象为空&#xff08;即该对象不存在&#xff09;&#xff0c;可以使用默认值和或者执行默认操作 四、方法 1、empty() 创…

【Qt知识】Qt Creator快捷键

以下是Qt Creator中的一些常用快捷键列表&#xff08;持续更新&#xff09;&#xff1a; 基本编辑 多行注释/取消多行注释: Ctrl /编译工程: Ctrl B运行工程: Ctrl R整行上移/下移: Ctrl Shift ↑/↓查找: Ctrl F函数声明和定义切换: F2向下查找: F3头文件和源文件切换:…

【postgresql初级使用】初识触发器,在数据行发生变化时自动执行用户行为,也可以SQL级别触发,特别是视图上可以有触发器了

初识触发器 ​专栏内容&#xff1a; postgresql使用入门基础手写数据库toadb并发编程 个人主页&#xff1a;我的主页 管理社区&#xff1a;开源数据库 座右铭&#xff1a;天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物. 文章目录 初识触发器概…

【成品设计】基于RT-thread星火开发板的智能监测系统设计

《基于RT-thread星火开发板的智能监测系统设计》 所需器件&#xff1a; 主控&#xff1a;STM32F407星火开发板。温湿度传感器&#xff1a;采集当前环境中的温湿度。环境传感器&#xff1a;采集当前环境中的光照强度。CO2传感器&#xff0c;采集当前环境中的C02浓度。粉尘传感…

excel怎么对非数字求和汇总?

如&#xff1a;学生小王的成绩为&#xff1a;A&#xff0c;A&#xff0c;A&#xff0c;A&#xff0c;B&#xff0c;B-……想得到的成绩汇总求和为&#xff1a;2A,2A,1B,1B- 如果在低版本里&#xff0c;用公式计算可能相当复杂&#xff0c;但是有了TEXTJOIN函数和UNIQUE函数&…

校园交友|基于SprinBoot+vue的校园交友网站(源码+数据库+文档)

校园交友网站 目录 基于SprinBootvue的校园交友网站 一、前言 二、系统设计 三、系统功能设计 1系统功能模块 2后台功能模块 5.2.1管理员功能模块 5.2.2用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#x…

CSS学习笔记:bootstrap的介绍、下载、使用及响应式布局的实现

介绍 Bootstrap 是由 Twitter 公司开发维护的前端 UI 框架&#xff0c;它提供了大量编写好的CSS 样式 bootstrap过时了吗&#xff1f; 其实在学bootstrap这一部分的网课时&#xff0c;在弹幕上看到一些同学说bootstrap已经过时了&#xff0c;这里谈谈我的看法 我的前端学习…

3天13部,端午档电影数何以是五一档2倍?

离端午还有12天&#xff0c;院线端午档再上热搜。 截止至5月29日&#xff0c;本次端午档将有13部电影在3天的节假日内集中上映&#xff0c;这一数量仅比2021年端午档的最高纪录少了2部&#xff0c;几乎是今年五一档期上映影片数量&#xff08;7部&#xff09;的两倍。 并且与…

UE5 Http Server

前言 最近要用UE 作为一个服务器去接收来自外部的请求&#xff0c;从而在UE中处理一些内容&#xff0c;但是之前只做过请求&#xff0c;哪整过这玩意&#xff0c;短期内还得出结果&#xff0c;那怎么搞嘞&#xff0c;本着省事的原则就找找呗&#xff0c;有没有现成的&#xff0…

结构体中内存的对齐

前言 学C的同学应该知道~ 想精通C语言就不得不面对—指针与内存 续上次指针进阶&#xff0c;这一章我来聊一聊C语言内存对齐的问题 学习结构体的你有没有注意过结构体向系统申请的内存为多少呢的&#x1f601; 思考 #include<stdio.h> typedef struct s1 {char a;char …