美文网首页
关于HTTP,Token,JWT,IDP,SSO,CAS那些事

关于HTTP,Token,JWT,IDP,SSO,CAS那些事

作者: TimLi_51bb | 来源:发表于2022-03-13 21:23 被阅读0次

    最近在项目中遇到了很多关于认证 & 授权的事情,状态不好的时候,时不时把自己绕进去了,需要记下来避免再犯错,由简单到难

    什么是认证&授权

    Authentication vs. Authorization

    It is easy to confuse authentication with another element of the security plan: authorization. While authentication verifies the user’s identity, authorization verifies that the user in question has the correct permissions and rights to access the requested resource. As you can see, the two work together. Authentication occurs first, then authorization.

    概念

    1.HTTP 身份验证

    HTTP 提供一个用于权限控制和认证的通用框架。最常用的HTTP认证方案是HTTP Basic authentication。本页介绍了通用的HTTP认证框架以及展示如何通过HTTP Basic authentication来限制权限访问您的服务器。

    通用的 HTTP 认证框架

    定义了一个 HTTP 身份验证框架,服务器可以用来针对客户端的请求发送 challenge,客户端则可以用来提供身份验证凭证。质询与应答的工作流程如下:服务器端向客户端返回 401(Unauthorized,未被授权的) 状态码,并在 WWW-Authenticate 首部提供如何进行验证的信息,其中至少包含有一种质询方式。之后有意向证明自己身份的客户端可以在新的请求中添加 Authorization 首部字段进行验证,字段值为身份验证凭证信息。通常客户端会弹出一个密码框让用户填写,然后发送包含有恰当的 Authorization 首部的请求。

    image.png

    步骤如下:

    1.当打开需要认证的页面时,会弹出一个对话框,要求输入用户名和密码


    image.png

    2.使用Fiddler监听请求,可以看到在未进行认证或认证失败的情况下,服务端会返回401 Unauthorized给客户端,并附带Challenge(质询),即在Response Header中添加WWW-Authenticate标头,浏览器识别到Basic后弹出对话框
    Realm表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码,用" "包括起来(截图中没有,但最好加上)。

    image.png

    3.输入正确的用户名和密码,认证成功后,浏览器会将凭据信息缓存起来,那么以后再进入时,无需重复手动输入用户名和密码。
    查看HTTP请求,可以看到Request Header中添加了Authorization标头,格式为:Authorization: <type> <credentials>

    • 类型为“Basic”
    • 凭证为“MTIzOjEyMw==”,是通过将“用户名:密码”格式的字符串经过的Base64编码得到的。而Base64不属于加密范畴,可以被逆向解码,等同于明文,因此Basic传输认证信息是不安全的


      image.png

    三、缺陷

    1.用户名和密码明文(Base64)传输,需要配合HTTPS来保证信息传输的安全。
    2.即使密码被强加密,第三方仍可通过加密后的用户名和密码进行重放攻击。
    3.没有提供任何针对代理和中间节点的防护措施。
    4.假冒服务器很容易骗过认证,诱导用户输入用户名和密码。

    Token

    Token的引入

    Token是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,Token便应运而生。

    Token的定义

    Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。

    使用Token的目的

    Token的目的是为了减轻服务器的压力,减少频繁的查询数据库,使服务器更加健壮。

    为什么要用 Token:

    Token 完全由应用管理,所以它可以避开同源策略
    Token 可以避免 CSRF 攻击(http://dwz.cn/7joLzx)
    Token 可以是无状态的,可以在多个服务间共享

    基于 Token 的身份验证方法

    1.客户端使用用户名跟密码请求登录
    2.服务端收到请求,去验证用户名与密码
    3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
    4.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
    5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
    6.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

    Token 时序图

    1.登陆


    image.png

    2.Token 过期,刷新 Token
    使用 Refresh Token,它可以避免频繁的读写操作。这种方案中,服务端不需要刷新 Token 的过期时间,一旦 Token 过期,就反馈给前端,前端使用 Refresh Token 申请一个全新 Token 继续使用。这种方案中,服务端只需要在客户端请求更新 Token 的时候对 Refresh Token 的有效性进行一次检查,大大减少了更新有效期的操作,也就避免了频繁读写。当然 Refresh Token 也是有有效期的,但是这个有效期就可以长一点了,比如,以天为单位的时间。


    image.png

    分离认证服务

    当 Token 无状态之后,单点登录就变得容易了。前端拿到一个有效的 Token,它就可以在任何同一体系的服务上认证通过——只要它们使用同样的密钥和算法来认证 Token 的有效性。就样这样:

    image.png

    当然,如果 Token 过期了,前端仍然需要去认证服务更新 Token:


    image.png

    JWT

    JWT 是Json Web Tokens的简称。用百度上面的解释讲,是目前流行的跨域认证解决方案,一种基于JSON的、用于在网络上声明某种主张的令牌(token)。

    JWT原理

    jwt验证方式是将用户信息通过加密生成token,每次请求服务端只需要使用保存的密钥验证token的正确性,不用再保存任何session数据了,进而服务端变得无状态,容易实现拓展。

    image.png

    JWT原理:服务端认证完之后,生成一个JSON对象返回个用户,后续客户端所有请求都会带上这个JSON对象,服务端依靠这个JSON来验证认定用户身份

    image.png image.png

    JWT组成

    image.png
    {
        "username": "vist",
        "role": "admin",
        "expire": "2020-11-06 15:14:20"
    }
    
    6gdfg7af816b907f2cc9acbe9c3b4625
    

    载荷
    载荷是数据的主体部分。一般使用base64编码。

    可以使用JWT官方推荐字段:
    iss: 签发者
    sub: 主题
    aud: 接收者
    exp(expires): 过期时间
    iat(issued at): 签发时间
    nbf(not before): 早于某个时间不处理
    jti(JWT ID): 唯一标识

    签名
    签名部分是对前两部分(头部,载荷)的签名,防止数据篡改。
    按下列步骤生成:
    1、先指定密钥(secret)
    2、把头部(header)和载荷(payload)信息分别base64转换
    3、使用头部(header)指定的算法加密

    HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), "密钥/盐");
    

    JWT的特点

    JWT更加简洁,更适合在HTML和HTTP环境中传递。,但更建议JWT建议使用HTTPS协议来传输代码。

    JWT适合一次性验证,如:激活邮件
    JWT适合无状态认证
    JWT适合服务端CDN分发内容
    相对于数据库Session查询更加省时

    跨域认证需要做Session共享,而使用了JWT则不需要。因为只要其他服务器只要是使用同一套算法,就可以做信息的校验。
    JWT默认不加密,使用期间不可取消令牌或更改令牌的权限

    token和jwt存在什么区别

    相同: 都是访问资源的令牌, 都可以记录用户信息,都是只有验证成功后
    区别:服务端验证客户端发来的token信息要进行数据的查询操作;JWT验证客户端发来的token信息就不用, 在服务端使用密钥校验就可以,不用数据库的查询。

    什么是IDP

    身份提供者 (IDP) 是一种数字服务,它创建和管理用户的数字身份和与之相关的身份属性。 IDP 使用这些身份向第三方服务提供商(如网站、Web 应用程序等)验证用户。

    IDP 允许用户将自己的身份带入他们的工作区,并使他们能够使用现有的一组凭据注册/登录到 Web 服务或应用程序,而不是为服务或应用程序创建新的凭据。

    我们大多数人都熟悉的身份提供者的一个流行示例是 Google。 “使用 Google 注册”或“使用 Google 登录”选项是 Google 作为我们正在登录或登录的服务的身份提供者的一个示例。 这样用户就可以使用他们的 Google ID 访问服务及其资源。

    什么是Keycloack

    Keycloak是为现代应用系统和服务提供开源的认证和授权访问控制管理。Keycloak 实现了OpenID、OAuth2.0、SAML单点登录协议,同时提供 LDAP 和 Active Directory 以及 OpenID Connect、SAML2.0 IdPs、Github、Google 等第三方登录适配功能,能够做到非常简单的开箱即用。提供的功能如下:

    1.Keycloak通过配置,可实现对不同身份认证服务的集成,通过这些身份认证服务登录应用

    2.在企业系统中有使用LDAP/AD管理用户,同样,Keycloak 提供了对LDAP/AD的集成方案,可以方便的同步用户。

    3.Keycloak提供了多种语言和不同平台的支持,支持标准协议,OpenID Connect, OAuth 2.0, and SAML等
    OpenID Connect
    Java,JBoss EAP,WildFly,Fuse,Tomcat,Jetty 9,Servlet Filter,Spring Boot,Spring Security,JavaScript,Node.js,C#,Python,Android,iOS,Apache HTTP Server

    SAML
    Java,JBoss EAP,WildFly,Tomcat,Jetty,Apache HTTP Server

    什么是SAML

    SAML即安全断言标记语言,英文全称是Security Assertion Markup Language。它是一个基于XML的标准,用于在不同的安全域(security domain)之间交换认证和授权数据。在SAML标准定义了身份提供者(identity provider)和服务提供者(service provider),这两者构成了前面所说的不同的安全域。 SAML是OASIS组织安全服务技术委员会(Security Services Technical Committee)的产品。

    SAML(Security Assertion Markup Language)是一个XML框架,也就是一组协议,可以用来传输安全声明。比如,两台远程机器之间要通讯,为了保证安全,我们可以采用加密等措施,也可以采用SAML来传输,传输的数据以XML形式,符合SAML规范,这样我们就可以不要求两台机器采用什么样的系统,只要求能理解SAML规范即可,显然比传统的方式更好。SAML 规范是一组Schema 定义。

    SAML流程的参与者包括Service Provider(SP)和Identity Provider(IDP)两个重要角色,且整个流程包括如下两个使用场景:
    • SP Initiated: 服务提供者主动发起
    • IDP Initiated: 身份认证服务器主动发起
    下面是大致的认证流程:

    image.png
    1. End User从浏览器中请求访问某SP:https://www.example.com
    2. https://www.example.com发现用户未登陆,则发起SAML的AuthnRequest请求至IDP, 用户浏览器跳转至IDP页面;
    3. IDP发现用户处于未登陆状态,重定向用户至IDP的登陆界面,请求用户进行身份验证
    4. 用户在登陆页面中进行身份认证, 通常情况下需要校验用户名和密码;
    5. IDP校验用户身份,若成功,则把包含着用户身份信息的校验结果,以SAML Reponse的形式,签名/加密发送给SP;
    6. SP拿到用户身份信息以后,进行签名验证/解密,拿到明文的用户身份信息,此时SP处于登陆状态,可以对用户提供服务。
      可以看到,在整个流程中,IDP是负责颁发用户身份,SP负责信任IDP颁发的用户身份, SP和IDP之间的信任关系是需要提前建立的,即SP和IDP需要提前把双方的信息预先配置到对方,通过证书信任的方式来建立互信。

    什么是OIDC

    OIDC 的全称是 OpenID Connect,是一套基于 OAuth 2.0 的认证 + 授权协议,用于用户身份认证,将用户数据安全地暴露给第三方。

    以上是 OAuth 2.0 的官方定义。我们举一个实际的例子,你在登录京东的时候,会发现在京东的登录框中有使用 QQ 登录、使用 QQ 登录的按钮,这些地方就是 OAuth 2.0 协议的用武之地。京东希望从 QQ 获取你的 QQ 用户数据,从而完成在京东的注册,这就需要数据的主人——你的授权。完成授权之后, QQ 会给京东一个 access_token,京东携带这个凭证,就能以你的名义,以及你授予此网站的权限(例如你授权京东能够访问你的个人信息而不是转账能力),访问你在 QQ 服务器上的数据,从而获取你的信息,在此过程中,你无须告诉京东你的 QQ** 账号和密码**,你输入账密信息的时候,是在腾讯的服务器完成的认证。
    OIDC 与 OAuth 2.0 相比,多了认证的能力。不但能够返回用户的 access_token,让第三方通过 access_token 调用用户授权过的接口(用户授权),还可以返回用户的 id_token,第三方可以将 id_token 用作用户身份标识(用户认证)。
    回到刚才的例子,京东获取到 QQ 颁发的 access_token (是一个随机字符串)之后确实能够获取到你的信息,但是如果不借助其他手段,是不具备用户身份认证功能的。而在 OIDC 协议中,获取 access_token 的同时,会返回一个 JWT 格式的 id_token,可直接用作身份标识,供第三方确认用户身份。

    OIDC 协议中的四个主体
    在介绍 OIDC 授权模式之前,先要明确四个主体:第三方应用、资源服务器、资源所有者、认证授权服务器。我们继续沿用前文京东与 QQ 登录的例子。

    第三方应用
    京东的角色是第三方应用。
    资源服务器
    QQ 的个人信息存放于资源服务器。
    资源所有者
    用户是 QQ 账号的所有者。
    认证授权服务器
    QQ 的授权服务器负责用户的身份认证和授权,管理第三方应用、受保护资源、资源所有者之间的关系

    image.png

    OIDC 的三种授权模式
    应用最为广泛的是授权码模式,此模式的交互过程如下图

    image.png
    1. 第三方应用访问认证服务器的授权链接。(用户在京东网站登录框点击使用 QQ 登录)
    2. 用户与认证服务器完成身份认证。(浏览器跳转到 QQ 授权页面,用户输入 QQ 号和密码)
    3. 认证服务器向第三方应用返回授权码 code。(QQ 服务器将用户的浏览器重定向,将授权码发送到京东服务器)
    4. 第三方应用携带授权码访问认证服务器的 token 接口。(京东服务器携带授权码与 QQ 服务器交互)
    5. 认证服务器返回 access_token 和 id_token 给第三方应用。(QQ 服务器返回 access_token 和 id_token 给京东服务器

    此后,第三方应用可以利用 access_token 到资源服务器获取用户的信息,完成在第三方应用的注册和登录业务,并可以将 id_token 作为用户的身份凭证,存放在前端。第三方应用的前端需要访问受保护的资源(例如用户账单信息、购物车)时需要携带 id_token,后端验证 id_token 合法性,核实用户身份之后,返回相关资源数据。

    Implicit Flow
    在隐式模式中,认证服务器的授权接口不会返回授权码 code,而是在与用户完成认证后返回 id_token 和 access_token,交互模式如下图所示:

    image.png
    1. 第三方应用访问认证服务器的授权链接。(用户在京东网站登录框点击使用 QQ 登录)
    2. 用户与认证服务器完成身份认证。(浏览器跳转到 QQ 授权页面,用户输入 QQ 号和密码)
    3. 认证服务器向第三方应用返回 id_token 和 access_token。(QQ 服务器将用户的浏览器重定向,将 id_token access_token 发送到京东前端页面)
      隐式模式比授权码模式简单,经常用于将 id_token、access_token 直接返回到前端,方便前端直接存储 id_token 用于证明用户身份。也需要前端自行将 access_token 发回后端,后端用于获取用户的详细信息,这增加了暴露 access_token 的风险。隐式模式不支持返回 refresh_token,即不能从后端刷新 access_token,登录一旦过期需要用户重新登录。

    Hybrid Flow
    混合模式是以上两种模式的组合,特点是能够在授权接口一次性获取到 code、id_token、access_token,一般 code 会与 id_token、access_token 混合出现,混合模式的交互如下图所示:

    image.png
    1. 第三方应用访问认证服务器的授权链接。(用户在京东网站登录框点击使用 QQ 登录)
    2. 用户与认证服务器完成身份认证。(浏览器跳转到 QQ 授权页面,用户输入 QQ 号和密码)
    3. 认证服务器向第三方应用返回 code、id_token 和 access_token。(QQ 服务器将用户的浏览器重定向,将 id_token access_token 发送到京东前端页面)
    4. 第三方应用携带授权码访问认证服务器的 token 接口。(京东服务器携带授权码与 QQ 服务器交互)
      认证服务器返回 access_token 和 id_token 给第三方应用。(QQ 服务器返回 access_token 和 id_token 给京东服务器)

    SSO 概念

    单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。SSO是指在多应用系统中,用户只需要在某一个应用上登录一次,就可以同时在所有相关又彼此独立的系统中共享登录态。即只登录一次,就能访问所有相互信任的应用系统,在其他所有系统中也都得到了授权而无需再次登录。另外用户也只需要退出一次,即可退出所有其他可信的服务。所以SSO包括单点登录与单点注销两部分。

    单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。SSO是指在多应用系统中,用户只需要在某一个应用上登录一次,就可以同时在所有相关又彼此独立的系统中共享登录态。即只登录一次,就能访问所有相互信任的应用系统,在其他所有系统中也都得到了授权而无需再次登录。另外用户也只需要退出一次,即可退出所有其他可信的服务。所以SSO包括单点登录与单点注销两部分。

    image.png

    根据上图,我们可以梳理出单点登录的请求执行流程(重点):

    比如用户访问系统1的受保护资源,结果系统1发现用户未登录,会先跳转到SSO认证中心,并将自己的地址作为参数,比如http://login.xxx.com/jump?target=http://系统1.com/xxx
    SSO认证中心发现用户未登录,则将用户引导到登录页面,并将系统1的地址作为参数带过去;
    用户输入用户名和密码,向SSO认证中心提交登录申请,并将系统1的地址作为参数带过去;
    SSO认证中心校验用户信息,校验成功后,会创建一个用户与SSO认证中心之间的会话,称之为全局会话,同时创建一个授权令牌;
    SSO认证中心带着令牌跳转回最初的请求地址(系统1);
    系统1拿到授权令牌后,接着去SSO认证中心校验令牌是否有效,并将系统1的地址作为参数带过去;
    SSO认证中心先校验令牌是否有效,正常则返回有效信息,并把系统1的信息注册进SSO授权中心;
    系统1使用该授权令牌创建出与用户的会话,称为局部会话,然后给用户返回受保护的资源;
    如果用户继续访问系统2的受保护资源,也会与SSO授权中心进行交互授权;
    比如系统2发现用户未登录,则跳转到SSO认证中心,并将自己的地址作为参数携带过去;
    如果SSO认证中心发现用户已登录,则跳转回系统2的地址,并带过去授权令牌;
    系统2拿到授权令牌,接着会去SSO认证中心校验授权令牌是否有效;
    SSO认证中心也会校验授权令牌,并返回有效信息,把系统2的信息也注册进行SSO授权中心;
    系统2使用该授权令牌创建一个与用户的局部会话,并返回受保护的资源。
    通过以上的SSO单点登录执行流程,我们可以得知,用户登录成功之后,会与SSO认证中心及各个子系统之间建立会话。用户与SSO认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过SSO认证中心。全局会话与局部会话有如下约束关系:

    局部会话存在,全局会话一定存在;
    全局会话存在,局部会话不一定存在;
    全局会话销毁,局部会话必须销毁。
    单点登录涉及到SSO认证中心与众多子系统,各子系统与SSO认证中心之间需要通信以交换令牌、校验令牌及发起注销请求,因而各子系统必须集成SSO客户端,SSO认证中心则是SSO服务端,整个单点登录过程实质是SSO客户端与服务端通信的过程。

    单独注销执行流程(重点)

    在多应用系统中,我们既然实现了单点登录,自然也要单点注销,即在一个子系统中注销后,所有子系统的会话都将被销毁。我们用下图来说明

    image.png

    SSO认证中心会一直监听全局会话的状态,一旦发现全局会话被销毁,监听器将通知所有注册系统执行注销操作。下面对上图进行简要说明:

    比如用户向系统1发起注销请求;
    系统1根据用户与系统1建立的局部会话id拿到授权令牌,接着系统1向SSO认证中心发起注销请求;
    SSO认证中心会先校验授权令牌是否有效,然后销毁全局会话,同时取出所有用此授权令牌注册的系统地址;
    SSO认证中心向所有注册系统发起注销会话的请求;
    各注册系统接收到SSO认证中心的注销请求,销毁局部会话;
    最后SSO认证中心会引导用户到登录页面。

    CAS 概念

    CAS(Central Authentication Service),即中心认证服务系统。在CAS系统中,分为CAS Server与CAS Client两部分,CAS Server是单点登录系统中负责验证的服务端,CAS Client是CAS Server登录态的客户端。

    Ticket Grantfng Ticke(TGT):这是用户登录后生成的票根,包含用户的认证身份、有效期等信息,存储于CAS Server中,类似于我们常见的服务器会话;

    Ticket Granted Cookie(TGC):这是存储在Cookie中的一段数据,类似于会话ID,用户与CAS Server进行交互时,帮助用户找到对应的TGT;

    Service Ticket(ST):这是CAS Server使用TGT签发的一张一次性票据,CAS Client 使用ST与CAS Server进行交互,以获取用户的验证状态。

    CAS单点登录的完整步骤如下:

    用户先通过浏览器访问CAS Client程序的某个页面,例如http://cas.client.com/me
    当CAS Client判断用户需要进行身份认证时,会携带service作为请求参数,并返回302状态码,指示浏览器重定向到CAS Server端,例如 http://cas.server.com/?service=http://cas.client.com/me.service,service是用户的原访问页面;
    然后浏览器利用 service 重定向到CAS Server;
    CAS Server 获取并校验用户cookie中携带的TGC,如果成功,则身份认证完成;否则将用户重定向到CAS Server 提供的登录页,例如 http://cas.server.com/login?service=http://cas.client.com/me,由用户输入用户名和密码,完成身份认证;
    如果用户已经登录过系统,那么CAS Server可以直接获取用户的TGC,并根据TGC找到TGT。如果是首次登录,则CAS Server 会首先生成TGT。每次验证时,CAS Server 会根据 TGT签发一个ST,并把ST拼接在service参数中,同时将相应的TGC设置到用户的cookie中(域为CAS Server),并返回302 状态码,指示浏览器重定向到 service,例如 http://cas.client.com/me?ticket=XXX
    浏览器存储TGC,并携带ST重定向到service;
    CAS Client取得ST(即请求参数中的ticket)后,会向CAS Server请求验证该ST的有效性;
    若CAS Server验证该ST是有效的,就告知CAS Client该用户有效,并返回该用户的信息。
    CAS Client在获取用户信息时,可以使用session的形式管理用户会话。后续的交互请求不再需要重定向到CAS Server,CAS Client直接返回用户请求的资源即可,整个流程如下图所示:

    image.png

    CAS 登陆简单版

    CAS单点登录| 两次前端跳转、一次后端验证
    CAS首次登录会经过两次前端跳转、一次后端验证。在应用系统端需要集成CasClient的jar包,把其中的filter配置到站点web.xml中,用于拦截请求、判断登录、发起跳转或发起验证等。在SSO服务器上部署CasServer的war包,需要配置用户数据源,根据需求修改登录页面。

    image.png

    第一次跳转:客户端访问应用系统,应用系统判断Session发现未登录,返回302跳转到sso登录页面,并传递service参数给sso,该service参数有两个作用:

    service一般传递应用系统url地址,用于sso认证通过后回跳到应用系统;
    service参数同时会被cas服务端的作为cas客户端的唯一标记记录下来,用于后期匹配相应的认证凭据;
    第二次跳转:浏览器显示登录页面,用户输入账号密码登录成功后,sso会返回302跳转回到原来请求的应用系统页面,并携带ticket参数,作为认证票据,同时通过Set-Cookie向浏览器记录TGT,(TGT的作用将在下一个应用系统需要登录的时候体现出作用,是避免重复登录的关键)

    一次后台验证:应用系统接收到带有ticket的请求后,从后台直接向sso服务器发起一个http请求,将service和ticket作为参数,用于验证ticket的有效性;如果ticket有效,sso服务器将返回该ticket对应的登录用户名。

    夏天来了!加油努力!推自己在往前一把!!

    image.png

    相关文章

      网友评论

          本文标题:关于HTTP,Token,JWT,IDP,SSO,CAS那些事

          本文链接:https://www.haomeiwen.com/subject/wudddrtx.html