鉴权 OAuth 2.0的实现(Spring_Security_Oauth2)

目录

一、基本概念

1.1、什么是认证

1.2 、什么是会话

1.3、授权的数据模型

1.3.1、权限的分类

1.4 RBAC

1.4.1 基于角色的权限控制

1.4.2 基于资源的权限控制

1.4.3、两种方式的优缺点对比

二、SSO单点登录

2.1、什么是SSO单点登录

2.2、单点登录的实现

1、共享session

2、使用redis存储token

3、客户端存储token(JWT token)

2.3、三种方案的对比和主流选取

三、分布式系统的权限控制

3.1、分布式认证授权的需求总结如下:

1、统一认证授权

2、应用接入认证

3.2、OAuth2.0/OAuth 2.1

OAauth2.0包括以下角色:

3.3、OAuth 2.0的四种授权模式

3.3.1、客户端凭证模式

3.3.2、密码模式

3.3.3、授权码模式

3.3.4、隐式授权(简易模式)

3.4、权限控制框架的选用

3.4.1、两大鉴权框架 shiro 和 spring security

Shiro

​编辑

1、特点

2、认证授权的相关概念

3、Shiro三个核心组件

4、Shiro 中有四大功能——身份验证,授权,会话管理和加密。

5、优势和劣势

Spring Security

1、Spring Security核心结构

2、工作原理:

认证流程:

授权流程:

3、Spring Security 的优势和劣势

3.4.2、Shiro和Spring Security比较

3.5、对OAuth的实现

四、实现分布式系统认证、授权功能

4.1、需求分析

4.2、注册中心

4.3、网关

4.4、服务划分:

4.4.1、auth服务的创建

4.4.2、oauth2-gateway网关

4.4.3、api模块

五、总结

六、附:

HMAC算法

运算步骤:

Hmac代码实现

JWT

什么是 JWT

JWT 的数据结构

header

playload

signature

JWT的优点

JWT的缺点:

JWT的使用注意

JWT 等待解决的问题

JWT 适用场景


一、基本概念

1.1、什么是认证

认证 :用户认证就是判断一个用户的身份是否合法的过程

通俗来讲,用户输入账户密码登录的过程就叫认证。

系统为什么要认证?

认证是为了保护系统的隐私数据与资源,用户的身份合法方可访问该系统的资源。

1.2 、什么是会话

可以理解为客户端和服务器之间的一次私密谈话,在一次对话中可能会有多个请求和响应;同时要具有鉴别多个请求是来自同一个客户端的一次对话的功能;

用户认证通过后,为了避免用户的每次操作都进行认证,可将用户的信息保证在会话中。会话就是系统为了保持当前用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。

基于session的认证方式如下图:

        它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。

基于token方式如下图:

        它的交互流程是,用户认证成功后,服务端生成一个token发给客户端,客户端可以放到 cookie 或 localStorage等存储中,每次请求时带上 token,服务端收到token通过验证后即可确认用户身份。

        基于session的认证方式由Servlet规范定制,服务端要存储session信息需要占用内存资源,客户端需要支持cookie;基于token的方式则一般不需要服务端存储token,并且不限制客户端的存储方式,但同时也了可能需要支持cookie,也可以利用localStorage来存储;如今移动互联网时代更多类型的客户端需要接入系统,系统多是采用前后端分离的架构进行实现,所以基于token的方式更适合。

1.3、授权的数据模型

主体 object(用户)—— 权限 permission(增删改查)—— 资源 resource(服务模块、功能)

主体、资源、权限相关的数据模型如下:

主体(用户id、账号、密码、...)

资源(资源id、资源名称、访问地址、...)

权限(权限id、权限标识、权限名称、资源id、...)

角色(角色id、角色名称、...)

角色和权限关系(角色id、权限id、...)

主体(用户)和角色关系(用户id、角色id、...)

主体(用户)、资源、权限关系如下图:

通常企业开发中将资源和权限表合并为一张权限表,如下:

资源(资源id、资源名称、访问地址、...)

权限(权限id、权限标识、权限名称、资源id、...)

合并为:

权限(权限id、权限标识、权限名称、资源名称、资源访问地址、...)

修改后数据模型之间的关系如下图:

1.3.1、权限的分类

        权限可以分为功能权限和数据权限,功能权限可以理解就是一个资源模块,或者一个API,数据权限就是该用户能访问某功能API,但是只能访问限定的数据范围。

1.4 RBAC

如何实现授权?业界通常基于RBAC实现授权。

1.4.1 基于角色的权限控制

RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,比如:主体的角色为总经理可以查询企业运营报表,查询员工工资信息等;

1.4.2 基于资源的权限控制

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,比如:用户必须具有查询工资权限才可以查询员工工资信息等;

1.4.3、两种方式的优缺点对比

基于角色的权限控制

优点:容易配置,只需要把每个角色的权限在配置好,保存在数据库,所有用户在注册的时候就分配好角色,那么所有的用户的权限可以同一配置了,当角色的权限发生变化的时候,只需要将对应的角色的权限配置好就行了。

缺点:有些用户归属于同一角色,但是因为岗位略有不同,那么需要一些特殊的权限,那么需要子角色,如果一直这样下去,可能有子角色的子角色...可能需要一直创建很多子级角色。概括来说,粒度不够细。

基于资源的权限控制

优点:权限的控制的粒度比较灵活,可以按照不同资源模块来限制用户的权限,比如客户端用户不能访问系统管理,那么不用管系统管理里面有多少功能API,不用细分地给用户分配每一个API的权限,一律屏蔽不允许,也可以允许访某个资源模块中的限定的一些API;

缺点:需要批量修改用户的权限的时候就很麻烦。

综合对比还是基于角色的权限控制实现会更便于控制,虽然同一角色会特殊情况会有不同的功能权限,但是只需要创建子角色就可以解决,这样,即使需要改变某些群体的权限就可以修改角色的权限达到批量修改的目的,但是如果纯粹按照基于资源的权限控制方式设计权限控制,那么当有些用户权限需要变动的时候,不能批量修改。

角色相当于用户和权限的之间的一个中间代理;

二、SSO单点登录

        单体、单机项目的权限控制比较简单,因为只限于一个系统中的权限认证和访问权限控制;注意前端和后端接口同时做权限控制就好;至于是会话的实现是用session和token的方式,都是可以的。token更复杂一点,但是不需要在服务端存储session,也不用在cookie存储sessionID,也更安全。

        那么在彼此相关的一系列系统之间,比如某个公司的垂直业务拆分体系系统,各个子系统,OA行政,人力资源管理(工作目标、绩效考核)、门户网站,财务汇总,生产管理,仓储管理,物流管理,系统后台等等,在访问每个系统都要去登录认证一遍,从而获取权限是不是很麻烦呢?

还有分布式系统、微服务系统中各个服务之间的权限校验,也是同样的需求;

        甚至还有第三方系统需要借助某个系统提供认证,或者某些小一点的平台需要借助第三方大一点的平台(支付宝、微信)来免注册登录认证,都是如此;

2.1、什么是SSO单点登录

SSO(Single Sign On)流行的业务整合解决方案之一。SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

  • 可以是同一个系统中,比如分布式系统中,跨不同的子系统的访问,不需要每个子系统都需要登录验证一遍,否则会非常麻烦;
  • 也可以是不同的应用,但却是可以互相信任,相互关联的一系列应用,比如垂直业务系统中,也要求只需要在一个应用登录后其他的应用站点,不再需要登录就可以跳转访问;
  • 还可以是毫无关联的系统(对第三方系统提供认证),但是允许其中一个作为另外一个应用的第三方认证方,比如微信,QQ,支付宝,可以作为很多电商的免注册的第三方认证系统。

什么是分布式系统?什么是垂直业务体系系统?参照分布式服务架构和微服务架构

2.2、单点登录的实现

1、共享session

        tomcat支持session共享,但是有广播风暴;session会存储在内存,用户量大的时候,占用资源就严重,不推荐;

这种方案下,通常由以下几种做法:

  • session复制。在多台应用服务器之间同步session,并使session保存一致,对外透明。
  • session黏贴。当用户访问集群中某台服务器后,强制指定后续所有请求均落到此服务器上。
  • session集中存储。将session存入指定的服务器中,所有服务器应用实例都统一从指定服务器中获取session信息。

2、使用redis存储token

  • 服务端使用UUID生成随机64位或者128位token,放入redis中,然后返回给客户端并存储在cookie中,或者在本地磁盘存储中,用户每次访问都携带此token,服务端去redis中校验是否有此用户即可(并不是逆向解析token);
  • 可以认为是共享session(集中存储)的改进版,因为这种方式和共享session一样需要在服务端存储已登录的信息。但是它拿出一个单独的缓存服务来存储token,不会影响正式的资源服务的服务器的内存资源。所有的服务校验的时候都去访问同一的缓存服务,没有像session共享的时候的session复制带来的资源消耗,当然主备之间也会有主从同步带来的数据通信的资源消耗,但是这也比session每个节点的共享复制要好得多。
  • 所以这种方式目前也有很多公司都在使用。但是这种方式不是最优的方案,这种方式需要每次都去redis里拿数据检验一下,在访问量大的时候,每次访问redis也是一种资源(比如网络资源和IO)的消耗,所以现在很多应用已经开始使用我们今天要讲的JWT;

3、客户端存储token(JWT token)

客户端存储JWT token,服务端不用存储token,且token是用户的一些基础信息的加上头部、签名的等信息一起加密后的密文。逆向解密可以得到用户的基础信息,比如用户ID,可以根据用户ID到缓存或者数据中获取用户的权限等。

2.3、三种方案的对比和主流选取

        基于session和基于jwt的方式的主要区别就是用户的状态保存的位置,session是保存在服务端的,而jwt是保存在客户端的。

        以上三种方式中,除了共享session,需要cookie的支持,后面两种token,可以储存在 Cookie 里面,也可以储存在 localStorage。而存储在cookie里面会有CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

        主流还是倾向于JWT来作为token,节省了一次redis中读取数据,验证token是否有效的过程,数据IO和网络传输的资源消耗要大过token的解析计算带来的性能消耗。只是如果要做到随时控制token强制过期,那还是需要在配置中心配置一个参数epoch,作为对比,如果token中解析的对比参数epoch不一致,就算token中的过期时间还没有到,仍然将所有的token都作废,需要认证,并获取刷新后的token;但是如果我需要精准控制部分用户的token过期,还是需要在缓存中缓存token,只是token就不需要JWT这种标准的加密方式,以免太长,请求头的载荷太大,带来性能消耗。这就是为什么很多公司还是采用缓存集中存储token,并由缓存校验token是否存在这种方案的原因。

