多维系统下单点登录深入详解

  • 1. 从淘宝天猫的单点登录说起
    • 1.1 SSO单点登录
    • 1.2 淘宝天猫登录场景解析
  • 2. 单点登录之整体解决方案
    • 2.1 设计方案-Cookie
    • 2.2 设计方案-分布式Session
    • 2.3 设计方案-客户端令牌Token
    • 2.4 技术方案-CAS认证
    • 2.5 技术方案-OpenID认证
    • 2.6 技术方案-SAML2.0认证
    • 2.7 技术方案-OAuth2认证
  • 3. 单点登录之技术方案深入详解
    • 3.1 基于SAML实现的统一认证
      • 3.1.1 概述
      • 3.1.2 什么是断言(Assertions)
      • 3.1.3 工作流程
      • 3.1.4 授权机制
      • 3.1.5 应用场景
      • 3.1.6 AWS云服务接入方案
      • 3.1.7 阿里云接入方案
    • 3.2 基于OAuth实现的统一认证
      • 3.2.1 概述
      • 3.2.2 OAuth2角色
      • 3.2.3 OAuth2 协议流程
      • 3.2.4 OAuth2 授权码模式
      • OAuth2 隐式/简化模式
      • 3.2.6 OAuth2 密码模式
      • 3.2.7 OAuth2 客户端模式
      • 3.2.8 Spring Security OAuth设计
      • 3.2.9 增强Token技术解决方案
      • 3.2.10 JWT技术解决方案
  • 4. 单点登录之生产实践
    • 4.1 基于Cookie跨域与分布式Session的技术实践
    • 4.2 基于Token增强的微服务技术实践
      • 代码实现
        • 工具类
        • 认证服务
        • 用户信息服务接口
        • 异常组件
        • 网关服务
    • 4.3 基于JWT扩展信息的微服务技术实践
      • 代码实现
        • 认证服务
        • JWT增强实现类:
        • 用户服务
        • 增加获取JWT扩展信息的接口
        • 测试验证
  • 5.大佬资料传送门
    • SSO单点登录

1. 从淘宝天猫的单点登录说起

1.1 SSO单点登录

  • 概述 随着互联网大数据不断发展,应用服务的不断增多,单点登录越来越能够凸显其作用。单点 登录SSO(Single Sign On),顾名思义就是单个节点登录,全局使用。是目前最为流行的统一登录 解决方案。
  • 为什么使用? 目的就是为了快速实现用户认证,统一管理用户信息, 避免重复维护用户数据; 分离用户与业务数 据,让业务服务专注于业务功能的实现,让用户中心服务统一认证,减少频繁认证次数, 同时保 障数据的安全性。
  • 应用场景
  • 内部的服务统一认证与授权,比如电商网站, 内部的用户服务、订单服务、库存服务、资金 服务等,以用户服务作为认证服务中心,实现统一认证与授权。
  • 外部的第三方登录认证与授权,比如登录某个论坛网站, 可以采用FaceBook或者Google账号进行登录。
  • 云服务应用,比如使用阿里云的消息推送服务,但不想创建和管理用户,就可以采用基于 SAML协议实现SSO单点登录。

1.2 淘宝天猫登录场景解析

访问淘宝网站, 登录之后, 再访问天猫网站, 你会发现, 天猫也是处于登录状态,那么具体是如何实现的?

  • 登录技术方案分析 淘宝登录


    目前整个登录体系是以淘宝作为中心,天猫通过淘宝作鉴权登录。整个鉴权体系是采用跨域cookie + 分布式session作为解决方案:
  • 淘宝是如何解决Cookie跨域问题

,目前淘宝是采用如下方案做处理:
通过内嵌iframe,访问统一域名,实现Cookie信息共享,如果禁用Cookie,你会发现无法正常登
录;同时利用静态资源不受同源策略的限制,通过JSONP跨域方式来获取用户的登录状态。

Response会返回Token信息:

var userCookie=
{dnk:'',_nk_:'',_l_g_:'',ck1:'',tracknick:'',mt:'ci=0_0',l:'eBMMyMa4QmFJBq7p
BO5aourza77T3Idb4sPzaNbMiInca6BPO3JuhNQqw5H95dtjgtC3xetzm21B9dLHR3fRwxDDBTJb
WMu-
exvO.',uc1:'',t:'aa749f01717bd2e29ccacc35701ebef7',unb:'',cna:'y4PeFr/mbEoCA
XQZX0Z2u8bq',_tb_token_:'e6163b18b5154',version:'4.0.0'};window.TB &&
TB.Global && TB.Global.run && TB.Global.run();

淘宝是如何解决分布式Session管理问题呢? 为了解决此问题,淘宝专门推出两个重要产品:
第一个是tbsession, 基于Tair缓存体系实现的共享Session; 另一个是passcookie,解决不同域名之间Cookie同步的问题,上述的登录鉴权Cookie信息就是通过passcookie实现的统一管理。
淘宝是如何防范Session劫持?
CSRF/XSRF 攻击的原理,就是利用浏览器对嵌入资源不做限制的行为进行跨站请求伪造攻击, 比 如

  • SSO登录架构设计
  • SSO登录实现流程解析
  1. 用户进入淘宝登录页面,调用地址: https://login.taobao.com/newlogin/login.do
  2. 调用成功之后,同步Cookie,保存Token认证信息。
  3. 访问天猫网站,从Cookie里面拿取Token信息,采用jsonp方式,获取淘宝的登录状态:
  4. 如果不是从淘宝登录, 由天猫发起登录,会请求至淘宝登录页面, 登录完成之后写入Cookie信息, 再返回至天猫网站。

2. 单点登录之整体解决方案

2.1 设计方案-Cookie

  • 概述
    用户登录之后, 将认证信息存储至Cookie,当再次访问本服务或者访问其他应用服务时,直接从Cookie中传递认证信息,进行鉴权处理。

  • 问题

  • 如何保障Cookie内用户认证信息的安全性?
    第一, Cookie内不能存放用户名和密码等敏感信息, 可以生成一串Token进行替代;
    第二, 通过加密方式存储Cookie信息,并且采用https加密方式传输,设定Cookie有效期,在服务端设定Token的有效期,避免攻击者伪造用户身份。

  • 如何解决跨域问题?
    在实际应用中, 经常会存在各种服务需要鉴权处理, 但受浏览器同源策略限制,无法去正常操作Cookie数据, 解决方式有两种:
    第一种,采用iframe方式解决跨域问题, 实现Cookie共享,但要注意,父窗口获取子窗口在跨域下可以正常获取,子窗口后去父窗口仍会存在跨域问题, 这点在实现的时候要注意。
    第二种,采用JSONP方式实现跨域传输,这需要在服务端设置允许跨域请求,response.setHeader(“Access-Control-Allow-Origin”, “*”); 设置允许任何域名跨域访问,服务端返回数据时,再设置callback,才能完成跨域请求。

  • 跨域Cookie设计实现方案

2.2 设计方案-分布式Session

  • 概述
    大型应用服务无论是整体拆分,还是集群部署,都会涉及到统一会话问题,如何保障各服务节点都能够统一有效鉴权? 某个服务节点宕机,重启后如何恢复登录状态? 在Cookie禁用的情况下如何实现SSO? 由此产生了分布式Session设计方案。 分布式Session方案,实质是通过自定义的Session机制来处理用户的登录鉴权信息,实现单点登录。
  • 实现流程
  • 技术框架
    Spring Session : 它是目前主流的Session 管理解决方案,Spring Session 并非特定应用于HTTP, 它是一种广义的分布式统一Session,支持WebSocket和WebSession等,并且可以基于Redis、MongoDB等多种高性能缓存来实现。
    XXL-SSO: 它是一个分布式单点登录框架。只需要登录一次就可以访问所有相互信任的应用系统。
    拥有”轻量级、分布式、跨域、Cookie+Token均支持、Web+APP均支持”等特性。现已开放源代码,开箱即用。架构图:

2.3 设计方案-客户端令牌Token

  • 概述
    根据客户端身份信息由认证服务生成签名令牌,令牌中会包含基本的用户信息,客户端在请求资源服务时会附带令牌,资源服务根据加密协议在本地进行验证, 或者发送给认证服务端进行校验。
    它可以解决分布式会话的安全性问题,比如会话劫持,同时不需要集中统一维护session,能够做到无状态化处理。OAuth2和JWT都是基于令牌Token实现的认证方案。
  • 适用场景
    JWT (JSON Web Token) 是一个开放安全的行业标准,用于多个系统之间传递安全可靠的信息。它由三部分组成,头部(Header)、载荷(playload)与签名(Signature)。Token实质是一个无意义的UUID,需要服务端做记录与认证, 但JWT则赋予了用户的身份信息,可以采用自定义算法进行加密与解密,直接实现信息的传输交换。那具体适用于哪些场景?
  • 可以适用于微服务应用, 无论是内部服务节点的认证与授权, 或是令牌与API网关结合的认证。
  • 可以适用于开放式的API接口访问,比如前后分离API对接,第三方API接口对接等。
  • 实现流程

2.4 技术方案-CAS认证

  • 概述
    CAS(Central Authentication Service)是耶鲁大学的开源项目,宗旨是为web应用系统提供一种可靠的单点登录解决方案。CAS从安全性角度来考虑设计,用户在CAS输入用户名和密码之后通过ticket进行认证,能够有效防止密码泄露。CAS广泛使用于传统应用场景中,比如企业内部的OA,ERP等应用,不适用于微服务领域。
  • **设计实现流程
  • CAS代理认证
    有两个应用App1和App2,它们都是受Cas Server保护,请求它们时都需要通过Cas Server的认证。现需要在App1中以Http方式请求访问App2,显然该请求将会被App2配置的Cas的AuthenticationFilter拦截并转向Cas Server,Cas Server将引导用户进行登录认证,这样我们也
    就不能真正的访问到App2了。针对这种应用场景,Cas也提供了对应的支持。

代理认证具体流程:
App1先通过Cas Server的认证,然后向Cas Server申请一个针对于App2的proxy ticket,之后在访问App2时把申请到的针对于App2的 proxy ticket 以参数 ticket 传递过去。App2的 AuthenticationFilter 将拦截到该请求,发现该请求携带了 ticket 参数后将放行交由后续的Ticket Validation Filter处理。Ticket Validation Filter将会传递该ticket到Cas Server进行认证,显然该ticket是由Cas Server针对于App2发行的,App2在申请校验时是可以校验通过的,这样我们就可以正常的访问App2了。

2.5 技术方案-OpenID认证

  • 概述
    OIDC( OpenID Connect) 是属于是OAuth 2.0协议之上的简单身份层,用API进行身份交互,允许客户端根据授权服务的认证结果确认用户的最终身份,它支持包括Web、移动、JavaScript在内的所有客户端类型。它与OAuth的主要区别是在于, OpenID 只用于身份认证,例如允许一个账户登录多个网站;而OAuth可以用于授权,允许授权的客户端访问指定的资源服务。
  • 应用场景
    如果有独立账号体系,需要为外部提供统一认证服务, 可以采用OIDC,OIDC目前有很多企业在使用,比如Google的账号认证体系,Microsoft的账号体系也采用了OIDC。
  • 如何工作
    OAuth2提供了Access Token来解决授权第三方客户端访问受保护资源的问题;OIDC在这个基础上提供了ID Token来解决第三方客户端标识用户身份认证的问题。OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token使用JWT格式来包装,得益于JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了UserInfo的接口,用户获取用户的更完整的信息。
  • 工作流程
    术语解析:
  • EU(End User):代表终端用户。
  • RP(Relying Party): 指OAuth2中受信任的客户端。
  • OP(OpenID Provider):有能力提供EU认证的服务(比如OAuth2中的授权服务),为RP提供EU的身份认证信息.
  • ID Token:JWT格式的数据,包含EU身份认证的信息。
  • UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPS
  • 工作模式
  1. 默认模式/简化模式(Implicit Flow):如果是Web应用服务,其所有的代码都有可能被加载到浏览器暴露出来,无法保证终端client_secret的安全性,则采用默认模式。
  2. 授权码模式(Authentication Flow): 如果是传统的客户端应用,后端服务和用户信息是隔离的,能保证client_secret的不被泄露,就可以使用授权码模式流程。
  3. 混合模式(Hybrid Flow): 实质上是以上两种模式的融合,混合模式下ID Token通过浏览器的前端通道传递,而Access Token和Refresh Token通过后端获取,混合使用, 可以弥补两种模式的缺点,一般推荐使用混合模式。

2.6 技术方案-SAML2.0认证

  • 什么是SAML
    SAML 全称是 Security Assertion Markup Language。SAML是支持身份认证的协议,它可以通过支持XACML协议进行权限控制。SAML是基于XML实现的协议,较OAUTH来说较复杂些,但功能也十分强大,支持认证,权限控制和用户属性识别等。目前在云服务的接入使用比较广泛,作为重点内容, 在下面的章节做详细讲解。

2.7 技术方案-OAuth2认证

  • 什么是OAuth
    OAuth 2.0 是一个行业的标准授权协议,它的最终目的是为第三方应用颁发一个有时效性的令牌token,使得第三方应用能够通过该令牌获取相关的资源。它的主要作用可以实现登录认证与授权,常见的场景:比如第三方登录,当你要登录某个论坛,但没有账号,通过QQ 登录的过程就是采用 OAuth 2.0 协议, 通过OAuth2的授权,可以获取QQ头像等资源信息。OAuth2是目前应用最为广泛的认证授权协议,这是重点内容,在下面的章节做详细深入讲解

3. 单点登录之技术方案深入详解

3.1 基于SAML实现的统一认证

3.1.1 概述

SAML 2.0 用来在安全域中交换身份验证(Authentication)数据和 授权(Authorization)数据。
SAML 2.0基于XML协议,使用包含断言(Assertions)的安全令牌在SAML授权方(即身份提供者IdP) 和SAML消费方(即服务提供者SP)之间传递委托人(终端用户)的信息。
SAML 2.0 可以实现基于网络跨域的单点登录(SSO), 以便于减少向一个用户分发多个身份验证令牌的管理开销。

3.1.2 什么是断言(Assertions)

断言是一个包含了由SAML授权方提供的0到多个声明(statement)的信息包。SAML断言通常围绕一个主题生成。该主题使用声明。SAML 2.0规范定义了三种断言声明,详细信息如下:

  • 身份验证(Authentication):该断言的主题是在某个时间通过某种方式被认证。
  • 属性(Attribute):该言的主题和某种属性相关联。
  • 授权决策(Authorization Decision):该断言的主题被允许或者被禁止访问某个资源。

断言举例:

<?xml version="1.0"?>
<saml:Assertion xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" ID="_d71a3a8e9fcc45c9e9d248ef7049393fc8f04e5f75" Version="2.0" IssueInstant="2014-07-17T01:01:48Z"><saml:Issuer>http://idp.example.com/metadata.php</saml:Issuer><saml:Subject><saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">xiaosy@bw30.com</saml:NameID><saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"><saml:SubjectConfirmationData NotOnOrAfter="2024-01-18T06:21:48Z" Recipient="http://sp.example.com/demo1/index.php?acs" InResponseTo="ONELOGIN_4fee3b046395c4e751011e97f8900b5273d56685"/></saml:SubjectConfirmation></saml:Subject><saml:Conditions NotBefore="2014-07-17T01:01:18Z" NotOnOrAfter="2024-01-18T06:21:48Z"><saml:AudienceRestriction><saml:Audience>http://sp.example.com/demo1/metadata.php</saml:Audience></saml:AudienceRestriction></saml:Conditions><saml:AuthnStatement AuthnInstant="2014-07-17T01:01:48Z" SessionNotOnOrAfter="2024-07-17T09:01:48Z" SessionIndex="_be9967abd904ddcae3c0eb4189adbe3f71e327cf93"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement><saml:AttributeStatement><saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue></saml:Attribute><saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">test@example.com</saml:AttributeValue></saml:Attribute><saml:Attribute Name="eduPersonAffiliation" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic"><saml:AttributeValue xsi:type="xs:string">users</saml:AttributeValue><saml:AttributeValue xsi:type="xs:string">examplerole1</saml:AttributeValue></saml:Attribute></saml:AttributeStatement>
</saml:Assertion>

它主要是用来实现Web浏览器的单点登录。该断言包括一个身份验证断言 saml:AuthnStatement 和一个属性断言 saml:AttributeStatement ,SP将使用该属性断言实现访问控制。

3.1.3 工作流程

SAML 认证流程一般都会牵涉到两方:服务提供方(SP)和身份提供方(IdP),典型的 SP 有阿里云、腾讯云以及很多很多的 SaaS 服务;IdP 其实就是我们企业自己,因为用户目录在我们这里。

访问 SP 服务的时候,SP 会向 IdP 发送一个 SAML Request(具体是什么我们暂时不关心),请求 IdP判断用户身份。IdP 收到 SAML Request 后,可以通过某种手段对用户身份进行认证,如果已登录,可
以直接返回用户身份信息给 SP;如果未登录,可以弹出一个登录框,用户登录之后再将用户身份返回给 SP。SP 收到用户信息之后,再在自己的数据库里面找出对应的用户,然后以这个用户的身份访问 SP服务。

  1. 用户通过浏览器访问网站(SP),网站提供服务但是并不负责用户认证。
  2. SP 向 IDP 发送了一个 SAML 认证请求,同时 SP 将 用户浏览器 重定向到 IDP。 3. IDP 在验证完来自 SP 的合法请求, 在浏览器中呈现登陆表单让用户填写用户名与密码信息,进行登陆。
  3. 用户登陆成功, IDP 会生成一个包含用户信息的 SAML token(SAML token 又称为 SAMLAssertion,本质上是 XML 节点)。IDP 向 SP 返回 token,并且将 用户重定向 到 SP。 5. SP 对拿到的 token 进行验证,并从中解析出用户信息,例如用户是谁以及用户的权限有哪些。此时可以根据这些授权信息允许用户访问我们网站的内容。

3.1.4 授权机制

SAML 只是认证协议,自身并不提供授权功能, 可以通过XACML实现授权。
XACML 是可扩展访问控制标记语言,以XML的形式描述策略语言和授权决策请求/响应,提供管理授权决策的语法。
SAML 和 XACML 结合实现权限访问控制,映射关系:

SAML 和 XACML 结合控制应用模型:

该模型是一个完整的访问控制体系结构,包含身份验证和授权两部分。身份验证可 以接受来自其它系统的各种安全令牌,包括 SAML 断言,对请求主体进行验证并产生 SAML 身份验证断言。只要合作的第三
方服务联合信任,就可以实现 服务的安全交互以及用户 的单点登录。
模型的授权基于 PMI 统一授权管理体系,授权系统向 AA(属性权威机构)请 求关于 Web 服务请求主体的属性信息,AA 实现 SAML 接口,返回 SAML 属性断言。
模型使用统一的策略语言 XACML,由 SAML 为其提供底层传输机制,适用于各种类型的访问 控制系统。策略可以被不同的应用使用,使策略的管理更加容易。

3.1.5 应用场景

目前SAML广泛应用于云服务的认证,比如阿里云、AWS和腾讯云等,在云服务上面维护统一的用户信息进行身份认证。SAML认证一般分为两部分,用户池与角色身份池。
用户池可以让应用程序接入,也可以通过第三方身份提供商 (IdP) ,对用户身份进行认证。
角色身份池可以通过凭证来控制访问云服务资源,比如阿里云推送服务,Amazon S3 和 DynamoDB等。
以AWS的Amazon Cognito为例,简单介绍下它的应用:
通过SAML协议验证用户身份,然后授予用户访问其他 AWS 服务的权限

  • 在第一步中,您的应用程序用户通过用户池登录,并在成功进行身份验证后收到用户池令牌。
  • 接下来,您的应用程序通过用户池令牌交换 AWS 凭证。
  • 最后,您的应用程序用户可以使用这些 AWS 凭证来访问其他 AWS 服务(如 Amazon S3 或DynamoDB)。

3.1.6 AWS云服务接入方案

  • 用户池进行身份验证

    用户使用用户池进行身份验证。应用程序用户可以通过用户池直接登录,也可以通过第三方身份提供商 (IdP) 联合。用户池管理从通过 Facebook、Google、Amazon 和 Apple 进行的社交登录返回的以及从 OpenID Connect (OIDC) 和 SAML IdP 返回的令牌的处理开销。

成功进行身份验证后, Web 或移动应用程序将收到来自 Amazon Cognito 的用户池令牌。可以使用这些令牌检索允许的应用程序访问其他 AWS 服务的 AWS 凭证,也可以选择使用它们来控制对您的服务器端资源或 Amazon API Gateway 的访问。

  • 用户池访问服务器端资源

用户池登录后,Web 或移动应用程序将收到用户池令牌。可以使用这些令牌控制对服务器端资源的访问。可以创建用户池组来管理权限以及表示不同类型的用户。

  • 用户池和身份池访问云服务

用户池登录认证成功之后,获取返回的令牌,再通过令牌换取身份池的信息,拿去身份池信息就可以访问其他的云服务资源。

  • 支持第三方进行身份验证并使用身份池访问云服务

身份池需来自第三方身份提供商,进行身份验证之后, 返回用户的 IdP 令牌。再通过令牌交换获取云服务的身份池信息,身份池将授予可用来访问其他云服务的临时凭证

更多资料参照官方文档:
Amazon Cognito 教程

3.1.7 阿里云接入方案

阿里云支持基于SAML 2.0的SSO(Single Sign On,单点登录),也称为身份联合登录。
阿里云提供以下两种基于SAML 2.0协议的SSO方式:
用户SSO:阿里云通过IdP颁发的SAML断言确定企业用户与阿里云RAM用户的对应关系 。企业用户登录后,使用该RAM用户访问阿里云。
角色SSO:阿里云通过IdP颁发的SAML断言确定企业用户在阿里
云上可以使用的RAM角色。企业用户登录后,使用SAML断言中指定的RAM角色访问阿里云。请参见进行角色SSO。用户SSO

  • 用户SSO

    当管理员在完成用户SSO的相关配置后,可以通过以下流程来实现用户SSO。 1. Alice使用浏览器登录阿里云,阿里云将SAML认证请求返回给浏览器。
  1. 浏览器向IdP转发SAML认证请求。
  2. IdP提示Alice登录,并在Alice登录成功后生成SAML响应返回给浏览器。
  3. 浏览器将SAML响应转发给SSO服务。
  4. SSO服务通过SAML互信配置,验证SAML响应的数字签名来判断SAML断言的真伪,并通过SAML断言的NameID元素值,匹配到对应阿里云账号中的RAM用户身份。
  5. SSO服务向浏览器返回控制台的URL。
  6. 浏览器重定向到阿里云控制台。

角色SSO

  1. 企业员工Alice可登录到阿里云,使用浏览器在IdP的登录页面中选择阿里云作为目标服务。
  2. IdP生成一个SAML响应并返回给浏览器。
  3. 浏览器重定向到SSO服务页面,并转发SAML响应给SSO服务。
  4. SSO服务使用SAML响应向阿里云STS服务请求临时安全凭证,并生成一个可以使用临时安全凭证登录阿里云控制台的URL。
  5. SSO服务将URL返回给浏览器。
  6. 浏览器重定向到该URL,以指定角色身份登录到阿里云控制台。
    更多资料参照官方文档:
    阿里云SSO

3.2 基于OAuth实现的统一认证

3.2.1 概述

OAuth2 实质是为第三方应用颁发一个具有时效性的Token令牌,使其他服务或第三方应用能够通过令牌获取相关资源。 常见的场景: 比如进入某个网站没有账号信息, 但可以通过QQ、微信、支付宝等账号进行登陆, 在这个登陆过程中采用的就是Oauth2协议; OAUTH2不仅支持认证,还具备授权功能, 比如通过QQ登录获取用户头像,基本资料等。

3.2.2 OAuth2角色

  • resource owner : 资源所有者,具备访问该资源的实体, 如果是某个人, 被称为end-user。
  • resources server: 资源服务器,受保护的资源服务器, 具备提供资源能力, 如订单服务, 商品 服务等。
  • client: 客户端,这并不是指用户, 而是对资源服务器发起请求的应用程序,比如前后分离项目, 前端服务访问管理接口,访问后台业务功能接口。
  • authorization server: 授权服务器, 能够给客户端颁发令牌, 这个就是我们上面所讲的统一认证 授权服务器。
  • user-agent: 用户代理, 作为资源所有者与客户端沟通的工具, 比如APP, 浏览器等。

3.2.3 OAuth2 协议流程

OAuth2包含四种授权模式:

  1. 授权码模式;
  2. 隐式/简化授权模式;
  3. 密码模式;
  4. 客户端模式。

  1. Resource Owner 与 Client 之间 , 资源所有者向Client发起认证请求, Client再返回认证授权信息。
  2. Client 收到 Resource Owner 的认证请求后, 会去Authorization Server 申请访问令牌,Authorization Server会让Client 进行认证, 通过之后会返回Access Token。
  3. Client 拿到 Authorization Server 的 Acceess Token , 访问Resource Server,Resource Server验证之后, 返回被保护的资源信息。
  4. Resource Server 可以通过JWT在本地进行验证, 也可以访问 Authorization Server, 对Client 的请求的合法性进行验证。

3.2.4 OAuth2 授权码模式

  1. 客户端携带 client_id, scope, redirect_uri, state 等信息引导用户请求授权服务器的授权端点下发code。
  2. 授权服务器验证客户端身份,验证通过则询问用户是否同意授权(此时会跳转到用户能够直观看到的授权页面,等待用户点击确认授权)。
  3. 假设用户同意授权,此时授权服务器会将 code 和 state(如果客户端传递了该参数)拼接在redirect_uri 后面,以302(重定向)形式下发 code。
  4. 客户端携带 code, redirect_uri, 以及 client_secret 请求授权服务器的令牌端点下发access_token。
  5. 授权服务器验证客户端身份,同时验证 code,以及 redirect_uri 是否与请求 code 时相同,验证通过后下发 access_token,并选择性下发 refresh_token,支持令牌的刷新。

示例:

  1. 授权请求:
response_type=code // 必选项
&client_id={客户端的ID} // 必选项
&redirect_uri={重定向URI} // 可选项
&scope={申请的权限范围} // 可选项
&state={任意值} // 可选项
  1. 授权响应参数
code={授权码} // 必填
&state={任意文字} // 如果授权请求中包含 state的话那就是必填
  1. 令牌请求:
grant_type=authorization_code // 必填
&code={授权码} // 必填 必须是认证服务器响应给的授权码
&redirect_uri={重定向URI} // 如果授权请求中包含 redirect_uri 那就是必填
&code_verifier={验证码} // 如果授权请求中包含 code_challenge 那就是必
填
  1. 令牌响应:
"access_token":"{访问令牌}", // 必填
"token_type":"{令牌类型}", // 必填
"expires_in":{过期时间}, // 任意
"refresh_token":"{刷新令牌}", // 任意
"scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填

OAuth2 隐式/简化模式

  1. 资源拥有者(用户)通过代理(WEB浏览器)访问客户端程序,发起简化模式认证。
  2. 客户端(Client)向认证服务器(Auth Server)发起请求, 此时客户端携带了客户端标识(client_id)和重定向地址(redirect_uri)。
  3. 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。

示例: 1. 授权请求

response_type=token // 必选项
&client_id={客户端的ID} // 必选项
&redirect_uri={重定向URI} // 可选项
&scope={申请的权限范围} // 可选项
&state={任意值} // 可选项
  1. 授权响应参数:
&access_token={令牌信息} // 必填
&expires_in={过期时间} // 任意
&state={任意文字} // 如果授权请求中包含 state 那就是必填
&scope={授权范围} // 如果请求和响应的授权范围不一致就必填

思考:为什么要有授权码和简化模式?看完这两种模式, 可能会有些疑问, 为什么要这么麻烦, 直接一次请求返回TOKEN不就可以吗?
我们可以看出, 两者主要差别, 是少了code验证环节, 直接返回token了, code验证是客户端与认证服务器在后台进行请求获取, 代理是获取不到TOKEN的, 如果缺少这个环节, 直接返回TOKEN, 相当于直接暴露给所有参与者, 存在安全隐患, 所以简化模式,一般用于信赖度较高的环境中使用。

3.2.6 OAuth2 密码模式

  1. 资源拥有者直接通过客户端发起认证请求。
  2. 客户端提供用户名和密码, 向认证服务器发起请求认证。
  3. 认证服务器通过之后, 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。

示例: 1. 令牌请求

grant_type=password // 必填
&username={用户ID} // 必填
&password={密码} // 必填
&scope={授权范围} // 任意 
  1. 令牌响应:
"access_token":"{访问令牌}", // 必填
"token_type":"{令牌类型}", // 必填
"expires_in":"{过期时间}", // 任意
"refresh_token":"{刷新令牌}", // 任意
"scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填

此模式简化相关步骤, 直接通过用户和密码等隐私信息进行请求认证, 认证服务器直接返回token,这需要整个环境具有较高的安全性。

3.2.7 OAuth2 客户端模式

  1. 此模式最为简单直接, 由客户端直接发起请求。
  2. 客户端与服务器信赖度较高, 服务端根据请求直接认证返回token信息。
  3. 客户端(Client)拿到令牌 token 后就可以向第三方的资源服务器请求资源了。
    这种模式一般在内部服务之间应用, 授权一次, 长期可用, 不用刷新token。

示例: 1. 令牌请求:

grant_type=client_credentials // 必填
client_id={客户端的ID} // 必填
client_secret={客户端的密钥} // 必填
&scope={授权范围} // 任意
  1. 令牌响应:
"access_token":"{访问令牌}", // 必填
"token_type":"{令牌类型}", // 必填
"expires_in":"{过期时间}", // 任意
"scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填

3.2.8 Spring Security OAuth设计

整体设计结构:

UML类图:

3.2.9 增强Token技术解决方案


优势与应用场景
基于Token的鉴权方案,实现方式有多种,增强Token属于其中一种,为什么要采用增强Token方式,它能够解决怎样的问题? 普通Token认证方式,没有附带必要的用户信息,如果要查询,需要再次调用OAuth2的用户资料认证接口,会增加传输开销;JWT虽然能够附带一定用户信息,但受限于长度,存储空间有限; 如果既要保障性能,又要求能够存储一定的信息,就可以采用增强Token方案,它是将信息存储至Redis缓存中,作为资源服务,接收到Token之后, 可以直接从Redis中获取信息。
它可以适用于微服务架构下,有一定用户信息要求的场景,比如订单服务、资金服务需要获取用户的基本资料,但如果是跨IDC,跨区域,需要暴露外网的情况下,不推荐采用此方案,因为需要保障数据的安全性。

3.2.10 JWT技术解决方案

JWT认证流程

JWT应用场景:

  1. 认证 Authentication;
  2. 授权 Authorization // 注意这两个单词的区别;
  3. 联合识别;
  4. 客户端会话(无状态的会话);
  5. Restful Api 无状态认证。

JWT缺陷:

  1. 更多的空间占用。如果将存在服务端session中的各类信息都放在JWT中保存在客户端,可能造成JWT占用更大空间,需要考虑cookie的空间限制因素,如果放在Local Storage,则可能受到XSS攻击。
  2. 更不安全。这里是特指将JWT保存在Local Storage中,然后使用Javascript取出后作为HTTPheader发送给服务端的方案。在Local Storage中保存敏感信息并不安全,容易受到跨站脚本攻击,跨站脚本(Cross site script,简称xss)是一种“HTML注入”,由于攻击的脚本多数时候是跨域的,所以称之为“跨域脚本”,这些脚本代码可以盗取cookie或是Local Storage中的数据( XSS攻 击的原理解释)。
  3. 无法作废已颁布的令牌。所有的认证信息都在JWT中,由于在服务端是无状态,即使你知道了某个JWT被盗取了,也没有办法将其作废。在JWT过期之前,除非主动增加过期接口,否则无法处理。
  4. 续签问题。传统 session请求时是可以自动续期,payload之中有一个exp过期时间参数,它可以代表JWT的时效性,但JWT自身设计并没有考虑续签问题,因为payload是参与签名处理,如果exp过期时间被修改,那整个JWT串就会产生变化,所以JWT原生并不支持续签。

JWT应用优化方案:

  1. 针对安全性问题: 可以使用Cookie存储, 并设置HttpOnly=true,只能由服务端保存以及通过自动回传的cookie取得JWT,以便防御XSS攻击; 在JWT载体中加入一个随机值作为CSRF令牌,服务端将令牌也保存在Cookie中,前端可以取得该令牌并在请求时作为HTTP header头部信息传递,服务端在认证时,从JWT取出CSRF令牌和HEADER中的令牌做比对,从而防止CSRF的攻击。
  2. 续签问题: 通过Token的Refresh机制来实现,需要对JWT的传递做统一封装,客户端再开辟一个线程定期检测有效期,临近过期时重新刷新Tokens,进行全局更新。JWT扩展知识

JWT扩展知识:

  • JWS(JSON Web Signature):其结构就是在JWT的基础上,在头部声明签名算法,并在最后添加上签名。创建签名,是保证jwt不能被他人随意篡改。为了完成签名,除了用到header信息和payload信息外,还需要算法的密钥,也就是secret。当利用非对称加密方法的时候,这里的secret就是为私钥。
  • JWE(JSON Web Encryption):它能够保护数据不被第三方查看,JWT是通过签名来验证数据来源的合法性,但载体信息只是通过Base64编码,不能严格保障数据的安全性,通过JWE,能够使JWT变得更为安全。

JWE数据组成结构:

4. 单点登录之生产实践

4.1 基于Cookie跨域与分布式Session的技术实践

  1. XXL-SSO整体架构:
  2. 实现原理剖析:
    首次请求:

    第二次请求

    跨域请求

    注销流程

4.2 基于Token增强的微服务技术实践

  1. 整体实现流程
    采用密码模式,基于Token增强的微服务应用实现方案

代码实现

工具类

public class GlobalConstants {/*** 缓存 -- 用户信息前缀*/public static final String OAUTH_KEY_STOCK_USER_DETAILS = "oauth:stock:user_details";/*** 缓存 --- 客户端信息前缀*/public static final String OAUTH_KEY_CLIENT_DETAILS = "oauth:client:details";/*** 缓存 -- 用于tokenstore的存取前缀*/public static final String OAUTH_PREFIX_KEY = "oauth";/*** 缓存 -- 用于tokenstore的存取前缀*/public static final String OAUTH_CLIENT_CREDENTIALS = "client_credentials";/*** 缓存 -- 用户ID前缀*/public static final String OAUTH_DETAILS_USER_ID = "user_id";/*** 缓存 -- 用户名称前缀*/public static final String OAUTH_DETAILS_USERNAME = "user_name";/*** 缓存 -- 用户登录信息*/public static final String OAUTH_DETAILS_LOGIN_INFO = "login_info";}

实体类

@Data
@Entity
@Table(name = "t_trade_user")
public class TradeUser extends BaseEntity {/*** 用户编号*/private String userNo;/*** 用户名称*/private String name;/*** 用户密码*/private String userPwd;/*** 电话号码*/private String phone;/*** 公司ID*/private Long companyId;/*** 邮箱*/private String email;/*** 地址*/private String address;/*** 最近一次用户登陆IP*/private String lastLoginIp;/*** 最近一次登陆时间*/private Date lastLoginTime;/*** 状态(0:有效, 1:锁定, 2:禁用)*/private int status;/*** 创建时间*/private Date craeteTime;}

认证服务

认证服务配置
AuthorizationServerConfig

import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.DefaultAuthenticationKeyGenerator;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** <p>Description: </p>* @date * @author * @version 1.0* <p>Copyright:Copyright(c)2020</p>*/
@Configuration
@EnableAuthorizationServer
@Log4j2
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {/*** 数据源配置*/@Autowiredprivate DataSource dataSource;/*** 用户认证信息鉴权的实现类*/@Autowiredprivate UserDetailsService authStockUserDetailService;/*** 认证服务管理器*/@Autowiredprivate AuthenticationManager authenticationManager;/*** Redis缓存服务*/@Autowiredprivate RedisConnectionFactory redisConnectionFactory;/*** t_oauth_client_details 表的字段,不包括client_id、client_secret*/String CLIENT_FIELDS = "client_id, client_secret, resource_ids, scope, "+ "authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, "+ "refresh_token_validity, additional_information, autoapprove";/*** JdbcClientDetailsService 查询语句*/String BASE_FIND_STATEMENT = "select " + CLIENT_FIELDS+ " from t_oauth_client_details";/*** 默认的查询语句*/String DEFAULT_FIND_STATEMENT = BASE_FIND_STATEMENT + " order by client_id";/*** 按条件client_id 查询*/String DEFAULT_SELECT_STATEMENT = BASE_FIND_STATEMENT + " where client_id = ?";/*** Redis 缓存配置* @return*/@Beanpublic RedisTemplate<String, Object> stockRedisTemplate() {RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// key编码类型, 采用StringredisTemplate.setKeySerializer(new StringRedisSerializer());// hashkey编码类型, 采用StringredisTemplate.setHashKeySerializer(new StringRedisSerializer());// value值得编码类型, 采用JDK序列化处理redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());// hashvalue值得编码类型, 采用JDK序列化处理redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());// 设置Redis的连接工厂配置redisTemplate.setConnectionFactory(redisConnectionFactory);return redisTemplate;}/*** 自定义Client查询,可以修改表名, 字段等* @param clients*/@Override@SneakyThrowspublic void configure(ClientDetailsServiceConfigurer clients) {AuthClientDetailService clientDetailsService = new AuthClientDetailService(dataSource);clientDetailsService.setSelectClientDetailsSql(DEFAULT_SELECT_STATEMENT);clientDetailsService.setFindClientDetailsSql(DEFAULT_FIND_STATEMENT);clients.withClientDetails(clientDetailsService);}/*** 防止申请token时出现401错误* @param oauthServer*/@Overridepublic void configure(AuthorizationServerSecurityConfigurer oauthServer) {oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()").allowFormAuthenticationForClients();}/*** 认证服务配置* @param endpoints*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore()).tokenEnhancer(tokenEnhancer()).userDetailsService(authStockUserDetailService).authenticationManager(authenticationManager).reuseRefreshTokens(false);}/*** TokenStore实现方式, 采用Redis缓存* @return*/@Beanpublic TokenStore tokenStore() {RedisTokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);tokenStore.setPrefix(GlobalConstants.OAUTH_PREFIX_KEY);tokenStore.setAuthenticationKeyGenerator(new DefaultAuthenticationKeyGenerator() {@Overridepublic String extractKey(OAuth2Authentication authentication) {return super.extractKey(authentication);}});return tokenStore;}/*** token增强处理, 支持扩展信息* @return TokenEnhancer*/@Beanpublic TokenEnhancer tokenEnhancer() {return (accessToken, authentication) -> {try {if (GlobalConstants.OAUTH_CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {return accessToken;}// 通过MAP 存储附加的信息final Map<String, Object> additionalInfo = new HashMap<>(16);OAuthTradeUser authTradeUser = (OAuthTradeUser) authentication.getUserAuthentication().getPrincipal();if (null != authTradeUser) {TradeUser tradeUser = authTradeUser.getTradeUser();// 需要扩充增加的用户附带信息additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USER_ID, tradeUser.getId());additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USERNAME, tradeUser.getName());additionalInfo.put(GlobalConstants.OAUTH_DETAILS_LOGIN_INFO, tradeUser.getEmail() + "|" + tradeUser.getAddress());additionalInfo.put("active", true);}// 将附加的信息记录保存, 形成增强的TOKEN((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);} catch (Exception e) {log.error(e.getMessage(), e);}return accessToken;};}
}

用户信息服务接口

AuthStockUserDetailServiceImpl

import com.itcast.bulls.stock.trade.oauth.repository.TradeUserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;/*** <p>Description: </p>* @date * @author * @version 1.0* <p>Copyright:Copyright(c)2020</p>*/
@Service("authStockUserDetailService")
public class AuthStockUserDetailServiceImpl implements UserDetailsService {/*** 用户的数据层接口*/@Autowiredprivate TradeUserRepository tradeUserRepository;/*** 缓存管理接口*/@Autowiredprivate CacheManager cacheManager;/*** 根据用户账号获取用户对象接口* @param username* @return* @throws UsernameNotFoundException*/@Overridepublic UserDetails loadUserByUsername(String userNo) throws UsernameNotFoundException {// 1. 从缓存中查找用户对象Cache cache = cacheManager.getCache(GlobalConstants.OAUTH_KEY_STOCK_USER_DETAILS);if(null != cache && null != cache.get(userNo)) {return (UserDetails)cache.get(userNo).get();}// 2. 如果缓存未找到, 查询数据库TradeUser tradeUser = tradeUserRepository.findByUserNo(userNo);if(null == tradeUser) {throw new UsernameNotFoundException(userNo + " not valid! ");}// 3. 对用户信息做封装处理UserDetails userDetails = new OAuthTradeUser(tradeUser);// 4. 将封装的用户信息放入到缓存当中cache.put(userNo, userDetails);return userDetails;}
}

这是Spring Security 提供的用户信息接口, 采用OAUTH的密码模式, 需要实现该接口的loadUserByUsername方法,为提升性能, 这里我们加入了Spring Cache缓存处理。

  • 自定义用户信息: OAuthTradeUser
public class OAuthTradeUser extends User {private static final long serialVersionUUID = -1L;/*** 业务用户信息*/private TradeUser tradeUser;public OAuthTradeUser(TradeUser tradeUser) {// OAUTH2认证用户信息构造处理super(tradeUser.getUserNo(), tradeUser.getUserPwd(), (tradeUser.getStatus() == 0 ? true : false),true, true, (tradeUser.getStatus() == 0 ? true : false), Collections.emptyList());this.tradeUser = tradeUser;}public TradeUser getTradeUser() {return tradeUser;}
}
  • 客户端信息服务接口
    AuthClientDetailService
public class AuthClientDetailService extends JdbcClientDetailsService {public AuthClientDetailService(DataSource dataSource) {super(dataSource);}/*** 重写原生方法支持redis缓存** @param clientId* @return* @throws InvalidClientException*/@Override@Cacheable(value = GlobalConstants.OAUTH_KEY_CLIENT_DETAILS, key = "#clientId", unless = "#result == null")public ClientDetails loadClientByClientId(String clientId) {return super.loadClientByClientId(clientId);}
}

这是OAUTH内置的客户端信息, 重新它是为了实现缓存, 减少数据库查询。

  • 用户服务

认证配置ResourceSecurityConfigurer

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.RestTemplate;import java.io.IOException;/*** <p>Description: </p>* @date * @author * @version 1.0* <p>Copyright:Copyright(c)2020</p>*/
@Primary
@Order(90)
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceSecurityConfigurer implements ResourceServerConfigurer {@Autowiredprivate RemoteTokenServices remoteTokenServices;@Autowiredprivate RestTemplate restTemplate;/*** 远程调用, 采用RestTemplate方式* @param resources* @throws Exception*/@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {remoteTokenServices.setRestTemplate(restTemplate);resources.tokenServices(remoteTokenServices);}@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("test123");return converter;}/*** 资源服务的安全配置* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/user/**").authenticated().and().formLogin().loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/home");}/*** RestTemplate配置* @return*/@Bean@Primary@LoadBalancedpublic RestTemplate lbRestTemplate() {RestTemplate restTemplate = new RestTemplate();restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {super.handleError(response);}}});return restTemplate;}}

