目录
- 一、认证机制
- 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 存在的跨域限制
本文主要详细介绍单点登录。一般来说,一个完整的登录方案,包括:
- 认证机制。如基于会话的认证(Session-based Authentication)、JSON Web Tokens (JWT)。
- 单点登录。支持跨多个子系统的登录方案。
一、认证机制
1.1 基于会话的认证(Session-based Authentication)
1. 介绍
会话基本上是一种在客户端(通常是浏览器)和服务器之间维护状态的方法。
在会话认证中,用户通过提供凭据(如用户名和密码)来登录,并且在登录成功后,服务器会创建一个会话,并为用户分配一个唯一的会话标识符(通常称为会话ID)。这个会话ID通常存储在客户端的Cookie中,或者通过其他方式(如URL参数)发送到客户端,并在之后的每个请求中被客户端发送回服务器。
2. 基本流程
以下是会话认证的基本流程:
-
用户提供凭据: 用户通过提供用户名和密码来登录。这些凭据通常通过一个登录表单或者其他身份验证界面提交给服务器。
-
服务器验证凭据: 服务器收到用户提供的凭据后,会验证其有效性。这通常涉及到将用户提供的凭据与服务器中存储的相应凭据进行比对,以确保用户提供的凭据是有效的。
-
创建会话: 如果提供的凭据是有效的,服务器会创建一个会话,并为用户分配一个唯一的会话ID。
-
将会话ID发送到客户端: 一旦会话被创建,服务器将会话ID发送到客户端。这通常通过在响应中设置一个Cookie,其中包含会话ID,或者通过其他方式(如URL参数)将其发送到客户端。
-
客户端发送会话ID: 客户端在之后的每个请求中都会发送会话ID到服务器。这通常通过在请求的Cookie中包含会话ID,或者通过其他方式(如URL参数)将其发送到服务器。
-
服务器验证会话ID: 服务器在接收到客户端的请求时,会检查请求中是否包含有效的会话ID。如果会话ID是有效的,服务器将允许请求继续处理;否则,可能需要用户重新进行身份验证。
在该环节,涉及到的具体原理:
- 会话ID的存储: 当服务器创建会话时,会为该会话生成一个唯一的会话ID,并将其与用户的身份信息(如用户ID、角色等)关联起来。服务器通常会将这些会话信息存储在内存(适合轻量级应用或短暂会话,因此在服务器重启时即丢失)、数据库(适合长期或持久性的会话)、缓存(适合需要优化访问速度和性能时,常见解决方案包括 Redis、Memcached)或者其他持久化存储中,以便后续验证用户的身份。
- 会话ID的发送: 服务器在响应中将会话ID发送到客户端。这通常通过在响应的Cookie中设置会话ID,或者通过其他方式将其发送到客户端。
- 请求中的会话ID验证: 当服务器收到客户端的请求时,它会检查请求中是否包含会话ID。这通常通过检查请求中的Cookie,或者其他地方(如URL参数)来获取会话ID。
- 会话ID的验证: 服务器将会话ID与存储的会话信息进行比对,以验证会话ID的有效性。服务器会检查会话ID是否存在,并且是否与已存储的会话信息相关联。如果会话ID有效且与相应的会话信息匹配,则服务器将确认用户的身份,并允许请求继续处理。
- 无效会话ID的处理: 如果会话ID无效或者与任何会话信息不匹配,服务器可能会要求用户重新进行身份验证,或者以其他方式处理无效会话ID的情况,例如返回错误信息或重定向到登录页面。
- 会话管理: 一旦用户完成了会话,服务器可能会终止会话并销毁相关数据,以便释放资源并提高安全性。
会话认证的原理是建立在服务器和客户端之间共享一个标识符(会话ID)的基础上,以便服务器可以识别特定用户的请求。通过这种方式,服务器可以跟踪用户的身份状态,并在必要时进行身份验证。
1.2 JSON Web Tokens (JWT)
1. 介绍
JSON Web Tokens (JWT) 是一种用于在网络应用间安全传递信息的一种简洁的、自包含的方式。JWT可以通过数字签名来验证数据的完整性和来源。JWT通常用于身份验证和信息交换,特别是在分布式环境中。
2. jwt 组成
在JWT中,签名是放在JWT的第三部分,即签名部分。JWT由三部分组成,它们分别是:
- Header(头部):包含了关于JWT的元数据信息,通常包括算法(alg)和令牌类型(typ)等。
- Payload(负载):包含了JWT的主要信息,例如用户ID、角色、过期时间等。
- 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进行用户登录的流程和原理:
-
用户提供凭据: 用户通过提供凭据(例如用户名和密码)来尝试登录到应用程序。
-
服务器验证凭据: 服务器接收到用户提供的凭据后,会验证其有效性。通常情况下,这涉及到与存储在服务器上的用户凭据进行比对。如果凭据有效,则服务器将继续处理登录请求。
-
生成JWT: 一旦服务器验证了用户的凭据,它会为用户生成一个JWT。JWT通常包含一些关键信息,如用户ID、角色、过期时间等。这些信息会被编码到JWT的负载(Payload)中。
-
签名JWT: 服务器使用密钥对JWT进行签名,以确保JWT的完整性和真实性。签名的过程使用一种加密算法,如HMAC(用于对称密钥)或RSA(用于非对称密钥)。
-
将JWT发送给客户端: 服务器在响应中将生成的JWT发送给客户端。这通常是通过将JWT包含在响应的JSON对象中发送回客户端的方式来完成的。
-
客户端存储JWT: 客户端收到JWT后,通常会将其存储在本地,例如在浏览器的本地存储(localStorage)或会话存储(sessionStorage)中。
-
将JWT发送到服务器: 客户端在后续的请求中,通常会将JWT包含在请求的头部(通常是Authorization头)中发送回服务器。
-
验证JWT: 服务器在接收到请求时,会检查JWT的有效性。这包括验证JWT的签名以确保其完整性,即对头部和负载部分使用相同的加密算法与服务器上存储的密钥进行签名,并与JWT中的签名部分进行比对;并检查JWT的有效期以确保其尚未过期,JWT通常包含一个exp字段,代表有效期,服务器基于此来判断是否需要重新进行身份验证。
-
提取JWT中的信息: 如果JWT有效,服务器会解码JWT的负载并提取其中的用户信息,以确定用户的身份和权限。这样服务器就可以基于用户的身份来处理请求。
-
处理请求: 如果JWT有效且包含了足够的信息来识别和授权用户,服务器会处理请求并响应相应的数据或操作。
-
处理过期的JWT: 如果JWT过期或者不再有效,服务器可能会要求客户端重新进行身份验证,或者以其他方式处理无效的JWT。
JWT 的原理在于它是一个自包含的令牌,其中包含了所需的用户信息,并且通过数字签名来确保令牌的完整性和真实性。这使得服务器无需在本地存储会话信息,从而简化了会话管理和状态维护。同时,JWT 的使用也提高了跨域和分布式环境下的身份验证和信息交换的安全性和效率。
4. 阻止列表
有时,服务器可能会维护一个阻止列表,用于存储已经失效的JWT或者被标记为不可信的JWT。在验证JWT时,服务器可能会先检查JWT是否在阻止列表中,如果是,则拒绝该JWT。
5. 刷新令牌
在某些情况下,服务器可能会使用JWT中的某些信息来生成新的JWT,以实现令牌的刷新。它允许客户端在访问令牌过期或失效时获取新的令牌,而无需重新进行身份验证。
这可以用于延长用户的会话期限或者更新令牌中的其他信息。
这个过程可能会涉及到一些特定的流程,例如检查刷新令牌的有效性等。具体来说,刷新令牌的流程如下:
-
获取刷新令牌: 在用户登录或者进行身份验证成功后,服务器会生成一对访问令牌(Access Token)和刷新令牌(Refresh Token),并将它们发送给客户端。刷新令牌通常包含在JWT中的Payload中,可以携带一些额外的信息,如用户ID等。
-
使用访问令牌访问资源: 客户端使用访问令牌来访问受保护的资源,例如API端点。服务器会验证访问令牌的有效性,并根据其包含的信息来确定用户的身份和权限。
-
访问令牌过期或失效: 当访问令牌过期或失效时,客户端可能会收到一个响应,指示访问被拒绝或者令牌过期。此时客户端可以使用刷新令牌来获取新的访问令牌,而无需重新进行用户身份验证。
-
使用刷新令牌获取新的访问令牌: 客户端使用刷新令牌发送请求到服务器,请求服务器颁发新的访问令牌。服务器接收到请求后,会验证刷新令牌的有效性,并根据需要对客户端进行身份验证。
-
颁发新的访问令牌: 如果刷新令牌有效且合法,服务器会生成一个新的访问令牌,并将其发送给客户端。客户端可以使用这个新的访问令牌来继续访问受保护的资源。
-
重复使用新的访问令牌: 客户端可以继续使用新颁发的访问令牌来访问资源,直到新的访问令牌过期或失效,或者直到需要再次刷新令牌。
刷新令牌的机制允许客户端在访问令牌过期或失效时获取新的访问令牌,而无需用户重新进行身份验证,从而提高了用户体验和安全性。
当使用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。
核心原理:
- 认证中心登录成功后会设置 Cookie 的 domain 为一级域名 mycompany.com,如
Set-Cookie: SSO_TOKEN=generated_token; Domain=mycompany.com; Path=/; Secure; HttpOnly
,根据规则,可以共享 Cookie 给所有 xxx.mycompany.com - 使用 Redis、Cookie 等技术让所有系统共享
2.6 跨域 SSO
1. 介绍
假如希望支持第三方系统,由于跨域,不能共享 Cookie 了,因为浏览器安全策略通常禁止跨域请求中的Cookie共享。
对于这种情况,可以通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的Cookie来实现单点登录。
举个例子,app1.com 和 app2.com,基于 UAA 授权中心 sso.com 实现单点登录:
2. 核心原理
- 访问系统1判断未登录,跳转到UAA系统请求授权
- 在UAA系统域名sso.com下的登录页面输入用户名和密码登录成功后,UAA 系统把登录信息存储到Redis中,并在浏览器写入 domain 为 sso.com 的 Cookie。并且会重定向回系统1,带上临时授权码code(一般10s内有效,并且只能校验一次)
- 系统1使用授权码code向UAA系统请求令牌(一般会返回访问令牌和ID令牌),系统1拿到后可以利用令牌获取用户信息并创建本地会话
- 访问系统2判断未登录,则跳转UAA系统请求授权
- 由于是跳转到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)
使用express
和jsonwebtoken
来模拟认证服务器:
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)
使用express
和jsonwebtoken
来模拟应用服务器:
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协议在跨域的情况下实现单点登录。
关键点在于:
- 使用授权码和令牌进行身份验证和授权。
- 通过重定向URL在不同域之间传递身份验证信息。
- 使用安全的令牌存储和传输机制(如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发起请求