三、分布式系统的权限控制

        单点登录应用涵盖的系统比较广泛,这里主要是以分布式系统为代表,研讨权限控制。

        面向服务架构SOA可以看做是分布式架构到微服务的过度,直接将SOA和微服务合在一起,不过分布式架构的认证授权服务的架构同样适用于微服务架构,所以这里讲分布式系统的权限控制就可以了。

        分布式系统的运行通常依赖网络,它将单体结构的系统分为若干服务,服务之间通过网络交互来完成用户的业务处理,当前流行的微服务架构就是分布式系统架构,如下图:

分布式系统的会话中,由于会跨服务,跨节点访问,用户已登录的状态信息如何在不同的子系统或者资源模块中共享呢?

        实现思想思路还是SSO单点登录的那一套,在业务垂直拆分体系系统中,用户登录认证,权限都是单独的一个系统,分布式系统中,也是将认证授权同一到一个服务中;

        分布式系统的每个服务都会有认证、授权的需求,如果每个服务都实现一套认证授权逻辑会非常冗余,考虑分布式系统共享性的特点,需要由独立的认证服务处理系统认证授权的请求;考虑分布式系统开放性的特点,不仅对系统内部服务提供认证,对第三方系统也要提供认证。

3.1、分布式认证授权的需求总结如下:

1、统一认证授权

  • 提供独立的认证服务,统一处理认证授权。
  • 无论是不同类型的用户,还是不同种类的客户端(web端,H5、APP),均采用一致的认证、权限、会话机制,实现统一认证授权。
  • 要实现统一则认证方式必须可扩展,支持各种认证需求,比如:用户名密码认证、短信验证码、二维码、人脸识别等认证方式,并可以非常灵活的切换。

2、应用接入认证

应提供扩展和开放能力,提供安全的系统对接机制,并可开放部分API给接入第三方使用,一方应用(内部 系统服务)和三方应用(第三方应用)均采用统一机制接入。

        通俗来讲,为了保证扩展和开放能力,安全可信的统一的登录认证授权服务,需要具备提供认证授权服务的API给更多的三方应用使用能力,而第三方应用也能兼容接受统一的认证授权的开放机制,那么就需要一个统一的标准来规范和约束。

3.2、OAuth2.0/OAuth 2.1

        OAuth(开放授权)是一个开放标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或分享他们数据的所有内容。OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。很多大公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。

OAauth2.0包括以下角色:

1、客户端

  • 本身不存储资源,需要通过资源拥有者的授权去请求资源服务器的资源,比如:Android客户端、Web客户端(浏览器端)、微信客户端等。
  • 第三方应用,其本身不存储资源,而是资源拥有者授权通过后,使用它的授权(授权令牌)访问受保护资源,然后客户端把相应的数据展示出来/提交到服务器。“客户端”术语不代表任何特定实现(如应用运行在一台服务器、桌面、手机或其他设备)。

2、资源拥有者

  • 通常为用户,也可以是应用程序,即该资源的拥有者。
  • 比如你的信息是属于你的。你就是资源的拥有者。

3、授权服务器(也称认证服务器)

  • 用于服务提供商对资源拥有的身份进行认证、对访问资源进行授权,认证成功后会给客户端发放令牌(access_token),作为客户端访问资源服务器的凭据。本例为微信的认证服务器。
  • 成功验证资源拥有者并获取授权之后,授权服务器颁发授权令牌(Access Token)给客户端。

4、资源服务器

  • 存储受保护资源,客户端通过access token请求资源,资源服务器响应受保护资源给客户端。
  • 存储资源的服务器,本例子为微信存储的用户信息。
  • 现在还有一个问题,服务提供商能允许随便一个客户端就接入到它的授权服务器吗?答案是否定的,服务提供商会给准入的接入方一个身份,用于接入时的凭据:client_id:客户端标识 client_secret:客户端秘钥。因此,准确来说,授权服务器对两种OAuth2.0中的两个角色进行认证授权,分别是资源拥有者、客户端。

3.3、OAuth 2.0的四种授权模式

3.3.1、客户端凭证模式

  1. 客户端向授权服务器发送自己的身份信息,并请求令牌(access_token)
  2. 确认客户端身份无误后,将令牌(access_token)发送给client,请求如下:
/uaa/oauth/token?client_id=c1&client_secret=secret&grant_type=client_credentials

参数列表如下:

client_id:客户端准入标识。

client_secret:客户端秘钥。

grant_type:授权类型,填写client_credentials表示客户端模式

这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的。因此这种模式一般用来提供给我们完全信任的服务器端服务。比如,合作方系统对接,拉取一组用户信息。

POST http://localhost:53020/uaa/oauth/token
  • 总结来说:该模式针对客户端而言,对用户是透明的,不需要用户参与,非用户层面授权。客户端向授权服务器发送自己的的 client_id 和 client_secrect 请求 access_token ,用户中心仅校验客户端应用身份 。客户端通过授权后可以获得授权范围内所有用户的信息,对客户端应用需要极高的信任。
  • 模式特点: 针对客户端层面进行授权,而非对单独用户进行授权的场景,用户中心仅校验客户端应用的身份。
  • 适用场景: 适用于的自家产品、微服务中,需要从接口层面发起请求的场景。
  • 当前模式仅生成 access_token,不生成 refresh_token。

3.3.2、密码模式

  1. 资源拥有者将用户名、密码发送给客户端
  2. 客户端拿着资源拥有者的用户名、密码向授权服务器请求令牌(access_token),请求如下:
  3. 授权服务器将令牌(access_token)发送给client
/uaa/oauth/token?
client_id=c1&client_secret=secret&grant_type=password&username=shangsan&password=123

参数列表如下:

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写password表示密码模式
  • username:资源拥有者用户名。
  • password:资源拥有者密码。

授权服务器将令牌(access_token)发送给client

        这种模式十分简单,但是却意味着直接将用户敏感信息泄漏给了client,把用户名和密码直接泄露给客户端,代表了资源拥有者和授权服务器对客户端的绝对互信,相信客户端不会做坏事。一般适用于内部开发的客户端的场景。因此这就说明这种模式只能用于client是我们自己开发的情况下。因此密码模式一般用于我们自己开发的,第一方原生App或第一方单页面应用。

        总结来说:用户直接提供用户名与密码给客户端应用,客户端使用用户的账号和密码、自己的 client_id 和 client_secrect 向授权服务器请求 token 。用户中心校验用户和客户端应用身份,响应 access_token 和 refresh_token。

模式特点: 用户的账号和密码直接暴露给客户端,安全性低。

适用场景: 适用于自家的产品、微服务中,需要从用户层面发起请求的场景。

该模式已经被 Oauth2.1 废弃,直接将用户的账号和密码明文交给客户端应用是一个传统的方案,其本身没有校验意义,需要逐步过渡到通过 token 凭证这种授权方式。

换而言之,客户端都有密码了,通过 Oauth2.0 流程要一个临时的 access_token 干嘛?

3.3.3、授权码模式

  1. 资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器被重定向到授权服务器,重定向时会附加客户端的身份信息。如:

/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

参数列表如下:

  • client_id:客户端准入标识。
  • response_type:授权码模式固定为code。
  • scope:客户端权限。
  • redirect_uri:跳转uri,当授权码申请成功后会跳转到此地址,并在后边带上code参数(授权码)。
  1. 浏览器出现向授权服务器授权页面,之后将用户同意授权。
  2. 授权服务器将授权码(AuthorizationCode)转经浏览器发送给client(通过redirect_uri)。
  3. 客户端拿着授权码向授权服务器索要访问access_token,请求如下:
/uaa/oauth/token? client_id=c1&client_secret=secret&grant_type=authorization_code&code=5PgfcD&redirect_uri=http://w ww.baidu.com

参数列表如下

  • client_id:客户端准入标识。
  • client_secret:客户端秘钥。
  • grant_type:授权类型,填写authorization_code,表示授权码模式
  • code:授权码,就是刚刚获取的授权码,注意:授权码只使用一次就无效了,需要重新申请。
  • redirect_uri:申请授权码时的跳转url,一定和申请授权码时用的redirect_uri一致。

        总结来说:客户端应用引导(重定向)用户携带着 client_id 和 redirect_url 前往认证服务器认证,认证通过后认证服务会附带上 code 参数重定向到redirect_url 地址(客户端应用提供的接收授权码的地址)。客户端应用的服务端携带自己的 client_id 、 client_secrect 和 code 请求认证服务器获取 access_token 和 refresh_token 返回到用户。

模式特点: 四种模式中最安全、最常见的一种模式。

适用场景: 适用于有服务端服务的客户端应用。

网上说,这种方式避免了 access_token 直接在公网传输,黑客截获到 code 也无法获得最终的 access_token,所以这种方案非常安全。不是非常认同,如果 code 会被截获,那么用户登录时的 token 是否也可以直接被截获到?

个人观点认为,安全主要体现在这种模式同时校验了用户和客户端应用的 client_secrect ,与密码模式不同,当前模式用户和客户端应用双方都不知道对方的密码明文。

3.3.4、隐式授权(简易模式)

1. 资源拥有者打开客户端,客户端要求资源拥有者给予授权,它将浏览器重定向到授权服务器,重定向时会附加客户端的身份信息。如:

/uaa/oauth/authorize?client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

参数描述同授权码模式 ,注意response_type=token,说明是简化模式。

2. 浏览器出现向授权服务器授权页面,之后需要用户同意授权。

3. 授权服务器将授权码将令牌(access_token)以Hash的形式存放在重定向uri的fargment中发送给浏览器。