用户服务为资源服务, 认证采用RestTemplate调用方式。 资源服务一定要开启
@EnableResourceServer注解, @EnableGlobalMethodSecurity为方法级别安全控制。

  • 提供获取用户增强信息接口
    StockUserController
import com.itcast.bulls.stock.common.exception.ComponentException;
import com.itcast.bulls.stock.entity.user.TradeUser;
import com.itcast.stock.common.web.vo.ApiRespResult;
import com.itcast.trade.bulls.stock.user.service.IStockUserService;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.Map;/*** <p>Description: </p>* @date * @author * @version 1.0* <p>Copyright:Copyright(c)2020</p>*/
@RestController()
@RequestMapping("/user")
@Log4j2
public class StockUserController extends BaseController{@Autowiredprivate IStockUserService stockUserService;/*** 用户登陆接口* @param userNo* @param userPwd* @return*/@RequestMapping("/userLogin")public ApiRespResult userLogin(@RequestParam("userNo")String userNo, @RequestParam("userPwd") String userPwd) {ApiRespResult  result = null;try {// 用户登陆逻辑处理TradeUser tradeUser = stockUserService.userLogin(userNo, userPwd);result = ApiRespResult.success(tradeUser);}catch(ComponentException e) {log.error(e.getMessage(), e);result = ApiRespResult.error(e.geterrorCodeEnum());}catch(Exception e) {log.error(e.getMessage(), e);result = ApiRespResult.sysError(e.getMessage());}return result;}/*** 获取用户JWT扩展信息* @return*/@RequestMapping("/getJwtInfo")public ApiRespResult getUserEnhancer() {ApiRespResult  result = null;try {// 获取用户JWT扩展信息Map<String, Object> userAdditionalInfos = getUserAdditionalInfos();result = ApiRespResult.success(userAdditionalInfos);}catch(ComponentException e) {log.error(e.getMessage(), e);result = ApiRespResult.error(e.geterrorCodeEnum());}catch(Exception e) {log.error(e.getMessage(), e);result = ApiRespResult.sysError(e.getMessage());}return result;}}

异常组件

import com.itcast.bulls.stock.common.exception.constants.IErrorCodeEnum;/*** 自定义组件异常*/
public class ComponentException extends AbstractException {/****/private static final long serialVersionUID = 2333790764399190094L;/*** 错误码枚举信息*/private IErrorCodeEnum errorCodeEnum;/*** 扩展的错误信息*/private String extendErrorMessage;public ComponentException(IErrorCodeEnum errorCodeEnum) {super(errorCodeEnum.getCode() + ":" + errorCodeEnum.getMessage());this.errorCodeEnum = errorCodeEnum;}public ComponentException(IErrorCodeEnum errorCodeEnum, String extendErrorMessage) {super(errorCodeEnum.getCode() + ":" + errorCodeEnum.getMessage() + "["+ extendErrorMessage + "]");this.errorCodeEnum = errorCodeEnum;this.extendErrorMessage = extendErrorMessage;}public IErrorCodeEnum geterrorCodeEnum() {return errorCodeEnum;}public void seterrorCodeEnum(IErrorCodeEnum errorCodeEnum) {this.errorCodeEnum = errorCodeEnum;}public String getExtendErrorMessage() {return extendErrorMessage;}public void setExtendErrorMessage(String extendErrorMessage) {this.extendErrorMessage = extendErrorMessage;}}

网关服务

