开放授权协议:Oauth2.0
Posted Heaven-Wang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开放授权协议:Oauth2.0相关的知识,希望对你有一定的参考价值。
简介
Oauth
的全称是Open Authorization,是一个开放授权协议,它制定了一些标准,可以使得第三方应用无需使用用户名密码即可获得用户资源。目前很多应用都提供了第三方社交账号登录及绑定,而这背后使用的技术就是Oauth2.0
.
Oauth协议最新版本是Oauth2.0,也是目前使用最广泛的协议,所以后面介绍的Oauth主要是指Oauth2.0。
Oauth协议背景
随着分布式web service、开放平台和云计算使用的越来越多,第三方应用需要能访问到一些用户的私有资源。然而这些资源都是受保护的,所以需要使用者提供资源拥有者的私有证书(用户名/密码)
进行身份认证。
但是,如果资源拥有者
(后面简称用户)把自己的私有证书(用户名/密码)透露给第三方应用
,这样会导致很多的问题:
- 信任:要想让用户输入用户名密码,应用必须得到用户信任,一般用户是不愿把自己的用户名密码透露给第三方的
- 第三方应用要明文保存用户的私有证书(密码),以便再次使用
- 一旦有了用户名密码,第三方应用可能获得过多的资源权限,比如允许访问的资源范围和限制使用时间
- 一旦用户修改密码,第三方应用则无法使用
- 用户无法单独撤销某个第三方的访问权限,只能通过修改密码来回收所有权限
现实生活中有一个非常典型的例子就是12306抢票软件,抢票软件属于第三方应用
,它需要用户的私有证书(用户名密码)
才能为用户抢票,因为铁道部12306购票系统没有对外开放,所以如果用户想要使用抢票软件,就必须提供用户名密码给抢票软件,这也是为何出现过几次12306用户名密码泄露事件的原因。
OAuth协议采取的策略是给第三方应用
一套与用户不同的私有证书来控制其对资源的访问。
这个特殊的私有证书就是访问令牌-Access Token
,它代表了一个表示特定作用域、持续时间及其它属性的字符串。
这个Access Token
是由用户授权给第三方应用,第三方应用使用Access Token来访问服务器受保护的资源。
名词术语
在具体介绍Oauth之前先介绍一些简单的属于帮助大家更好的理解Oauth。如果一开始看不懂这些术语也不要紧,可以先跳过,等后面遇到这些术语是可以再回来参考一下。
Authentication(认证)
认证
的含义是确定who are you 。在现实世界中,警察通过对比你身份证上的照片与你的长相是否一致来确认你的身份,而在网络世界中,认证就是确定电脑面前的你是不是当前账户的拥有者。最典型的认证方式就是让用户提供用户名和密码,登陆就是最常见的认证行为。
Federated Authentication(联合认证)
很多应用都有自己的认证系统,但是还有很多应用需要依赖其它的服务来认证用户,这就是联合认证
。如单点登录、OpenID等等。
Authorization(授权)
认证是确定你是谁,而授权
是更近了一步,确定你能做什么,比如是否能够阅读相关文档、是否能够获取一个email信息。授权是认证后的进一步操作,先认证才能授权。一般的web系统也是遵循先认证后授权模式,用户先进行登录,系统再判断登录用户有何权限。
Delegated Authorization(委托授权)
委托授权
就是将权限的授予委托给其它人或应用。
Oauth中的角色
Oauth协议中涉及到很多角色,我们就拿现实中的一个例子介绍。微信开放平台为第三方应用开放了微信登录接口, 有一个第三方应用今日头条,为了提高用户体验要支持微信登录,需要得到用户的账号和头像等资源。
Client客户端
客户端指的是需要获取用户资源的第三方应用
,客户端在获取资源之前必须得到用户的允许。上面例子中今日头条就是客户端。
ResouceOwner 资源服务器(api server)
资源服务器
就是客户端要调用的用户资源所在的服务器,资源服务器一般以API的形式暴露资源给客户端。上面例子中微信的开放平台就是资源服务器,它拥有过微信用户的账号、头像登录资源。
ResourceOwner 资源拥有者
资源拥有者
指的是资源的所属用户,客户端要访问资源服务器受保护的资源,必须得到资源拥有者的授权。上面例子中的微信用户就是资源拥有者。
Authorization Server 授权服务器
授权服务器
负责认证用户登录信息,颁发token给客户端。对于小的应用提供者或者大型系统的开放平台,一般既是资源服务器又是授权服务器。上面例子中微信开放平台也是授权服务器。
Oauth1.0中关于是否签名的争议
其实在OAuth2.0之前还有一个Oauth1.0。在Oauth1.0中,客户端的每次请求都需要进行加密的签名。签名对于开发人员来说非常不友好,api的调用者往往更喜欢简单直接的授权协议。
Oauth1.0诞生于2007年,当时它的发明者认为只有加密的签名才能使api更加安全,因为那个时代SSL/TLS还没有兴起。但是近些年,SSL/TLS已经非常流行,成为了保障api安全的有力途径之一。
一些安全社区也渐渐弱化了对签名必要性的态度。一方面Oauth1.0加密的复杂性导致api接受程度很低,另一方面SSL/TLS变得越来越流行,这两方面因素推动了OauthWRAP(Web Resource Authorization Profiles)的发展。OauthWRAP是Oauth2.0的先驱,它降低了签名复杂度,而且又引入了bearer tokens。
尽管Oauth2.0已经在标准社区内达成共识,但仍有一些个人对弃用签名持反对态度。所以工程师们需要在安全和易用性之间寻求一个微妙的平衡。
应用注册
Oauth要求第三方应用
在请求授权之前先在授权服务器
进行注册
,这样可以更好的管理接入的第三方应用。注册应用的时候一般需要提供一些基本信息,比如应用名称、网址、logo等。另外,还需要提供一个Redirect URI,这个就是第三方应用的地址。Redirect URI
可以在用户授权完成之后重定向回你的应用。Redirect URI 授权服务器只会重定向用户到已经注册过的URI,以避免一些恶意攻击。任何的Redirect URI必须经过TLS的保护,授权服务器
应该只会重定向到https开头的URI。这样做的目的是保护token在授权过程中不被截取。
ClientID和ClientSecret
当注册完应用时,你将会收到一个ClientId
和ClientSecret
。ClientID是公共的信息,用来识别你是哪一个应用,相当于用户名,ClientID可以直接写在javascript或者源码页面里面。但是, ClientSecret必须保证绝对机密,不能泄露给其他人。如果你部署的应用无法保证ClientSecret安全的话,比如Javascript应用或者native APP,那么则不能使用ClientSecret,一般来说,只有服务器端才可以保存ClientSecret。
为什么需要注册?
首先需要通过注册获取客户端的clientId和clientSecret。clientId用来识别是哪个应用,而clientSecret是应用获取accessToken和refreshToken的凭证。
其次是提升授权过程的用户体验。在授权服务的页面可以展示申请应用的名称和logo。
关于客户端类型、AccessToken、授权流程
第一版的OAuth最初是被用来解决C/S Web应用的api鉴权,没有考虑到移动应用、客户端应用、JavaScript应用以及其它情况。在OAuth2.0中,对每一种客户端类型,都定义了不同的授权流程,下面会介绍Oauth2.0的四大授权流程
,这些流程分别适用于不同的客户端类型
。
客户端类型
有服务端的web应用(server-side web applicatioin)
Oauth客户端(就是第三方应用)运行在web应用的服务端,也就第三方应用是运行在server后台上的,客户端应用与授权服务器
是server to server 的交互,用户无法看到clientSecret和accessToken。
浏览器应用 (client-side application running in a web browser)
Oauth客户端运行在用户的浏览器中,代码被分发到用户的浏览器端,用户可以看到客户端代码。这种类型的客户端无法保证clientSecret安全,所以Oauth不能签发clientSecret给这种客户端。
Native 应用
这种客户端可浏览器应用类似,应用被整体打包给用户,也不能签发clientSecret。还有一点就是Native应用无法使用浏览器的一些特性。
关于AccessToken
在Oauth2.0中,不管是哪一类的客户端,对保护资源的访问方式都是一样的:即每次请求携带一个accessToken即可。可见Oauth2.0使用起来非常简单,没有加密没有签名。
关于AccessToken以什么方式传递,推荐的做法是将accessToken放到HTTP的 Authorization header
中:
GET /tasks/v1/lists/@default/tasks
HTTP/1.1 Host: www.googleapis.comAuthorization:
Bearer ya29.AHES6ZSzX
Authorization header
有如下特性:
- 这个header通常不会被打印到log中
- 这个header不会被缓存
- 这个header不会被存储到浏览器中
当然你也可以直接将AccessToken放到url参数中,也可以通过表单提交,这具体得看api提供者是否支持。如果放到url中会是这样:
https://www.googleapis.com/tasks/v1/lists/@default/tasks?callback=outputTasks&access_token=ya29.AHES6ZTh00gsAn4
四大授权流程
上面客户端类型
介绍中提到了不同的客户端安全特性是不一样的,server端类型的应用是比较安全的,用户无法看到源代码,可以持有clientSecret。而浏览器、JavaScript、Native应用由于用户可以直接看到源代码,不能使用clientSecret。
所以,OAuth2提供了不同的grant type
以适应不同的客户端类型以及应用场景,具体有如下几种:
- Authorization Code 授权码模式:主要是web server 类型的应用
- Implicit 简化模式:浏览器应用或者移动APP
- Password 密码模式:通过用户名密码登录,适用于信任应用(自研应用)
- Client credentials 客户端模式:只认证应用,无需用户授权Web Server 应用
下面将详细介绍Oauth2.0的具体授权流程。
有server端的Web应用授权流程(AuthorizationCode)
这是最常见的一种应用,这种应用都是运行在服务端,应用的源码对外界是不可见的。此类型应用一般采用AuthorizationCode
模式。
AuthorizationCode 授权码模式
应用请求用户授权
应用(即Oauth客户端)先引导用户到授权服务器页面,此阶段要提供三个参数:
- responseType:code。告诉授权服务器使用授权码流程
- clientId:告诉授权服务器是哪个应用
- RedirectUrl:用户授权后通过此地址将authorizationCode传递给应用
- scope:应用申请的资源范围
- state:应用后端生成的唯一随机值,每次请求都要变化,防止csrf攻击(后面会介绍)
用户授权
用户在授权服务器页面中会看到clientId的应用要请求你收钱scope范围的资源,用户可以在这个页面点击同意,即进行用户授权。此时授权服务器将自动生成一个authorizationCode,并以参数的形式附在redirectUrl地址上重定向到应用。
注意,授权服务器在重定向到redirectUrl时,应该根据clientId校验此url是否与注册中的redirectUrl一致。
应用持授权码(authorizationCode)换取accessToken
用户授权之后应用会通过redirectUrl参数中获取到用户的授权码,然后用授权码向授权服务器换取accessToken。此步骤需要提供如下几个参数:
- grantType:authorization_code。告诉授权服务器使用授权码流程
- clientId:应用(oauth客户端)id
- clientSecret:秘钥,相当于应用的密码
- code:上一步获得的用户授权码
此时,授权服务器会返回:
- accessToken:即访问资源的访问码
- refreshToken:刷新码,后面会介绍
应用持accessToken访问相应的资源
有了accessToken,就可以持accessToken访问相应的api了。
具体流程图如下:
具体实例
假如有这么一个场景,工资管理应用希望能访问经理的Google任务管理应用,来提醒经理及时批准,保证员工按时得到工资。
第一步:请求用户授权
首先工资管理应用(下图中的Payroll)中有一个功能,就是请求访问经理的Google任务管理应用。
当经理点击这个功能连接时,Payroll创建一个登录连接把用户连接到Google的授权服务器:
https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=01&redirect_uri= https://payroll.saasyapp.com/oauth_response&scope=tasks
此时Payroll将自动跳转到Google的授权服务器授权页面:
如果用户点解了“Allow”,授权服务器通过客户端指定的redirect uri返回一个auth code:
https://payroll.saasyapp.com/oauth_response?code=AUTH_CODE_HERE
第二步:用auth code换取access token
当第一步没有出现任何问题,并且用户点击了同意,Google的授权服务器会根据redirectUrl将用户重定向回工资管理(Payroll)应用。并且还会在redirectURL后面附上两个应答参数:
- code:授权码,代表用户的授权
- state:应与第一步请求授权中的state值一模一样,原样返回
例如:
https://payroll.saasyapp.com/oauth2callback? code=AB231DEF2134123kj89&state=987d43e51a262f
下面,payroll应用就可以凭获得的auth code换取可以访问api的access token了。
授权服务器都会提供一个token接口用来换取access token,如Google的为:
https://accounts.google.com/o/oauth2/token
上面已经介绍过了,这一步换取需要传递如下参数给token接口:
- grantType:authorization_code。告诉授权服务器使用授权码流程
- clientId:应用(oauth客户端)id
- clientSecret:秘钥,相当于应用的密码
- code:上一步获得的用户授权码
上面参数中,clientId和clientSecret相当于应用的用户名密码,授权服务器根据这两个参数认证应用的合法性。前面也提到了,clientId和clientSecret最好通过HTTP的Authorization Header来传递,即其加密成Base64UrlEncode(clientId:clientSecret)字符串,如下所示:
Authorization: Basic
MDAwMDAwMDA0NzU1REU0MzpVRWhrTDRzTmVOOFlhbG50UHhnUjhaTWtpVU1nWWlJNg==
当授权服务器认证通过之后 ,token接口会返回:
"access_token" : "ya29.AHES6ZSzX",
"token_type" : "Bearer",
"expires_in" : 3600,//access_token有效期,单位秒 "refresh_token":"1/iQI98wWFfJNFWIzs5EDDrSiYewe3dFqt5vIV-9ibT9k"
关于access token和 refresh token?
上面的token接口返回数据中,除了access_token 还有一个 expires_in 和refresh_token。expire_in代表的是accessToken的有效期是3600秒,一般来说accessToken有效期不会太长。之所以会有一个长有效期的refresh_token和一个短有效期的access_token,主要是解决安全问题、性能问题以及提高用户体验。
为什么要给accessToken一个有效期呢,因为accessToken存在一定的安全风险。尽管accessToken是基于https传输的,但是由于它本身没有进行加密,一旦泄露,任何人都可以凭这个accessToken访问相关的api。所以,通过赋予accessToken一个短暂的有效期,可以降低其泄露带来的风险。
accessToken的校验本身存在一定的性能损耗。当一个客户端访问一个api时,server端需要从请求中解析出accessToken,先对accessToken进行校验,校验通过后才允许真正访问api。对accessToken校验一般存在下面几种方式:
- 访问专门的access token校验服务器进行校验
- 访问数据库进行校验(即将认证信息存储在数据库中)
- 访问内存缓存(redis)进行校验(即将认证信息存储在Redis中)
- 对accessToken进行解密、验签(无须存储)
不管通过上面哪种方式,accessToken的校验都会耗费一定时间消耗。
accessToken还有一个特点就是它可以被撤销。但是如果accessToken允许撤销的话,校验服务器就要需要存储accessToken的状态,而不能采用解密、签名等方式。所以从这个角度来说,客户端因为accessToken生命周期较短,及时被撤销也不会长久存储它。
第三步 调用api
获得accessToken之后,下面就很简单了,在请求api时带上这个accessToken就可以了。
上面介绍过了,accessToken可以放在
- http header中(推荐bear token方式)
- query param中
下面是Java示例代码:
public static String requestApi(String url, String accessToken)
HttpClient client = new DefaultHttpClient();
post = new HttpPost(url);
//直接将accessToken放到header里面
post.setHeader("Authorization: Bearer",accessToken);
//也可以直接放到请求参数里面
//StringEntity entity = new StringEntity(accessToken, "UTF-8");
//post.setEntity(entity);
HttpResponse response = client.execute(post);
result = EntityUtils.toString(response.getEntity(), "UTF-8");
return result;
下面是校验accessToken服务伪代码:
public void validateAccessToken(HttpServletRequest request,String url)
String token=request.getParameter("access_token");
if(token==null)
throw new RequestException(401,"need access_token");
AccessToken accessToken=redisClient.get(token);
if(accessToken!=null)
throw new RequestException(401,"invalid_token");
if(!accessToken.getScops.contains(url))
throw new RuntimeException(401,"insufficient scope");
错误处理
当客户端在调用api时,一定会遇到各种各样的错误。比如,access token过期或者失效,应该给客户端返回HTTP 4XX错误。
Oauth协议推荐当access token校验失败时,返回一个HTTP WWW-Authenticate response header给客户端,比如当token过期时,客户端会收到:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example",
error="invalid_token",
error_description="The access token expired"
当然你也可以自己定义返回错误的方式,比如直接返回一个HTTP 400,并提供具体的错误信息,Facebook是这么做的:
HTTP status:400
"error":
"type": "OAuthException",
"message": "Error validating access token."
Google是这么做的:
"error":
"errors": [
"domain": "global",
"reason": "authError",
"message": "Invalid Credentials", "locationType": "header", "location": "Authorization"
],
"code": 401,
"message": "Invalid Credentials"
第四步:用refresh token 换取access token
上面提到过了,access token生命周期较短,并且在用auth code换取access token时,授权服务器还会返回两个额外的字段:
- expires_in:这个字段是指access token的有效期,单位一般定义为秒
- refresh_token:长生命周期的refresh token,用来换取新的access token
在应用设计时有一点需要注意,客户端最好同时保存access_token和expires_in值,在调用api之前,客户端应该先拿expires_in与当前时间做比较,若当前时间大于过期时间,则说明access token已过期,需要重新换取新的access token。当然如果客户端不判断的话也可以,这样调用api时服务器会返回access_token过期错误信息,客户端需要再换取新的access token重新访问api。
下面是refresh_token换取access_token的示例:
请求接口地址:https://accounts.google.com/o/oauth2/token
请求参数:
- client_id: 240195362.apps.googleusercontent.com
- client_secret: hBMLD98Zi4wiqmiwmqDq
- grant_type: refresh_token
- refresh_token: $refreshToken
返回结果:
- access_token:新的access token
- expires_in: 新的access token的过期时间
- refresh_token: 新的refresh_token
撤销授权
有授权就有撤销授权。很多情况下需要撤销授权:
- 用户主动退出
- 用户修改密码
- 用户卸载APP
撤销授权的形式很多,如客户端可以通过主动删除存储在本地的acess_token和refresh_token。
授权服务器也可以提供撤销授权接口,如:
https://accounts.google.com/o/oauth2/revoke?token=ya29.AHES6ZSzF
当客户端调用授权服务器撤销接口时,token变失效了。
浏览器应用 (client-side application)授权流程(Implicit)
浏览器应用的特点是客户端就是浏览器或者运行在浏览器上面的一段js代码。这种应用的特点是用户可以直接看到源代码,所以授权服务器不能分配这种客户端client_secret秘钥。
对于这类应用的授权,Oauth协议采用的是四大授权流程中的implicit
流程。
implicit流程使用场景
- 临时授权
- 用户定期登录到api应用
- oauth客户端运行在浏览器端
- 浏览器是受信任的,并且有限地担心访问令牌会泄漏给不受信任的用户或应用程序
implicit授权流程
第一步:请求用户授权
首先要先获取授权服务地址,一般可以从api提供者的接入文档中找到这个地址。比如Google的第三方接入文档中提供的的用户授权地址是:
https://accounts.google.com/o/oauth2/auth?client_id=1&redirect_uri=https://photoviewer.saasyapp.com/ oauth_response.html&scope= https://www.google.com/m8/ feeds/&response_type=token
其实上面这个地址和authorizationCode流程中的地址是同一个,只不过参数不一样。
涉及到的参数有:
- client_id
- redirect_uri
- scope
- response_type:这里固定填
token
- state
从上面参数可以看出看,此流程和authorizationCode最大的区别是response_type的值是token。其实授权服务器也是根据response_type的值确定客户端用采用的是哪个流程,如果是code,则是authorizationCode流程,如果是code这是implicit grant flow流程。
具体用户是如何授权的呢?一般来说,第三方应用向授权服务器发送用户授权请求时,授权服务器会自动检查当前用户有没有登录(通过cookie机制),如果用户已登录,则直接弹出一个确认页面,让用户点击按钮企确认是否授权。若授权服务器检测到当前用户没有登录,则先会弹出登录框让用户进行登录,用户输入用户名密码登录之后再让用户确认是否授权。
第二步:从url中解析access token
当用户点击确认授权按钮之后,授权服务器会自动重定向当前请求到redirect_uri指定的url,并附带一个access token,如下面所示:
http://photoviewer.saasyapp.com/pv/ oauth2callback.html#access_token=ya29.AHES6ZSzX&token_type=Bearer&expires_in=3600
redirect_uri其实就是第三方应用地址,此时第三方应用就可以从redirect_uri中截取access_token,有了access token令牌,第三方应用便可以访问资源服务器api获取用户信息了。
第三步:调用api
Oauth协议调用api非常简单,所有Oauth流程调用api的方式都一样,直接在请求中带上access token即可,具体可以参考上面的AuthorizationCode流程中的介绍。
implicit流程的一些特点
Implicit
流程没有refresh token,所以一旦access token请求过期,就需要重新走一遍implicit整个流程。在实际操作中,如果access token已经过期,但当前用户还没有退出登录,第三方应用再重新申请access token时,授权服务器一般都会直接颁发access token无须再让用户确认,这样可以提高用户体验。
Password授权流程
Password流程更简单,顾名思义就是使用用户的用户名、密码换取access token。一般只有用户非常信任的应用才会使用这种流程,比如api提供者发布的应用就可以使用这种流程,移动App开发可以采用这种模式,因为api提供者资源服务器本身就属于移动APP。
Password流程图
Password授权流程
第一步:请求用户输入认证凭证(用户名密码)
这一步一般会弹出一个用户界面,让用户输入自己的用户名和密码。
第二步:用用户凭证换取access token
这一步和Authorization Code
流程中的authorization code 换取 access token 类似,通过请求授权服务器的token接口换取access token:
https://login.salesforce.com/services/oauth2/token
请求参数为:
- grant_tye: 填password,代表采用的是paasword流程
- client_id
- client_secret
- username:用户名
- password:用户密码
如果授权服务器认证用户凭证通过,便直接返回access token信息,如:
"id":"https://login.salesforce.com/id/00DU0000000Io8rMAC/005U0000000hMDCIA2”,
"issued_at":"1316990706988",
"instance_url":"https://na12.salesforce.com”,
signature":"Q2KTt8Ez5dwJ4Adu6QttAhCxbEP3HyfaTUXoNI=“,
"access_token":"00DU0000000Io8r!AQcKbNiJPt0OCSAvxU2SBjVGP6hW0mfmKH07QiPEGIX"
第三步:调用api
所有流程的调用api这一步都是一样的,调用api的时候附带上access token即可。
curl -d "q=SELECT+name+FROM+Account"\\
-H 'Authorization: Bearer 00DU0000000Io8r!AQcAQKJ.Cg1dCBCVHmx2.Iu3lroPQBV2P65_jXk’
"https://na12.salesforce.com/services/data/v20query"
ClientCredentials授权流程
ClientCredentials
是Oauth四大授权流程中最简单的一个流程。只需要用client_id和client_secret即可换取access token。这个流程只用来访问客户端拥有的资源而非用户拥有的资源,因为这个流程无须用户授权,只需要客户端的认证凭证。
比如你打算开发一个相册应用,并且你想把照片资源存放在阿里云的云服务器上,这个时候你就要用到阿里云的文件存取api。因为阿里云的文件存取api是给第三方应用使用的,跟用户无关,所以此种场景下就要用到ClientCredentials
流程。
ClientCredentials流程图
流程实例
我们以Facebook的APP登录为例子介绍一下这个流程
第一步:用应用的认证凭证换取access token
我们可以从Facebook官方api开发者文档找到app登录接口:
https://graph.facebook.com/oauth/access_token
接口参数为:
- grant_type:固定填写client_credentials,表示采用的是ClientCredentials流程
- client_id
- client_secret
若认证通过,Facebook的授权服务器会返回access token:
"access_token":"2016271111111117128396|8VG0riNauEzttXkUXBtUbw"
第二步:调用api
只需要在调用api时带上access token即可,下面是将access token放在请求参数中的例子:
https://graph.facebook.com/202627763128396/insights?access_token=2016271111111117128396|8VG0riNauEzttXkUXBtUbw"
当然最好把access token放在HTTP Authorization header
中:
Authorization: Bearer 8VG0riNauEzttXkUXBtUbw
上面示例中没有返回access token的过期时间,我们自己实现时可以返回一个过期时间,如果access token过期,应用重新获取一遍即可。
以上是关于开放授权协议:Oauth2.0的主要内容,如果未能解决你的问题,请参考以下文章