注:fragment 主要是用来标识 URI 所标识资源里的某个资源,在 URI 的末尾通过 (#)作为 fragment 的开头,

其中 # 不属于 fragment 的值。如https://domain/index#L18这个 URI 中 L18 就是 fragment 的值。大家只需要

知道js通过响应浏览器地址栏变化的方式能获取到fragment 就行了。

一般来说,简化模式用于没有服务器端的第三方单页面应用,因为没有服务器端就无法接收授权码。

浏览器访问认证页面:

http://localhost:53020/uaa/oauth/authorize? client_id=c1&response_type=token&scope=all&redirect_uri=http://www.baidu.com

然后输入模拟的账号和密码点登陆之后进入授权页面:

确认授权后,浏览器会重定向到指定路径(oauth_client_details表中的web_server_redirect_uri)并以Hash的形式存放在重定向uri的fargment中,如:

http://aa.bb.cc/receive#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZW5hbn...
  • 总结来说:客户端应用引导用户携带 client_id 前往授权服务器认证,认证通过后认证服务器直接返回 access_token 。
  • 模式特点: 不需要与客户端应用的服务端进行交互,没有校验 client_secrect。与授权码模式,省去返回授权码,再重定向到授权服务,直接一步到位。
  • 适用场景: 适用于仅有前端页面,没有后端服务的客户端应用。
  • 通过 # 锚点链接的方式返回 access_token,避免 token 被携带传输到 web 前端文件托管的服务器上。
  • 该模式已经被 Oauth2.1 废弃,这种方式容易泄露 token ,不安全。但是token以#fragment 返回,并传输,容易泄露

3.4、权限控制框架的选用

       

         所以OAuth 只是一个协议,只要实现了以上的规范和四个角色,用什么框架都是无所谓的。

        只要自己能实现api请求拦截认证授权、和token验证颁发的功能,不用别人的框架也是可以的,如果有框架能实现大部分的功能,以及防web攻击的机制,那选用一种认证授权框架也是不错的。

3.4.1、两大鉴权框架 shiro 和 spring security

Shiro

Apache Shiro是一个强大且易用的Java安全框架,能够非常清晰的处理认证、授权、管理会话以及密码加密。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。执行流程

1、特点
  1. 易于理解的 Java Security API;
  2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  3. 对角色的简单的签权(访问控制),支持细粒度的签权;
  4. 支持一级缓存,以提升应用程序的性能;
  5. 内置的基于 POJO 企业会话管理,适用于 Web 以及非 Web 的环境;
  6. 异构客户端会话访问;
  7. 非常简单的加密 API;
  8. 不跟任何的框架或者容器捆绑,可以独立运行。
2、认证授权的相关概念
  • 安全实体:系统需要保护的具体对象数据
  • 权限:系统相关的功能操作,例如基本的CRUD
  • Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web支持,可以非常容易的集成到Web环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
3、Shiro三个核心组件

Subject, SecurityManager 和 Realms.

  • Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
  • SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
  • Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
4、Shiro 中有四大功能——身份验证,授权,会话管理和加密。
  1. Authentication:有时也简称为“登录”,这是一个证明用户是谁的行为。
  2. Authorization:访问控制的过程,也就是决定“谁”去访问“什么”。
  3. Session Management:管理用户特定的会话,即使在非 Web 或 EJB 应用程序。
  4. Cryptography:通过使用加密算法保持数据安全同时易于使用。

除此之外,Shiro 也提供了额外的功能来解决在不同环境下所面临的安全问题,尤其是以下这些:

  1. Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
  2. Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作快速而又高效。
  3. Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
  4. Testing:测试支持的存在来帮助你编写单元测试和集成测试。
  5. "Run As":一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
  6. "Remember Me":在会话中记住用户的身份,这样用户只需要在强制登录时候登录。
5、优势和劣势
  • 就目前而言,Shiro 最大的问题在于和 Spring 家族的产品进行整合的时候非常不便,在 Spring Boot 推出的很长一段时间里,Shiro 都没有提供相应的 starter,后来虽然有一个 shiro-spring-boot-web-starter 出来,但是其实配置并没有简化多少。所以在 Spring Boot/Spring Cloud 技术栈的微服务项目中,Shiro 几乎不存在优势。
  • 但是如果你是传统的 SSM 项目,不是微服务项目,那么无疑使用 Shiro 是最方便省事的,因为它足够简单,足够轻量级。
Spring Security
  • Spring Security 主要实现了Authentication(认证,解决who are you? ) 和 Access Control(访问控制,也就是what are you allowed to do?,也称为Authorization)。Spring Security在架构上将认证与授权分离,并提供了扩展点。
  • 能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
  • 它是一个轻量级的安全框架,它确保基于Spring的应用程序提供身份验证和授权支持。它与Spring MVC有很好地集成,并配备了流行的安全算法实现捆绑在一起。安全主要包括两个操作“认证”与“验证”(有时候也会叫做权限控制)。“认证”是为用户建立一个其声明的角色的过程,这个角色可以一个用户、一个设备或者一个系统。“验证”指的是一个用户在你的应用中能够执行某个操作。在到达授权判断之前,角色已经在身份认证过程中建立了。
1、Spring Security核心结构

从设计思想角度,自顶而下功能实现上来解析(它的设计是基于框架内大范围的依赖的),可以被划分为以下几块:

  • FilterChainProxy:Web/Http 安全的实现;这是最复杂的部分。通过建立 filter 和相关的 service bean 来实现框架的认证机制。当访问受保护的 URL 时会将用户引入登录界面或者是错误提示界面。认证的流程主要就是由这一部分完成的。
    • Spring Security所解决的问题就是安全访问控制,而安全访问控制功能其实就是对所有进入系统的请求进行拦截,校验每个请求是否能够访问它所期望的资源。根据前边知识的学习,可以通过Filter或AOP等技术来实现,Spring Security对Web资源的保护是靠Filter实现的,所以从这个Filter来入手,逐步深入Spring Security原理。
    • 当初始化Spring Security时,会创建一个名为 SpringSecurityFilterChain 的Servlet过滤器,类型为org.springframework.security.web.FilterChainProxy,它实现了javax.servlet.Filter,因此外部的请求会经过此类,下图是Spring Security过虑器链结构图:

    • FilterChainProxy是一个代理,真正起作用的是FilterChainProxy中SecurityFilterChain所包含的各个Filter,同时这些Filter作为Bean被Spring管理,它们是Spring Security核心,各有各的职责,但他们并不直接处理用户的认证,也不直接处理用户的授权,而是把它们交给了认证管理器(AuthenticationManager)和决策管理器(AccessDecisionManager)进行处理,下图是FilterChainProxy相关类的UML图示。

    • spring Security功能的实现主要是由一系列过滤器链相互配合完成。

    • 下面介绍过滤器链中主要的几个过滤器及其作用:
      • SecurityContextPersistenceFilter 这个Filter是整个拦截过程的入口和出口(也就是第一个和最后一个拦截器),会在请求开始时从配置好的 SecurityContextRepository 中获取 SecurityContext,然后把它设置给
      • SecurityContextHolder。在请求完成后将 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同时清除 securityContextHolder 所持有的 SecurityContext;
      • UsernamePasswordAuthenticationFilter 用于处理来自表单提交的认证。该表单必须提供对应的用户名和密码,其内部还有登录成功或失败后进行处理的 AuthenticationSuccessHandler 和AuthenticationFailureHandler,这些都可以根据需求做相关改变;
      • FilterSecurityInterceptor 是用于保护web资源的,使用AccessDecisionManager对当前用户进行授权访问,前面已经详细介绍过了;
      • ExceptionTranslationFilter 能够捕获来自 FilterChain 所有的异常,并进行处理。但是它只会处理两类异常:AuthenticationException 和 AccessDeniedException,其它的异常它会继续抛出。
  • 授权:业务对象或者方法的安全;控制方法访问权限的。后面会讲述授权流程
  • AuthenticationManager:处理来自于框架其他部分的认证请求。
    • AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List 列表,存放多种认证方式。
    • 当你选择了资源所有者密码(password)授权类型的时候,请设置这个属性注入一个 AuthenticationManager 对象。
  • AccessDecisionManager:为 Web 或方法的安全提供访问决策。会注册一个默认的(Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是AffirmativeBased、ConsensusBased和UnanimousBased。),但是我们也可以通过普通 bean 注册的方式使用自定义的 AccessDecisionManager。
    • AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。
    • 这一部分机制比较复杂,也没有必要,深究,账号密码认证通过后,自然会得到用户的权限信息,自定义一个AccessDecisionManager,权限匹配就授权通过。
  • AuthenticationProvider:AuthenticationManager 是通过它来认证用户的。
    • 认证管理器(AuthenticationManager)委托AuthenticationProvider完成认证工作。
    • AuthenticationProvider是一个接口,定义如下:
public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> var1);
}
    • authenticate()方法定义了认证的实现过程,它的参数是一个Authentication,里面包含了登录用户所提交的用户、密码等。而返回值也是一个Authentication,这个Authentication则是在认证成功后,将用户的权限及其他信息重新组装后生成。
    • Spring Security中维护着一个 List 列表,存放多种认证方式,不同的认证方式使用不同的AuthenticationProvider。如使用用户名密码登录时,使用AuthenticationProvider1,短信登录时使用AuthenticationProvider2等等这样的例子很多。
    • 每个AuthenticationProvider需要实现supports()方法来表明自己支持的认证方式,如我们使用表单方式认证,在提交请求时Spring Security会生成UsernamePasswordAuthenticationToken,它是一个Authentication,里面封装着用户提交的用户名、密码信息。而对应的,哪个AuthenticationProvider来处理它?
    • 我们在DaoAuthenticationProvider的基类AbstractUserDetailsAuthenticationProvider发现以下代码:
public boolean supports(Class<?> authentication) {
    return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}

也就是说当web表单提交用户名密码时,Spring Security由DaoAuthenticationProvider处理。

    • 最后,我们来看一下Authentication(认证信息)的结构,它是一个接口,我们之前提到的UsernamePasswordAuthenticationToken就是它的实现之一:
//Authentication是spring security包中的接口,直接继承自Principal类,而Principal是位于 java.security包中的。它是表示着一个抽象主体身份,任何主体都有一个名称,因此包含一个getName()方法。
public interface Authentication extends Principal, Serializable { 
    //getAuthorities(),权限信息列表,默认是GrantedAuthority接口的一些实现类,通常是代表权限信息的一系列字符串。
    Collection<? extends GrantedAuthority> getAuthorities(); 
    //getCredentials(),凭证信息,用户输入的密码字符串,在认证过后通常会被移除,用于保障安全。
    Object getCredentials(); 
    // getDetails(),细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值
    Object getDetails();
    // getPrincipal(),身份信息,大部分情况下返回的是UserDetails接口的实现类,UserDetails代表用户的详细
    // 信息,那从Authentication中取出来的UserDetails就是当前登录用户信息,它也是框架中的常用接口之一。
    Object getPrincipal(); 
    
    boolean isAuthenticated();
    