  • 全局过滤器StockRequestGlobalFilter
import io.netty.util.internal.StringUtil;
import lombok.extern.log4j.Log4j2;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;/*** <p>Description: </p>* @date * @author * @version 1.0* <p>Copyright:Copyright(c)2020</p>*/
@Component
@Log4j2
public class StockRequestGlobalFilter implements GlobalFilter, Ordered {/*** 通过filter来自定义配置转发信息* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String authentication = exchange.getRequest().getHeaders().getFirst("Authorization");if(!StringUtil.isNullOrEmpty(authentication)){log.info("enter stockRequestGlobalFilter filter method: " + authentication);exchange.getRequest().mutate().header("Authorization",authentication);}return chain.filter(exchange.mutate().build());}@Overridepublic int getOrder() {return -1000;}
}

这是自定义全局过滤器的实现, 防止header中的Authorization没有转发的问题。

  1. 测试验证

申请Token

POST http://127.0.0.1:10680/oauth/token?
grant_type=password&username=admin&password=admin&scope=server
Accept: */*
Cache-Control: no-cache
Authorization: Basic YXBwOmFwcA==

返回Token信息:

{
"access_token": "cc5c4c1d-b519-458f-b338-ad4bd1ec06b0",
"token_type": "bearer",
"refresh_token": "86fec4ff-6c24-4171-a257-bf2d4e6bc30c",
"expires_in": 29749,
"scope": "server",
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"active": true
}

获取增强用户信息

aGET 127.0.0.1:10680/user/getUserEnhancer
Accept: */*
Cache-Control: no-cache
Authorization: Bearer cc5c4c1d-b519-458f-b338-ad4bd1ec06b0

返回增强的用户信息:

{
"code": "SYS_200",
"msg": "成功",
"extendData": null,
"data": {
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"active": true
},
"success": true
}

4.3 基于JWT扩展信息的微服务技术实践

  1. 整体实现流程
    采用密码模式,基于JWT扩展信息的微服务应用实践方案:

代码实现

认证服务

认证服务系统配置
AuthorizationServerConfig

 /*** 认证服务配置* @param endpoints*/@Overridepublic void configure(AuthorizationServerEndpointsConfigurer endpoints) {// JWT信息增强配置,采用链式配置, 包含JWT签名配置与JWT扩展信息配置。TokenEnhancerChain enhancerChain = new TokenEnhancerChain();List<TokenEnhancer> delegates = new ArrayList<>();delegates.add(jwtTokenEnhancer());delegates.add(accessTokenConverter());enhancerChain.setTokenEnhancers(delegates);endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).tokenStore(tokenStore()).userDetailsService(authStockUserDetailService).authenticationManager(authenticationManager).reuseRefreshTokens(false).tokenEnhancer(enhancerChain);}@Beanpublic JwtTokenEnhancer jwtTokenEnhancer() {return new JwtTokenEnhancer();}/*** TokenStore实现方式, 采用Redis缓存* @return*/@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("test123");return converter;}

认证服务采用JWT方式配置,JWT配置,采用链式配置, 包含JWT签名配置与JWT扩展信息,
JWT签名设为test123。这里采用自定义的增强JWT作实现。

JWT增强实现类:

JwtTokenEnhancer:

public class JwtTokenEnhancer implements TokenEnhancer {/*** JWT扩展存储用户信息* @param accessToken* @param authentication* @return*/@Overridepublic OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {Map<String, Object> additionalInfo = new HashMap<>();OAuthTradeUser authTradeUser = (OAuthTradeUser) authentication.getUserAuthentication().getPrincipal();if(null != authTradeUser) {TradeUser tradeUser = authTradeUser.getTradeUser();// 存储用户扩展信息additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USER_ID, tradeUser.getId());additionalInfo.put(GlobalConstants.OAUTH_DETAILS_USERNAME, tradeUser.getName());additionalInfo.put(GlobalConstants.OAUTH_DETAILS_LOGIN_INFO, tradeUser.getEmail() + "|" + tradeUser.getAddress());((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);}return accessToken;}
}

在JWT存储扩展用户信息,可以根据需要扩展不同的信息,但长度要有限制。

用户服务

认证配置ResourceSecurityConfigurer

@Primary
@Order(90)
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResourceSecurityConfigurer implements ResourceServerConfigurer {@Autowiredprivate RemoteTokenServices remoteTokenServices;@Autowiredprivate RestTemplate restTemplate;/*** 远程调用, 采用RestTemplate方式* @param resources* @throws Exception*/@Overridepublic void configure(ResourceServerSecurityConfigurer resources) throws Exception {remoteTokenServices.setRestTemplate(restTemplate);resources.tokenServices(remoteTokenServices);}@Beanpublic TokenStore tokenStore() {return new JwtTokenStore(accessTokenConverter());}@Beanpublic JwtAccessTokenConverter accessTokenConverter() {JwtAccessTokenConverter converter = new JwtAccessTokenConverter();converter.setSigningKey("test123");return converter;}/*** 资源服务的安全配置* @param http* @throws Exception*/@Overridepublic void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/user/**").authenticated().and().formLogin().loginPage("/login").failureUrl("/login?error").defaultSuccessUrl("/home");}/*** RestTemplate配置* @return*/@Bean@Primary@LoadBalancedpublic RestTemplate lbRestTemplate() {RestTemplate restTemplate = new RestTemplate();restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {@Overridepublic void handleError(ClientHttpResponse response) throws IOException {if (response.getRawStatusCode() != HttpStatus.BAD_REQUEST.value()) {super.handleError(response);}}});return restTemplate;}}

修改认证配置,采用JWT方式,设置签名为test123,这里要和认证服务里面的签名保持一致,否则不能正常解析JWT信息。

增加获取JWT扩展信息的接口

StockUserController:

    /*** 获取用户JWT扩展信息* @return*/@RequestMapping("/getJwtInfo")public ApiRespResult getUserEnhancer() {ApiRespResult  result = null;try {// 获取用户JWT扩展信息Map<String, Object> userAdditionalInfos = getUserAdditionalInfos();result = ApiRespResult.success(userAdditionalInfos);}catch(ComponentException e) {log.error(e.getMessage(), e);result = ApiRespResult.error(e.geterrorCodeEnum());}catch(Exception e) {log.error(e.getMessage(), e);result = ApiRespResult.sysError(e.getMessage());}return result;}

自定义解析JWT数据
增加依赖:

 <!-- JWT TOKEN 组件 --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.0</version></dependency>
  protected String getJwtToken() {// 1. 获取Request对象HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 2. 获取token信息String token = request.getHeader("Authorization");if(null != token) {token = token.replace(OAuth2AccessToken.BEARER_TYPE, "").trim();}return Jwts.parser().setSigningKey("test123".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody().toString();}

测试验证

申请Token

POST http://127.0.0.1:10680/oauth/token?
grant_type=password&username=admin&password=admin&scope=server
Accept: */*
Cache-Control: no-cache
Authorization: Basic YXBwOmFwcA==

返回Token信息:

{
"access_token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y
2FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6
WyJzZXJ2ZXIiXSwiZXhwIjoxNTk0ODQ5NTg1LCJqdGkiOiI2OWI4MWQzMi05MTk2LTQ5YmI
tOTU3ZC05YmRlZDM2OTY3ZTAiLCJjbGllbnRfaWQiOiJhcHAifQ.bFBKhPf0IYnJ9dpZG4a
PIlmpLECYwK-jTYTPHd2fc_M",
"token_type": "bearer",
"refresh_token":
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y
2FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6
WyJzZXJ2ZXIiXSwiYXRpIjoiNjliODFkMzItOTE5Ni00OWJiLTk1N2QtOWJkZWQzNjk2N2U
wIiwiZXhwIjoxNTk3Mzk4Mzg1LCJqdGkiOiIyMjhkMmIyZS02YmRkLTQ1NzktYTljNy03ZG
I0NmZmMjA3ZjkiLCJjbGllbnRfaWQiOiJhcHAifQ.yHD0U1WtOH_SAGev3mPwD1L1_XucWv
tRpTT-upHNqTM",
"expires_in": 43199,
"scope": "server",
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"jti": "69b81d32-9196-49bb-957d-9bded36967e0"
}

获取JWT扩展用户信息

GET 127.0.0.1:10680/user/getJwtInfo
Accept: */*
Cache-Control: no-cache
Authorization: Bearer
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dpbl9pbmZvIjoiaGVrdW4xQGl0Y2
FzdC5jbnxudWxsIiwidXNlcl9pZCI6MSwidXNlcl9uYW1lIjoiYWRtaW4iLCJzY29wZSI6W
yJzZXJ2ZXIiXSwiZXhwIjoxNTk0ODQ5NTg1LCJqdGkiOiI2OWI4MWQzMi05MTk2LTQ5YmIt
OTU3ZC05YmRlZDM2OTY3ZTAiLCJjbGllbnRfaWQiOiJhcHAifQ.bFBKhPf0IYnJ9dpZG4aP
IlmpLECYwK-jTYTPHd2fc_M

返回JWT扩展用户信息:

{
"code": "SYS_200",
"msg": "成功",
"extendData": null,
"data": {
"login_info": "hekun1@itcast.cn|null",
"user_id": 1,
"user_name": "admin",
"jti": "69b81d32-9196-49bb-957d-9bded36967e0"
},
"success": true
}

5.大佬资料传送门

OAuth & OpenID & SAML 工作流程梳理对比

SAML vs OAuth2

SAML和XACML相结合的Web服务访问控制模型

SAML协议应用_企业微信登录阿里云

阿里P8架构师谈:单点登录的原理、来源、实现、以及技术方案比较

单点登陆(SSO)协议简介:OpenID、OAuth2、SAML

基于OIDC(OpenID Connect)的SSO

万文长字分析OAuth 2.0+JWT+spring security完成认证授权-生产级-附带源码

SSO单点登录

阿里云SSO概览

AWS第三方登录(SAML)

维基SAML2.0

前端关于单点登录的知识

CAS与OAuth2的区别
CAS的单点登录时保障客户端的用户资源的安全 。

OAuth2则是保障服务端的用户资源的安全 。

CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源。

OAuth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问。

CAS的单点登录,资源都在客户端这边,不在CAS的服务器那一方。 用户在给CAS服务端提供了用户名密码后,作为CAS客户端并不知道这件事。 随便给客户端个ST,那么客户端是不能确定这个ST是用户伪造还是真的有效,所以要拿着这个ST去服务端再问一下,这个用户给我的是有效的ST还是无效的ST,是有效的我才能让这个用户访问。

OAuth2认证,资源都在OAuth2服务提供者那一方,客户端是想索取用户的资源。 所以在最安全的模式下,用户授权之后,服务端并不能直接返回token,通过重定向送给客户端,因为这个token有可能被黑客截获,如果黑客截获了这个token,那用户的资源也就暴露在这个黑客之下了。 于是聪明的服务端发送了一个认证code给客户端(通过重定向),客户端在后台,通过https的方式,用这个code,以及另一串客户端和服务端预先商量好的密码,才能获取到token和刷新token,这个过程是非常安全的。 如果黑客截获了code,他没有那串预先商量好的密码,他也是无法获取token的。这样oauth2就能保证请求资源这件事,是用户同意的,客户端也是被认可的,可以放心的把资源发给这个客户端了。

总结:所以cas登录和OAuth2在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码。

多维系统下单点登录深入详解相关推荐

  1. CAS 单点登录使用详解

    ============================================================================== 开发环境 :MyEclipse6.5+to ...

  2. SSO单点登录流程详解

    概念 单点登录(Single Sign On),简称为 SSO,是比较流行的企业业务整合的解决方案之一.SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统. 背景 企业 ...

  3. CSA实现单点登录原理详解 ,如何判断访问不同域名时用户是否登录

    原理图 www.cas.client.com为cas客户端,也就是用户要访问的资源所在,www.cas.server.com为cas服务端,是单点登录的认证中心. 图中各步骤拆解说明: ①:首先用户访 ...

  4. 单点登录(SSO)详解

    文章目录 前言 一.单点登录是什么? 二.单点登录的实现方式 1.Cookie方案: 2.Session方案: 3.Token方案: 三.JWT是什么 1.JWT的概况 2.JWT的组成 3.JWT的 ...

  5. SSO单点登录原理详解

    本文主要对SSO单点登录与CAS.OAuth2.0两种授权协议的关系和原理进行详细说明. 1. 基础概念 术语解释 SSO-Single Sign On,单点登录 TGT-Ticket Grantin ...

  6. mysql-win安装教程,WINDOWS下安装MYSQL教程详解

    1.下载安装包 2.配置环境变量 2.1 解压所下载的压缩包 2.2 环境变量 win 10 电脑 这么进去 3.生成data文件 在你解压的目录下,eg:F:\Program Files\mysql ...

  7. 火狐浏览器账号登录步骤详解

    打开火狐浏览器,点击右上角的火狐账户图标,并选取登录; 火狐怎么登录账号?火狐浏览器账号登录步骤详解 火狐怎么登录账号?火狐浏览器账号登录步骤详解 输入点击右键地址,再按下"确定" ...

  8. linux下测试ftp传输,linux下ftp命令使用详解---linux文件传输ftp命令

    linux下ftp命令使用详解---linux文件传输ftp命令 上一篇 / 下一篇  2010-12-18 09:15:35 / 个人分类:Linux ftp(file transfer proto ...

  9. Navicat的mysql远程登录方法详解

    Navicat的mysql远程登录方法详解 工具和前提 远程连接方法 疑惑问题 报错汇总 关闭并删除用户 引用 工具和前提 1.均在Navicat上面进行操作: 2.Navicat15的版本,本地数据 ...

最新文章

  1. pyqt5动态删除控件问题
  2. Java调用net的Webservice时提示:undefined element declaration 's:schema'
  3. vsftpd搭建和创建虚拟账号
  4. linux下w和who使用说明
  5. mysql 子查询 as_mysql子查询
  6. psutil python库
  7. 分布式大型互联网企业架构
  8. js中事件处理程序的内存优化
  9. 【观点】失败应聘的五大原因
  10. 苹果手机来电归属地_如何批量计算手机号码的归属地?
  11. QT 如何在QPushButton上加载gif动图
  12. 将CSDN文章下载为markdown文档
  13. 利用 adb 对手机进行屏幕分辨率设置
  14. 常规WebRoot项目在Idea中通过tomcat运行
  15. 程序、进程、作业的联系
  16. Stellarium:Compile with MSVC2012 and Qt5 (OpenGL)
  17. python语言常用的中文分词第三方库是_基于boost使用Python调用NLPIR(ICTCLAS2013)中文分词组件...
  18. 完美世界手游服务器维护中,完美世界手游黑屏闪退解决办法 玩不了怎么办
  19. Bert 得到中文词向量
  20. [每日一题]C语言:将输入的一句话的单词倒置,但标点符号不倒置

热门文章

  1. 韩舜尧 论: JavaScript如何实现省市级联动~(面试宝典必会题之一)
  2. P5687 [CSP-SJX2019] 网格图(Kruskal扩展)
  3. 基金的全面介绍,看这一篇就够了
  4. 履带式机器人运动模型及应用分析
  5. 简单python代码——猜数字
  6. DataWorks的使用到的函数
  7. 在线html 浏览器,web浏览器在线兼容性测试工具_检测html网页在不同浏览器上的兼容问题...
  8. 【运动】坚持骑车上班咯
  9. 北京工业大学2022年893真题考点
  10. Qt之textedit设置字体颜色