    void setAuthenticated(boolean var1) throws IllegalArgumentException;
}
  • UserDetailsService:跟 AuthenticationProvider 关系密切,用来获取用户信息的。
    • DaoAuthenticationProvider处理了web表单的认证逻辑,认证成功后既得到一个Authentication(UsernamePasswordAuthenticationToken实现),里面包含了身份信息(Principal)。这个身份信息就是一个 Object ,大多数情况下它可以被强转为UserDetails对象。
    • DaoAuthenticationProvider中包含了一个UserDetailsService实例,它负责根据用户名提取用户信息UserDetails(包含密码),而后DaoAuthenticationProvider会去对比UserDetailsService提取的用户密码与用户提交的密码是否匹配作为认证成功的关键依据,因此可以通过将自定义的 UserDetailsService 公开为spring bean来定义自定义身份验证。
    • 它和Authentication接口很类似,比如它们都拥有username,authorities。Authentication的getCredentials()与UserDetails中的getPassword()需要被区分对待,前者是用户提交的密码凭证,后者是用户实际存储的密码,认证其实就是对这两者的比对。Authentication中的getAuthorities()实际是由UserDetails的getAuthorities()传递而形成的。还记得Authentication接口中的getDetails()方法吗?其中的UserDetails用户详细信息便是经过了AuthenticationProvider认证之后被填充的。
    • 通过实现UserDetailsService和UserDetails,我们可以完成对用户信息获取方式以及用户信息字段的扩展。
    • Spring Security提供的InMemoryUserDetailsManager(内存认证),JdbcUserDetailsManager(jdbc认证)就是UserDetailsService的实现类,主要区别无非就是从内存还是从数据库加载用户。
  • PasswordEncoder:密码传输是用密文的话,接受到密码之后如何解密Authentication中的密码来和我们获取到的UserDetails中的密码做对比的呢?这就需要PasswordEncoder密码编码器来做加密解密的事情;
    • 其实大多数数据库中存储的密码也是加密后的密文,简单点可以在前端就利用相同的PasswordEncoder来对输入密码做加密,并且在网络中传输,当然前端的js文件必须要保护好。然后用户在注册的时候,密码也用同样的PasswordEncoder加密后,存储在数据库。作对比的时候,就直接拿UserDetails中密码和Authentication中的密码作对比。
    • 在这里Spring Security为了适应多种多样的加密类型,又做了抽象,DaoAuthenticationProvider通过PasswordEncoder接口的matches方法进行密码的对比,而具体的密码对比细节取决于实现:
public interface PasswordEncoder {
    String encode(CharSequence var1);
    boolean matches(CharSequence var1, String var2);
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}
    • 而Spring Security提供很多内置的PasswordEncoder,能够开箱即用,使用某种PasswordEncoder只需要进行如下声明即可,如下:
@Bean
public PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
    • NoOpPasswordEncoder采用字符串匹配方法,不对密码进行加密比较处理,密码比较流程如下:
      • 1、用户输入密码(明文 )
      • 2、DaoAuthenticationProvider获取UserDetails(其中存储了用户的正确密码)
      • 3、DaoAuthenticationProvider使用PasswordEncoder对输入的密码和正确的密码进行校验,密码一致则校验通过,否则校验失败。
      • NoOpPasswordEncoder的校验规则拿 输入的密码和UserDetails中的正确密码进行字符串比较,字符串内容一致则校验通过,否则 校验失败。

实际项目中推荐使用BCryptPasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder等,感兴趣的可以看看这些PasswordEncoder的具体实现。

2、工作原理:

工作原理中分为认证流程,和授权流程;

  • 下面是整体的认证授权执行流程图总览:

认证流程:

1. 用户提交用户名、密码被SecurityFilterChain中的 UsernamePasswordAuthenticationFilter 过滤器获取到,封装为请求Authentication,通常情况下是UsernamePasswordAuthenticationToken这个实现类。

2. 然后过滤器将Authentication提交至认证管理器(AuthenticationManager)进行认证

3. 认证成功后, AuthenticationManager 身份管理器返回一个被填充满了信息的(包括上面提到的权限信息,身份信息,细节信息,但密码通常会被移除) Authentication 实例。

4. SecurityContextHolder 安全上下文容器将第3步填充了信息的 Authentication ,通过SecurityContextHolder.getContext().setAuthentication(…)方法,设置到其中。

  • 可以看出AuthenticationManager接口(认证管理器)是认证相关的核心接口,也是发起认证的出发点,它的实现类为ProviderManager。而Spring Security支持多种认证方式,因此ProviderManager维护着一个List 列表,存放多种认证方式,最终实际的认证工作是由AuthenticationProvider完成的。
  • 咱们知道web表单的对应的AuthenticationProvider实现类为DaoAuthenticationProvider,它的内部又维护着一个UserDetailsService负责UserDetails的获取。最终AuthenticationProvider将UserDetails填充至Authentication。

认证核心组件的大体关系如下:

授权流程:

Spring Security可以通过 http.authorizeRequests() 对web请求进行授权保护。SpringSecurity使用标准Filter建立了对web请求的拦截,最终实现对资源的授权访问。

Spring Security的授权流程如下:

分析授权流程:

  • 1. 拦截请求,已认证用户访问受保护的web资源将被SecurityFilterChain中的 FilterSecurityInterceptor 的子类拦截。
  • 2. 获取资源访问策略,FilterSecurityInterceptor会从 SecurityMetadataSource 的子类DefaultFilterInvocationSecurityMetadataSource 获取要访问当前资源所需要的权限Collection 。SecurityMetadataSource其实就是读取访问策略的抽象,而读取的内容,其实就是我们配置的访问规则, 读取访问策略如:
http
.authorizeRequests()
.antMatchers("/r/r1").hasAuthority("p1")
.antMatchers("/r/r2").hasAuthority("p2")
...
  • 3. 最后,FilterSecurityInterceptor会调用 AccessDecisionManager 进行授权决策,若决策通过,则允许访问资源,否则将禁止访问。

AccessDecisionManager(访问决策管理器)的核心接口如下:

public interface AccessDecisionManager {
/**
* 通过传递的参数来决定用户是否有访问对应受保护资源的权限
*/
void decide(Authentication authentication , Object object, Collection<ConfigAttribute>
configAttributes ) throws AccessDeniedException, InsufficientAuthenticationException;
//略..
}

这里着重说明一下decide的参数:

  • authentication:要访问资源的访问者的身份
  • object:要访问的受保护资源,web请求对应FilterInvocation
  • configAttributes:是受保护资源的访问策略,通过SecurityMetadataSource获取。
  • decide接口就是用来鉴定当前用户是否有访问对应受保护资源的权限。

授权决策

AccessDecisionManager采用投票的方式来确定是否能够访问受保护资源。

AccessDecisionManager中包含的一系列AccessDecisionVoter将会被用来对Authentication是否有权访问受保护对象进行投票,AccessDecisionManager根据投票结果,做出最终决策。

AccessDecisionVoter是一个接口,其中定义有三个方法,具体结构如下所示。

public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = ‐1;
    boolean supports(ConfigAttribute var1);
    boolean supports(Class<?> var1);
    int vote(Authentication var1, S var2, Collection<ConfigAttribute> var3);
}

vote()方法的返回结果会是AccessDecisionVoter中定义的三个常量之一。ACCESS_GRANTED表示同意,ACCESS_DENIED表示拒绝,ACCESS_ABSTAIN表示弃权。如果一个AccessDecisionVoter不能判定当前Authentication是否拥有访问对应受保护对象的权限,则其vote()方法的返回值应当为弃权ACCESS_ABSTAIN。

Spring Security内置了三个基于投票的AccessDecisionManager实现类如下,它们分别是

AffirmativeBased、ConsensusBased和UnanimousBased,。

AffirmativeBased的逻辑是:

(1)只要有AccessDecisionVoter的投票为ACCESS_GRANTED则同意用户进行访问;

(2)如果全部弃权也表示通过;

(3)如果没有一个人投赞成票,但是有人投反对票,则将抛出AccessDeniedException。

Spring security默认使用的是AffirmativeBased。

ConsensusBased的逻辑是:

(1)如果赞成票多于反对票则表示通过。

(2)反过来,如果反对票多于赞成票则将抛出AccessDeniedException。

(3)如果赞成票与反对票相同且不等于0,并且属性allowIfEqualGrantedDeniedDecisions的值为true,则表

示通过,否则将抛出异常AccessDeniedException。参数allowIfEqualGrantedDeniedDecisions的值默认为true。

(4)如果所有的AccessDecisionVoter都弃权了,则将视参数allowIfAllAbstainDecisions的值而定,如果该值

为true则表示通过,否则将抛出异常AccessDeniedException。参数allowIfAllAbstainDecisions的值默认为false。

UnanimousBased的逻辑与另外两种实现有点不一样,另外两种会一次性把受保护对象的配置属性全部传递给AccessDecisionVoter进行投票,而UnanimousBased会一次只传递一个ConfigAttribute给AccessDecisionVoter进行投票。这也就意味着如果我们的AccessDecisionVoter的逻辑是只要传递进来的ConfigAttribute中有一个能够匹配则投赞成票,但是放到UnanimousBased中其投票结果就不一定是赞成了。

UnanimousBased的逻辑具体来说是这样的:

(1)如果受保护对象配置的某一个ConfigAttribute被任意的AccessDecisionVoter反对了,则将抛出AccessDeniedException。

(2)如果没有反对票,但是有赞成票,则表示通过。

(3)如果全部弃权了,则将视参数allowIfAllAbstainDecisions的值而定,true则通过,false则抛出AccessDeniedException。

Spring Security也内置一些投票者实现类如RoleVoter、AuthenticatedVoter和WebExpressionVoter等,可以自行查阅资料进行学习。

总结工作流程:

  1. 客户端发起一个请求,进入 Security 过滤器链。
  2. 当到 LogoutFilter 的时候判断是否是登出路径,如果是登出路径则到 logoutHandler ,如果登出成功则到 logoutSuccessHandler 登出成功处理,如果登出失败则由 ExceptionTranslationFilter ;如果不是登出路径则直接进入下一个过滤器。
  3. 当到 UsernamePasswordAuthenticationFilter 的时候判断是否为登录路径,如果是,则进入该过滤器进行登录操作,如果登录失败则到AuthenticationFailureHandler 登录失败处理器处理,如果登录成功则到 AuthenticationSuccessHandler 登录成功处理器处理,如果不是登录请求则不进入该过滤器。
  4. 当到 FilterSecurityInterceptor 的时候会拿到 uri ,根据 uri 去找对应的鉴权管理器,鉴权管理器做鉴权工作,鉴权成功则Controller 层否则到 AccessDeniedHandler 鉴权失败处理器处理。
3、Spring Security 的优势和劣势

劣势:

  • 配置臃肿:
    • 如果是 SSM + Spring Security 的话,我觉得这话有一定道理。
    • 但是如果是 Spring Boot 项目的话,其实并不见得臃肿。Spring Boot 中,通过自动化配置 starter 已经极大的简化了 Spring Security 的配置,我们只需要做少量的定制的就可以实现认证和授权了。
  • Spring Security 中概念复杂
    • Spring Security 由于功能比较多,支持 OAuth2 等原因,就显得比较重量级,不像 Shiro 那样轻便。

优势:

  • 健全的web安全机制:
    • 在 Spring Security 中你会学习到许多安全管理相关的概念,以及常见的安全攻击。这些安全攻击,如果你不是 web 安全方面的专家,很多可能存在的 web 攻击和漏洞你可能很难想到,而 Spring Security 则把这些安全问题都给我们罗列出来并且给出了相应的解决方案。各种各样的安全攻击、各种各样的登录方式、各种各样你能想到或者想不到的安全问题,Spring Security 都给我们罗列出来了,并且给出了解决方案,从这个角度来看,你会发现 Spring Security 好像也是很有优势的。
  • 适配SpringBoot、SpringCloud全家桶,只需要简单的配置上会相对shiro节省很多代码量
    • 在微服务架构的项目中,我们可能使用 Eureka 做服务注册中心,默认情况下,Eureka 没有做安全管理,如果你想给 Eureka 添加安全管理,只需要添加 Spring Security 依赖,然后在application.properties 中配置一下用户名密码即可,Eureka 就自动被保护起来了,别人无法轻易访问;然后各个微服务在注册的时候,只需要把注册地址改为 **http://username :password@localhost:8080/eureka **即可。类似的例子还有 Spring Cloud Config 中的安全管理。
    • 在微服务这种场景下,如果你想用 Shiro 代替 Spring Security,那 Shiro 代码量绝对非常可观,Spring Security 则可以非常容易的集成到现在流行的 Spring Boot/Spring Cloud 技术栈中,可以和 Spring Boot、Spring Cloud、Spring Social、WebSocket 等非常方便的整合。

3.4.2、Shiro和Spring Security比较

  • Shiro比Spring更容易使用、实现和理解。
  • Spring Security更加知名的唯一原因是因为品牌名称“Spring”以简单而闻名,但讽刺的是很多人发现使用Spring Security很困难。但是,Spring Security却有更好的社区支持。
  • Apache Shiro与Spring Security处理密码学方面相比有一个额外的模块。
  • Spring Security 与Spring 结合地较好,如果项目用的springmvc ,使用起来很方便。但是如果项目中没有用到spring框架,那就不要考虑它了。
  • Shiro 功能强大、且简单、灵活。是Apache基金会下的项目,比较可靠,且不跟任何框架或者容器绑定,可以独立运行。

总结来说:shiro能实现的,Spring Security 基本都能实现,同时也更复杂和臃肿,但依赖于Spring体系,但是好是处适配Spring全家桶,集成上更加契合,在使用上安全上,比shiro略负责。

具体如何取舍呢?

  • 首先,如果是基于 Spring Boot/Spring Cloud 的微服务项目,Spring Security 无疑是最方便的。
  • 如果是就是普通的 SSM 项目,那么 Shiro 基本上也够用。
  • 另外,选择技术栈的时候,我们可能也要考虑团队内工程师的技术栈,如果工程师更擅长 Shiro,那么无疑 Shiro 是合适的,毕竟让工程师去学习一门新的技术,一来可能影响项目进度,而来也可能给项目埋下许多未知的雷。

3.5、对OAuth的实现

从上面的分析对比,无论是Shiro还是Spring Security,都是十几年前的产物,Shiro或者Spring Security很多功能都是为jsp量身定做的。但是现在都是前后端分离的项目,要做鉴权,基本都要遵循对OAuth标准的实现,同时也要去掉用不上的自带的登录页面,保留认证的核心内容。

似乎Spring Security的比较适配当前流行的Spring 全家桶的框架架构趋势;所以这里讲述Spring Security对OAuth2.0的实现。Spring-Security-OAuth2是对OAuth2的一种实现,虽然现在已经被spring 放弃,推出新的鉴权框架:Spring Authorization Server;但很多公司的系统,还是采用的Spring-Security-OAuth2来实现鉴权、web安全机制。

  • OAuth2.0的服务提供方涵盖两个服务,即授权服务 (Authorization Server,也叫认证服务) 和资源服务 (Resource Server),使用 Spring Security OAuth2 的时候你可以选择把它们在同一个应用程序中实现,也可以选择建立使用同一个授权服务的多个资源服务。
  • 授权服务 (Authorization Server)应包含对接入端以及登入用户的合法性进行验证并颁发token等功能,对令牌的请求端点由 Spring MVC 控制器进行实现,下面是配置一个认证服务必须要实现的endpoints:
    • AuthorizationEndpoint 服务于认证请求。默认 URL: /oauth/authorize 。
    • TokenEndpoint 服务于访问令牌的请求。默认 URL: /oauth/token 。资源服务 (Resource Server),应包含对资源的保护功能,对非法请求进行拦截,对请求中token进行解析鉴权等,下面的过滤器用于实现 OAuth 2.0 资源服务:
    • OAuth2AuthenticationProcessingFilter用来对请求给出的身份令牌解析鉴权。

从上图可以看出,Spring Security OAuth2相对于Spring Security多出了对应OAuth的四大角色:客户端,授权服务器,资源拥有者,资源服务器;但是这些主要是用户自己对架构的建模划分和设计;

Spring Security OAuth2在代码结构上,也就是多出了JWT token的实现,以及四大角色端点的配置。当然你要是不喜欢他的spring‐security‐jwt>实现的JWT,也可以只依赖Spring Security,自己借助其他的JWT实现框架,来实现JWT token相关的生成和校验。

JWT token主要需要实现已下几个功能组件:

  • JwtTokenProvider
    • 工具类,提供创建token、校验token是否合法的方法。
  • JwtAuthenticationFilter(重要)
    • 最为关键,用于校验请求中是否包含 JWT Token,并且从 token 中提取 username,最终封装成 UsernamePasswordAuthenticationToken,并将它保存到 security 上下文中,供后续 spring security 认证使用。

除了自己实现JWT token,Spring Security这部分还需要自己实现以下几个角色:

  • LoginSuccessHandler:

登录成功后的处理器,因为自带的两个实现类肯定不符合登录成功后的指定跳转,所以这个角色必须要自己实现;

  • LoginFailureHandler:

登录失败后的处理,SSM+jsp老项目都是用xml配置form-login中的各个属性:authentication-failure-url

 <form-login login-page="/login.html" 
        login-processing-url="/j_spring_security_check"
        default-target-url="/user/homePage.html"
        username-parameter="j_username"  
        password-parameter="j_password"
        always-use-default-target="false" 
        authentication-failure-url="/login.html?login_error=1" 
        authentication-success-handler-ref="loginSuccessHandler" />
        
<logout invalidate-session="true" 
      logout-success-url="/login.html"  
        logout-url="/j_spring_security_logout" />

所以勉强可以不需要一定要自己去实现,但是在springboot springCloud中,还是配置一下LoginFailureHandler登录失败后的处理器

  • LogoutSuccessHandler:登出成功后的处理器同上
  • AuthenticationEntryPoint:认证失败后的处理器
  • UserDetails:用户信息框架自带一套的实现,比如有UserModel,最好自己实现
  • UserDetailsService:用户信息服务必须实现
  • WebSecurityConfigurer(重要):代替传统xml配置
    • spring security 整体配置类,继承 WebSecurityConfigurerAdapter 类,并重写 configure(HttpSecurity http) 和 configure(AuthenticationManagerBuilder auth) 方法。
    • 在 configure(HttpSecurity http) 方法中,可以配置放行的接口(无需认证)、登录成功/登录失败/登出成功/认证失败的处理器、登录/登出请求、jwt认证过滤器等内容。
    • configure(AuthenticationManagerBuilder auth) 方法中配置认证管理器和密码加密器。

以上就是spring security需要自己实现的部分。

四、实现分布式系统认证、授权功能

4.1、需求分析

1、UAA认证服务负责认证授权。

2、所有请求经过 网关到达微服务

3、网关负责鉴权客户端以及请求转发

4、网关将token解析后传给微服务,微服务进行授权。

4.2、注册中心

所有微服务的请求都经过网关,网关从注册中心读取微服务的地址,将请求转发至微服务。注册中心选取nacos。

spring:
  application:
    name: maintenance
  cloud:
    contentId:
    nacos:
#      username: naocs
#      password: nacos
      config:
        server-addr: 127.0.0.1:8848 # 本地的nacos 服务器
        group: ${spring.application.name}
        prefix: application
        file-extension: yml
        namespace: e65e8570-9350-40e2-ac41-9b1106d47e1f
      discovery:
        server-addr: localhost:8848
        register-enabled: false
        group: ${spring.application.name}
        namespace: e65e8570-9350-40e2-ac41-9b1106d47e1f

pom.xml依赖:


       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
       </dependency>

<!--       <dependency>
           <groupId>com.alibaba.nacos</groupId>
           <artifactId>nacos-client</artifactId>
       </dependency>-->

       <dependency>
           <groupId>com.alibaba.cloud</groupId>
           <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
       </dependency>

       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-starter-openfeign</artifactId>
       </dependency>

4.3、网关

API网关在认证授权体系里主要负责两件事:

  1. 作为OAuth2.0的资源服务器角色,实现接入方权限拦截。
  2. 令牌解析并转发当前登录用户信息(明文token)给微服务

微服务拿到明文token(明文token中包含登录用户的身份和权限信息)后也需要做两件事:

  1. 用户授权拦截(看当前用户是否有权访问该资源)
  2. 将用户信息存储进当前线程上下文(有利于后续业务逻辑随时获取当前用户信息)

        网关和资源服务都做权限拦截吗?的确是,但是网关那里主要是校验token是否合法,过期等。而资源服务这里才是真正的校验用户的权限是否可以访问该资源的某个API(根据访问地址的uri)

还有另外一种方案:认证服务负责认证,网关负责校验认证和鉴权,其他API服务负责处理自己的业务逻辑。安全相关的逻辑只存在于认证服务和网关服务中,其他服务只是单纯地提供服务而没有任何安全相关逻辑。

        以上两种方案都需要确保资源服务的地址绝对保密和安全,尤其第二种;比如只对网关做对外安全,其他的资源服务器一律杜绝外网访问,只对网关所在的服务器开放内网访问的权限,并做好一系列的网络安全措施。

4.4、服务划分:

        为了避免网关这里拦截请求,解析token并验证token是否有效,然后请求发到其他微服务或者api模块的web访问接口的时候,需要再次拦截请求,提取token明文中的用户信息,然后校验用户的权限,导致有2次请求,这也会导致请求的效率变慢,所以采用上述的第二种方案,所有的token校验,和权限校验都放在网关模块来做。

  • oauth2-gateway:网关服务,负责请求转发和鉴权功能,整合Spring Security+Oauth2;
  • oauth2-auth:Oauth2认证服务,负责对登录用户进行认证,整合Spring Security+Oauth2;
  • oauth2-api:受保护的API服务,用户鉴权通过后可以访问该服务,不整合Spring Security+Oauth2。

4.4.1、auth服务的创建

oauth2-auth建认证服务:

        在pom.xml中添加相关依赖,主要是Spring Security、Oauth2、JWT、Redis相关依赖;

        redis可以根据用户id缓存用户的所有权限,采用hash的存储结构,每个field存储一个资源服务名称,value存储该该用户在服务下所有有权限的api的列表

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
   <!--自己的项目-->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>oauth2-auth</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.4.RELEASE</version>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

    </dependencies>
</project>
  • 在application.yml中添加相关配置,主要是Nacos和Redis相关配置;
server:
  port: 9081
spring:
  profiles:
    active: dev
  application:
    name: oauth2-auth
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss
  redis:
    database: 1
    port: 6379
    host: localhost
    password:
management:
  endpoints:
    web:
      exposure:
        include: "*"

使用keytool生成RSA证书jwt.jks,复制到resource目录下,在JDK的bin目录下使用如下命令即可;

    keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks

创建UserServiceImpl类实现Spring Security的UserDetailsService接口,用于加载用户信息;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {


    private static final String REGEXP = "^1([34578])\\d{9}$";
    @Resource
    private UserService userService;

    @Resource
    private PermissionService permissionService;


    @Override
    public UserDetails loadUserByUsername(String login) throws AuthenticationException {
        User user;
        if (login.matches(REGEXP)) {
            user = userService.findByMobile(login);
        } else {
            user = userService.findById(login);
        }
        //登录账号不存在
        UserModel userdetail = new UserModel();
        if (user == null) {
            throw new UsernameNotFoundException(login);
            //登录账号状态 1 正常 0 冻结
        } else if (user.getEnableState() == 0) {
            throw new LockedException("该账号已冻结");
        }
        //加载用户权限
        Collection<GrantedAuthority> grantedAuths = obtionGrantedAuthorities(user);
//        boolean enables = true;
//        boolean accountNonExpired = true;
//        boolean credentialsNonExpired = true;
//        boolean accountNonLocked = true;
        //将user转化为userDetails的实现类
        BeanUtils.copyProperties(user, userdetail);
        userdetail.setAuthorities(grantedAuths);
        return userdetail;
    }

    /**
     * load user Authority
     *
     * @param user DB t_user Mapping class
     * @return
     */
    private Set<GrantedAuthority> obtionGrantedAuthorities(User user) {
        Set<GrantedAuthority> authSet = new HashSet<>();
        List<Permission> funcs = null;

        funcs = permissionService.findAllByUserId(user.getId());

        if (funcs != null) {
            for (Permission func : funcs) {
                authSet.add(new SimpleGrantedAuthority(func.getCode() + "_" + func.getDescription()));
            }
        }

        return authSet;
    }
}

如果你想往JWT中添加自定义信息的话,比如说登录用户的ID,可以自己实现TokenEnhancer接口;

提供的令牌端点(Token Endpoint)如下:

  • /oauth/authorize:授权端点
  • /oauth/token:令牌端点
  • /oauth/confirm_access:用户确认授权提交端点
  • /oauth/error:授权服务错误信息端点
  • /oauth/check_token:用于资源服务访问的令牌解析端点
  • /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
public class TokenConfig {

    private final static String SIGN_ALGORITHM = "HMACSHA512";

    @Resource
    private Properties authProperties;

    @Resource
    private AccessTokenConverter accessTokenConverter;

    @Resource
    private KeyPair keyPair;



    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter);
    }

    @Bean
    public AccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        //配置JWT使用的秘钥
        jwtAccessTokenConverter.setSigningKey(authProperties.getProperty("jwtSecret"));
        // 重点!!!
        jwtAccessTokenConverter.setSigner((Signer) new MacSigner(SignatureAlgorithm.valueOf(SIGN_ALGORITHM),
                new SecretKeySpec(DatatypeConverter.parseBase64Binary(authProperties.getProperty("jwtSecret")), SIGN_ALGORITHM)));
        jwtAccessTokenConverter.setVerifier((SignatureVerifier) new MacSigner(SignatureAlgorithm.valueOf(SIGN_ALGORITHM)
                , new SecretKeySpec(DatatypeConverter.parseBase64Binary(authProperties.getProperty("jwtSecret")), SIGN_ALGORITHM)));
        jwtAccessTokenConverter.setKeyPair(keyPair);
        return jwtAccessTokenConverter;
    }

    /**
     * 使用非对称加密算法来对Token进行签名
     * @return
     */
    @Bean
    public KeyPair keyPair() {
        //从classpath下的证书中获取秘钥对
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("jwt.jks"), "123456".toCharArray());
        return keyStoreKeyFactory.getKeyPair("jwt", "123456".toCharArray());
    }
}

 TokenEnhancer的实现

@Component
public class JwtTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        UserModel securityUser = (UserModel) authentication.getPrincipal();
        Map<String, Object> info = new HashMap<>();
        //把用户ID设置到JWT中
        info.put("id", securityUser.getId());
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

1.编写一个配置类: 继承AuthorizationServerConfigurerAdapter,

​ 开启认证配置服务标签:@EnableAuthorizationServer;

2.复写三个配置

​ 2.1 客户端详情配置:数据库创建一张表:auth_client_details 插入两条数据;

​ 定义一个JdbcClientDetailsService方法,需要两个参数:datasource,passwordEncoder;

​ 2.2 服务端点配置:

​ ①:配置认证管理器:在Security配置类中配置authenticationManager

​ ②:授权服务配置器:a:数据库创建一张表 auth_code

​ b:定义方法 JdbcAuthorizationCodeServices 需要一个参数datasource;

​ ③:令牌服务配置:new DefaultTokenService()

​ a: 设置客户端详情,

​ b: 设置token刷新,

​ c:设置TokenStore

​ d:Token 加强 ,令牌转换器

​ ④:允许post方式请求;

2.3 服务安全配置 ;check_token放行

@AllArgsConstructor
@Configuration
@EnableAuthorizationServer
public class OAuth2ServerConfig extends AuthorizationServerConfigurerAdapter {
    @Resource
    private DataSource dataSource;
    @Resource
    private PasswordEncoder passwordEncoder;
    @Resource
    private UserDetailsServiceImpl userDetailsService;
    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private AccessTokenConverter accessTokenConverter;
    @Resource
    private  JwtTokenEnhancer jwtTokenEnhancer;
    @Resource
    private TokenStore tokenStore;


    //1.1.注册客户端详情Bean,基于数据库,自动操作表:oauth_client_details
    @Bean
    public JdbcClientDetailsService jdbcClientDetailsService(){
        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        //数据库的秘钥使用了PasswordEncoder加密
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    //2.1.定义授权码服务,连接数据库 oauth_code
    @Bean
    public JdbcAuthorizationCodeServices jdbcAuthorizationCodeServices(){
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    //2.2.令牌服务配置
    //令牌的管理服务
    @Bean
    public AuthorizationServerTokenServices tokenService(){
        //创建默认的令牌服务
        DefaultTokenServices services = new DefaultTokenServices();
        //指定客户端详情配置
        services.setClientDetailsService(jdbcClientDetailsService());
        //支持产生刷新token
        services.setSupportRefreshToken(true);
        //token存储方式
        services.setTokenStore(tokenStore);

        //设置token增强 - 设置token转换器
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList((JwtAccessTokenConverter)accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);  //jwtAccessTokenConverter()

        return services;
    }



    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
//                .withClientDetails(jdbcClientDetailsService())
                .inMemory()
                .withClient("gateway")
                .scopes("al l")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(86400);
//               .withClient("orderApp")  //声明访问认证服务器的客户端
//                .secret(passwordEncoder.encode("123456"))  //客户端访问认证服务器需要带上的密码
//                .scopes("read","write")  //获取token包含的哪些权限
//                .accessTokenValiditySeconds(3600)  //token过期时间
//                .resourceIds("order-service")  //指明请求的资源服务器
//                .authorizedGrantTypes("password")  //密码模式
//                .and()
//                //资源服务器拿到了客户端请求过来的token之后会请求认证服务器去判断此token是否正确或者过期
//                //所以此时的资源服务器对于认证服务器来说也充当了客户端的角色
//                .withClient("order-service")
//                .secret(passwordEncoder.encode("123456"))
//                .scopes("read")
//                .accessTokenValiditySeconds(3600)
//                .resourceIds("order-service")
//                .authorizedGrantTypes("password");
        //把客户端的信息以及token都存储在数据库中
            clients.jdbc(dataSource);

    }


    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> delegates = new ArrayList<>();
        delegates.add(jwtTokenEnhancer);
        enhancerChain.setTokenEnhancers(delegates); //配置JWT的内容增强器
        endpoints.authenticationManager(authenticationManager)
                //2.授权码模式服务
                .authorizationCodeServices(jdbcAuthorizationCodeServices())
                //3.配置令牌管理服务
                .tokenServices(tokenService())
                //允许post方式请求
                .allowedTokenEndpointRequestMethods(HttpMethod.POST)
                .userDetailsService(userDetailsService) //配置加载用户信息的服务
                .accessTokenConverter(accessTokenConverter)
                .tokenEnhancer(enhancerChain);
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 自定义异常处理端口
//        security.authenticationEntryPoint(customAuthenticationEntryPoint);
//        security.accessDeniedHandler(customAccessDeniedHandler);
        security
                // oauth/token_key
//                .tokenKeyAccess("permitAll()")
                .tokenKeyAccess("isAuthenticated()")
                // oauth/check_token
                .checkTokenAccess("isAuthenticated()")
                // 允许客户表单认证
                .allowFormAuthenticationForClients();
    }

}

SecurityConfig:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //密码加密器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //授权规则配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //授权配置
        http.csrf().disable()   //屏蔽跨域防护
                .authorizeRequests()          //对请求做授权处理
                .antMatchers("/login").permitAll()  //登录路径放行
                // .antMatchers("/login.html").permitAll()//对登录页面跳转路径放行
                .anyRequest().authenticated() //其他路径都要拦截
                .and().formLogin()  //允许表单登录, 设置登陆页
                .successForwardUrl("/loginSuccess") // 设置登陆成功页(对应//controller),一定要有loginSuccess这个路径
                .and().logout().permitAll();    //登出
    }

    //配置认证管理器,授权模式为“poassword”时会用到
    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

}

4.4.2、oauth2-gateway网关

在pom.xml中添加相关依赖,主要是Gateway、Oauth2和JWT相关依赖;

<!--gateway网关-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>4.0.3</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
    <version>6.0.2</version>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
    <version>6.0.2</version>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!--<dependency>
           <groupId>com.alibaba.nacos</groupId>
           <artifactId>nacos-client</artifactId>
</dependency>-->

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在application.yml中添加相关配置,主要是路由规则的配置、Oauth2中RSA公钥的配置及路由白名单的配置;

server:
  port: 9091
spring:
  profiles:
    active: dev
  application:
    name: oauth2-gateway
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes: #配置路由规则
        - id: oauth2-api-route
          uri: lb://oauth2-api
          predicates:
            - Path=/api/**
          filters:
            - StripPrefix=1
        - id: oauth2-auth-route
          uri: lb://oauth2-auth
          predicates:
            - Path=/auth/**
          filters:
            - StripPrefix=1
      discovery:
        locator:
          enabled: true #开启从注册中心动态创建路由的功能
          lower-case-service-id: true #使用小写服务名,默认是大写
  security:
    oauth2:
      resourceserver:
        jwt:
          jwk-set-uri: 'http://localhost:9001/rsa/publicKey' #配置RSA的公钥访问地址
  redis:
    database: 0
    port: 6379
    host: localhost
    password:
secure:
  ignore:
    urls: #配置白名单路径
      - "/actuator/**"
      - "/auth/oauth/
  • 对网关服务进行配置安全配置,由于Gateway使用的是WebFlux,所以需要使用@EnableWebFluxSecurity注解开启;
  • 在WebFluxSecurity中自定义鉴权操作需要实现ReactiveAuthorizationManager接口;
@Component
public class AuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {
    
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    
    @Override
    public Mono<AuthorizationDecision> check(Mono<Authentication> mono, AuthorizationContext authorizationContext) {
        //从Redis中获取当前用户的权限(uri路径是否包含该URI)
        URI uri = authorizationContext.getExchange().getRequest().getURI();
       //....
    }

}

这里我们还需要实现一个全局过滤器AuthGlobalFilter,当鉴权通过后将JWT令牌中的用户信息解析出来,然后存入请求的Header中,这样后续服务就不需要解析JWT令牌了,可以直接从请求的Header中获取到用户信息。

@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
 
    private static Logger LOGGER = LoggerFactory.getLogger(AuthGlobalFilter.class);
 
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getHeaders().getFirst("Authorization");
        if (StringUtils.isEmpty(token)) {
            return chain.filter(exchange);
        }
        try {
            //从token中解析用户信息并设置到Header中去
            String realToken = token.replace("Bearer", "");
            JWSObject jwsObject = JWSObject.parse(realToken);
            String userStr = jwsObject.getPayload().toString();
            LOGGER.info("AuthGlobalFilter.filter() user:{}",userStr);
            ServerHttpRequest request = exchange.getRequest().mutate().header("user", userStr).build();
            exchange = exchange.mutate().request(request).build();
        } catch (java.text.ParseException e) {
            // ...    
        }
        return chain.filter(exchange);
    }
 
    @Override
    public int getOrder() {
        return 0;
    }
}

当为登录或者token无效时访问接口,自定义返回结果

@Slf4j
@Component
public class JwtAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {

    @Override
    public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.FORBIDDEN);
        response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");

        HashMap<String, String> map = new HashMap<>();
       // 填写拒绝信息内容
        log.error("access forbidden path={}", exchange.getRequest().getPath());

        DataBuffer dataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(map));
        return response.writeWith(Mono.just(dataBuffer));
    }
}

4.4.3、api模块

        所有的访问控制层都在这个模块,不需要做任何权限验证,全靠网关来做鉴权,且保证web访问的安全,因为它不对外网服务器暴露地址和端口,它是否提供访问服务,全看网关那里是否转发请求过来。

在pom.xml中添加相关依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 在application.yml添加相关配置,很常规的配置:

server:
  port: 2113
spring:
  profiles:
    active: dev
  application:
    name: oauth2-api
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
management:
  endpoints:
    web:
      exposure:
        include: "*"

五、总结

        相对传统的共享session(集中存储),缓存(redis)存储token,在安全和性能方面已经改进很多,我的第一份后端开发工作中架构在认证、鉴权方面采用的就是这种方案。但是需要在缓存服务器缓存所有用户的web端、手机端(安卓和苹果)的token(与token绑定的参数还有设备ID),每次校验token是否有效就需要到缓存中去读取数据,IO和网络的消耗相对JWT Token的只做解密计算校验token方式还是要大很多的,另外对缓存硬件资源的要求也比较高。但是这种方式相对JWT也有一个好处就是可以灵活精准控制,部分或者单独的用户的token提前过期失效掉。

        而且当时的OAuth的实现并不是使用的是Spring_Security_OAuth2.0,而是采用的是Shiro,相比起来,确实比Spring_Security_OAuth2.0要轻很多(即使都是通过springboot的starter来简化配置),没有这么一大堆的配置和实现类需要做。虽然有些针对web的安全访问控制,比如只允许某个别客户端访问认证服务,但是这些保护配置,通过内网访问,屏蔽外网,配合注册中心和配置中心来实现也比较好做。

        这个项目的受众是医疗体系的用户,除了功能权限还做了数据权限的控制,虽然对权限的控制要求比较严格,但其实token的过期的控制要求并不需要那么精准,因为用户的token过期时间用户自己要求的还是比较长的,他们也不喜欢老是去登录;看起来也没有特别严格的要求,如果有人要刻意泄露信息,还是有很多途径,一个token过期因素影响并不大。再者医疗体系的员工调动并不是特别频繁。完全可以采取全部token过期的方式就好了。当然肯定有架构师优先采用自己熟悉的架构的因素。

更多资源分享,请关注我的公众号:搜索或扫码 砥砺code

六、附:

HMAC算法

        HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code)。 HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。也就是说HMAC通过将哈希算法(SHA1, MD5)与密钥进行计算生成摘要。

HMAC与哈希算法一起使用。

HMAC的计算公式为:

HMAC(K,M)=H(K⊕opad∣H(K⊕ipad∣M))

运算步骤:

(1)检查密钥K的长度。如果K的长度大于B则先使用摘要算法计算出一个长度为L的新密钥。如果K的长度小于B,则在其后面追加0来使其长度达到B。

(2)将上一步生成的B字长的密钥字符串与ipad做异或运算,得到比特序列ipadkey。

(3)将ipadkey附加在消息M的开头;

(4)使用哈希函数H计算第3步中生成的数据流的信息摘要值。

(5) 将第1步生成的B字长密钥字符串与opad做异或运算,得到opadkey。

(6)再将第4步得到的结果填充到opadkey之后。

(7)使用哈希函数H计算第6步中生成的数据流的信息摘要值,输出结果就是最终的HMAC值。

Hmac代码实现

对hmac-sha256 加密使用示例

#define B SHA256_CBLOCK
#define L (SHA256_DIGEST_LENGTH)
#define K (SHA256_DIGEST_LENGTH * 2)

#define I_PAD 0x36
#define O_PAD 0x5C

/*
 * HMAC(H, K) == H(K ^ opad, H(K ^ ipad, text))
 *
 *    H: Hash function (sha256)
 *    K: Secret key
 *    B: Block byte length
 *    L: Byte length of hash function output
 *
 * https://tools.ietf.org/html/rfc2104
 */
void hmac_sha256(const unsigned char *key, int key_len, const unsigned char *d, size_t n,
                 unsigned char *md, unsigned int *md_len) {
    assert(key);
    assert(d);
    assert(md);

    if (*md_len < SHA256_DIGEST_LENGTH){
        return;
    }

    SHA256_CTX shaCtx;
    uint8_t kh[SHA256_DIGEST_LENGTH];

    /*
     * If the key length is bigger than the buffer size B, apply the hash
     * function to it first and use the result instead.
     */
    if (key_len > B) {
        SHA256_Init(&shaCtx);
        SHA256_Update(&shaCtx, key, key_len);
        SHA256_Final(kh, &shaCtx);
        key_len = SHA256_DIGEST_LENGTH;
        key = kh;
    }

    /*
     * (1) append zeros to the end of K to create a B byte string
     *     (e.g., if K is of length 20 bytes and B=64, then K will be
     *     appended with 44 zero bytes 0x00)
     * (2) XOR (bitwise exclusive-OR) the B byte string computed in step
     *     (1) with ipad
     */
    uint8_t kx[B];
    for (size_t i = 0; i < key_len; i++) kx[i] = I_PAD ^ key[i];
    for (size_t i = key_len; i < B; i++) kx[i] = I_PAD ^ 0;

    /*
     * (3) append the stream of data 'text' to the B byte string resulting
     *     from step (2)
     * (4) apply H to the stream generated in step (3)
     */
    SHA256_Init(&shaCtx);
    SHA256_Update(&shaCtx, kx, B);
    SHA256_Update(&shaCtx, d, n);
    SHA256_Final(md, &shaCtx);

    /*
     * (5) XOR (bitwise exclusive-OR) the B byte string computed in
     *     step (1) with opad
     *
     * NOTE: The "kx" variable is reused.
     */
    for (size_t i = 0; i < key_len; i++) kx[i] = O_PAD ^ key[i];
    for (size_t i = key_len; i < B; i++) kx[i] = O_PAD ^ 0;

    /*
     * (6) append the H result from step (4) to the B byte string
     *     resulting from step (5)
     * (7) apply H to the stream generated in step (6) and output
     *     the result
     */
    SHA256_Init(&shaCtx);
    SHA256_Update(&shaCtx, kx, B);
    SHA256_Update(&shaCtx, md, SHA256_DIGEST_LENGTH);
    SHA256_Final(md, &shaCtx);

    *md_len = SHA256_DIGEST_LENGTH;
}

JWT

什么是 JWT

        JSON Web Token(JWT)是一个开发标准(RFC 7519),它定义了一种紧凑独立的基于 JSON 对象在各方之间安全地传输信息的方式。这些信息可以被验证和信任,因为它是数字签名的。JWT 可以使用一个密钥(HMAC算法),或使用 RSA 的公钥/私钥密钥对对信息进行签名。

  • 紧凑

由于其较小的体积,JWTs 可以通过 URL、POST 参数或 HTTP 头部参数进行传递,体积小也意味着其传输速度会相当快。

  • 独立

有效负载包含了所需要的关于用户的所有信息,避免了多次查询数据库的需要。

JWT 的数据结构

JWT 由以下三部分,每部分之间用(.)分隔:

      a. Header(头部)

      b. Payload(载荷)

      c. Signature(签名)

因此,JWT 通常看起来如下:

xxxxx.yyyyy.zzzzz

JWT 的头部承载两部分信息:

  • 声明类型,这里是 JWT
  • 声明加密的算法 通常直接使用 HMAC SHA256、RSA

完整的头部就像下面这样的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
playload

JWT 的第二部分,由声明构成。声明是对实体和其他信息的描述,声明可以分成三大类,

       a. 标准中注册的声明

       b. 公共的声明

       c. 私有的声明

标准中注册的声明 (建议但不强制使用) :

  1. iss(Issuer): JWT 签发者
  2. sub(Subject): JWT 所面向的用户
  3. aud(Audience): 接收 JWT 的一方
  4. exp(Expiration Time): JWT 的过期时间,这个过期时间必须要大于签发时间
  5. nbf(Not Before): 定义在什么时间之前,该 JWT 都是不可用的
  6. iat(Issued At): JWT 的签发时间
  7. jti(JWT ID): JWT 的唯一身份标识,主要用来作为一次性 token,从而回避重放攻击

公共的声明:

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息。但不建议添加敏感信息,因为该部分在客户端可解密。

私有的声明:

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 Base64Url 是对称解密的,意味着该部分信息可以归类为明文信息。

例如:

{
    "sub": "1234567890",
    "name": "John Doe",
    "admin": true
}

然后将其进行base64加密,得到Jwt的第二部分:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
signature

签名用于验证 JWT 的发送者是谁,并确保消息在过程中不会被篡改。创建签名部分,你需要用到编码后的 header、编码后的 payload、密钥、在 header 中指定的算法。

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串(头部在前),然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

UQmqAUhUrpDVV2ST7mZKyLTomVfg7sYkEjmdDI5XF8Q

密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和验证,所以需要保护好。

如下使用 HMAC SHA256 算法创建签名的方式:
HMACSHA256(base64UrlEncode(header) +  "."  + base64UrlEncode(payload), secret)
  • 最后一步签名的过程,实际上是对头部以及载荷内容进行签名。一般而言,加密算法对于不同的输入产生的输出总是不一样的。对于两个不同的输入,产生同样的输出的概率极其地小(有可能比我成世界首富的概率还小)。所以,我们就把“不一样的输入产生不一样的输出”当做必然事件来看待吧。
  • 所以,如果有人对头部以及载荷的内容解码之后进行修改,再进行编码的话,那么新的头部和载荷的签名和之前的签名就将是不一样的。而且,如果不知道服务器加密的时候用的密钥的话,得出来的签名也一定会是不一样的。
  • 服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名。那么服务器应用是怎么知道我们用的是哪一种算法呢?别忘了,我们在JWT的头部中已经用alg字段指明了我们的加密算法了。
  • 如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。
JWT的优点
  1. 因为 JSON 数据格式的通用性,所以JWT是可以跨语言的,主流语言都可以支持。
  2. payload 部分可以存储其他业务逻辑所必要的非敏感信息。
  3. JWT 构成简单,字节占用很小,所以非常便于传输的。
  4. 不需要在服务端保存会话信息,易于应用的扩展和安全等。

CSRF:因为不再依赖于Cookie,所以你就不需要考虑对CSRF(跨站请求伪造)的防范。

性能: 一次网络往返时间(通过数据库查询session信息)总比做一次HMACSHA256计算 的Token验证和解析要费时得多。

不需要为登录页面做特殊处理: 如果你使用Protractor 做功能测试的时候,不再需要为登录页面做特殊处理。

基于标准化:你的API可以采用标准化的 JSON Web Token (JWT). 这个标准已经存在多个后端库(.NET, Ruby, Java,Python, PHP)和多家公司的支持(如:Firebase,Google, Microsoft)。

因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。

便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。

它不需要在服务端保存会话信息, 所以它易于应用的扩展。

JWT的缺点:
  • 安全性

由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。

  • 性能

jwt太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用jwt的http请求比使用session的开销大得多。

  • 一次性
    • 无状态是jwt的特点,但也导致了这个问题,jwt是一次性的。想修改里面的内容,就必须签发一个新的jwt。
    • 无法废弃 通过上面jwt的验证机制可以看出来,一旦签发一个jwt,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的jwt,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。
    • 续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变jwt的有效时间,就要签发新的jwt。最简单的一种方式是每次请求刷新jwt,即每个http请求都返回一个新的jwt。这个方法不仅暴力不优雅,而且每次请求都要做jwt的加密解密,会带来性能问题。另一种方法是在redis中单独为每个jwt设置过期时间,每次访问时刷新jwt的过期时间。

        可以看出想要破解jwt一次性的特性,就需要在服务端存储jwt的状态。但是引入 redis 之后,就把无状态的jwt硬生生变成了有状态了,违背了jwt的初衷。而且这个方案和session都差不多了。

JWT的使用注意
  1. 不要在 payload 存放敏感信息,因为该部分是可解密的。
  2. 保存好 secret 私钥十分重要。
  3. 尽量使用 https 协议
JWT 等待解决的问题
  1. JWT 的最大缺点是,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  2. JWT 续约问题。
JWT 适用场景

有效期短

只希望被使用一次

比如,用户注册后发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户,一次性的。这种场景就适合使用jwt。

而由于jwt具有一次性的特性。有要求随时控制token强制过期的单点登录和会话管理非常不适合用jwt,如果在服务端部署额外的逻辑存储jwt的状态,那还不如使用session。基于session有很多成熟的框架可以开箱即用,但是用jwt还要自己实现逻辑。

  • 认证

用户一旦登录,之后的每一个请求都会带上这个 JWT ,用来访问该 token 权限下的路由,服务和资源。由于 JWT 的开销小,能解决跨域问题等特点,被广泛的应用于不需要强控制过期的 SSO。

  • 信息交换

JSON Web 能给在客户端和服务端之间安全地传输信息。 通过使用非对称加密签名技术,可以对客户端进行签名验签。此外,可以使用标头和有效负载计算签名,您还可以验证内容是否未被篡改。

  • 31
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 好的,我可以为您提供关于Spring Security OAuth 2.0实现的中文回答。 Spring Security OAuth 2.0Spring Security框架的一部分,它为应用程序提供了OAuth 2.0认证和授权的支持。OAuth 2.0是一种常用的授权协议,它允许用户授权第三方应用程序访问他们的受保护资源。Spring Security OAuth 2.0实现OAuth 2.0协议中的授权服务器和资源服务器两个角色。 要使用Spring Security OAuth 2.0,您需要完成以下步骤: 1.添加Spring Security OAuth 2.0依赖项:在Maven或Gradle项目中,您需要添加Spring Security OAuth 2.0的依赖项。例如,在Maven项目中,您可以添加以下依赖项: ```xml <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.6.RELEASE</version> </dependency> ``` 2.配置OAuth 2.0客户端:您需要配置OAuth 2.0客户端,以便您的应用程序可以访问受保护的资源。这通常涉及到注册应用程序并获取客户端ID和客户端密钥。 3.配置授权服务器:您需要配置授权服务器,以便用户可以授权应用程序访问他们的受保护资源。授权服务器可以与您的应用程序集成,也可以作为单独的服务运行。 4.配置资源服务器:您需要配置资源服务器,以便它可以验证访问令牌并授予对受保护资源的访问权限。 5.保护受保护的资源:您需要配置Spring Security以保护受保护的资源,并确保只有经过身份验证和授权的用户可以访问它们。 以上是使用Spring Security OAuth 2.0实现OAuth 2.0认证和授权所需的主要步骤。如果您需要更详细的指导,请参考Spring Security OAuth 2.0的官方文档。 ### 回答2: Spring Security OAuth2.0是一个基于Spring框架的安全认证和访问控制框架,它实现OAuth2.0协议并提供了OAuth2.0客户端和服务器的实现,可以帮助Spring应用程序保护和管理访问资源。 Spring Security OAuth2.0实现主要包括客户端和服务器的认证和授权过程。在客户端认证过程中,首先会发送请求获取访问令牌,然后将访问令牌发送给资源服务器以访问所需的资源。在服务器认证过程中,首先要检验客户端是否有访问资源的权限,如果有则颁发授权码或访问令牌,否则返回错误信息。 在实现中,Spring Security OAuth2.0主要涉及到四个角色:资源拥有者、客户端、授权服务器和资源服务器。资源拥有者可以是用户,识别资源拥有者是通过认证授权服务器来完成的。客户端是向授权服务器申请OAuth2.0访问令牌的应用程序。授权服务器是用来对客户端进行身份验证和授权的服务器,它可以使用多种身份验证方式和批准策略来验证客户端的请求,然后授权它访问所需的资源。资源服务器是维护、提供API的服务器,它可以验证OAuth2.0访问令牌,然后允许或拒绝客户端的请求。 Spring Security OAuth2.0框架中提供了一些接口和类来实现OAuth2.0的认证和授权过程。例如,OAuth2AuthenticationProcessingFilter是用于授权客户端访问资源的过滤器,它首先对客户端的访问请求进行身份验证,然后检查是否有访问资源的权限。如果客户端有访问权限,则颁发访问令牌。 在实际使用中,Spring Security OAuth2.0可以与其他技术栈集成,例如Spring Boot、Spring Cloud、JavaEE等,可以实现用户级别、角色级别、API级别、组织级别等多种细粒度的访问控制方式,从而帮助企业实现灵活的访问控制。同时,Spring Security OAuth2.0框架也提供了一些扩展配置和插件,可以根据企业自身需求进行二次开发和定制,实现更加高效和安全的应用程序。 ### 回答3: Spring Security是一个功能强大的安全框架,提供了基于应用程序的安全性控制和身份验证机制。而OAuth2.0则是一种允许用户使用第三方应用程序访问资源的框架,并且该框架将安全性设计为一个核心功能。 Spring SecurityOAuth2.0可以结合使用来提高应用程序的安全性。Spring Security OAuth2.0是一个非常特殊的模块,它为OAuth2.0提供了完整的实现。 使用Spring Security OAuth2.0,我们可以实现以下功能: 1. 身份验证和授权管理:提供授权服务器和资源服务器来进行身份验证和授权的管理。 2. Token管理:生成和管理OAuth2.0令牌以确保安全性。 3. 支持多种授权类型:支持授权码模式、客户端模式、密码模式、隐式模式等多种授权类型,实现不同场景下的资源访问控制。 4. 集成Spring Boot:Spring Security OAuth2.0被设计成与Spring Boot高度集成,方便易用。 5. 提供公开API:提供了一组公开API,方便第三方应用程序的接入。 Spring SecurityOAuth2.0的集成需要我们做以下几个步骤: 1. 在Spring Security配置文件中添加OAuth2.0配置。 2. 定义安全领域对象,包括用户、角色和授权管理等。 3. 实现OAuth2.0的授权服务器和资源服务器。 4. 配置OAuth2.0客户端,使其可以访问受保护的资源。 总之,Spring Security OAuth2.0能够帮助开发者集成身份验证和授权管理功能,并为第三方应用程序提供安全的访问资源方式。它为应用程序提供了更高级别的安全性和可扩展性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值