如何实现一套简单的oauth2授权码类型认证,一些思路,供参考
背景
组内人不少,今年陆陆续续研发了不少系统,一般都会包括一个后台管理系统,现在问题是,每个管理系统都有RBAC那一套用户权限体系,实在是有点浪费人力,于是今年我们搞了个统一管理各个应用系统的RBAC的系统,叫做应用权限中心,大致就是:
- 各个应用在我们系统注册,并录入应用支持的各类权限(如菜单权限、数据权限、接口权限等);
- 统一管理所有用户(包括公司员工、合作伙伴员工等);
- 各个接入系统的管理员可以在自己的应用建立角色,赋予其某些权限;接下来,又可以给人员分配这些角色。
在以上数据维护完成后,就可以由我们系统提供oauth2认证这一套体系,oauth2简单理解,类似于平时那些网站的第三方渠道登录,比如,第一次去到一个陌生网站,不想注册用户、密码那些,此时,如果网站支持微信、qq、google、github等登录方式,就很方便,登录完成后,这个网站就已经知道我们是谁了,知道我们是谁之后,再来做权限也就简单了,还是RBAC那一套。
由于我们的应用权限中心管理了多个应用的权限,所以可以给某个人分配各个系统下的角色,这个人也就有了各个系统下的权限。
oauth2那一套,是在用户完成身份认证的基础下才能走完整个流程的,那就是说,已经知道这个用户是谁了,那就可以去应用权限中心获取这个人在里各个应用下有哪些角色,有哪些权限了。
oauth2的整体数据流
oauth2这个东西,流程图比较复杂,我就不从这方面去讲了,我先说下大体思路,然后直接给大家看我们系统的网络抓包数据,来了解整个数据流向吧。
我们这里涉及两个系统的交互,一个是类似于微信、qq、github这种的oauth2授权服务器,一个是需要接入到这些授权服务器的应用,如应用A,它的角色是oauth2客户端。
现在开发应用A,一般都是前后端分离,前端调用应用A后端接口,此时假设用户是没登录,后端接口判别到这种情况,给前端抛错误码,前端此时就再调用后端另一个接口,该接口会组装一个指向oauth2授权服务器的授权请求url,意思是前端需要到授权服务器那边去进行身份认证、授权等,前端拿到这个url,就跳转过去。
跳转过去后,oauth2服务器那边会检查用户在这边登录了没有,没登录的话,流程没法继续往下走,会先把这个授权请求给保存下来,然后让用户登录;用户登录成功后,再把之前保存的那个请求拿出来执行。
授权请求主要做的事情就是,检查参数是否合法,如这个第三方应用在自己这边注册了没,如果检查没问题,就会随机生成一个临时的code,拼接到第三方应用提供的回调url中,然后302重定向到第三方应用A。
第三方应用A需要拿着这个code,请求自己的后端,第三方应用的后端拿到code后,去通过后台http调用,调用授权服务器的根据code获取token的接口,拿到token后,返回给第三方应用A的前端。
后续,第三方应用的前端每次请求就带着这个token来请求后端,后端拿着token去请求授权服务器,获取这个token对应的用户信息,权限信息(如这个人在应用A中有哪些菜单权限等),进行权限控制。
技术选型
目前,要实现oauth2客户端的话,可以选择spring security,具体可以看官网文档。
要实现oauth2授权服务器的话,有如下选择:
spring-authorization-server
spring官方发布的第二代的授权服务项目,但目前使用的人较少,感觉也还不是很成熟。且因为还在使用java8,所以只能用0.4.x的版本,就更不成熟了。
https://spring.io/projects/spring-authorization-server#support
版本 | Initial Release | End of Support | jdk | 备注 |
---|---|---|---|---|
1.1.x | 2023-05-16 | 2024-05-16 | java17 | https://docs.spring.io/spring-authorization-server/docs/current/reference/html/getting-started.html |
1.0.x | 2022-11-21 | 2025-11-21 | java17 | https://docs.spring.io/spring-authorization-server/docs/1.0.3/reference/html/getting-started.html |
0.4.x | 2022-11-20 | 2025-11-21 | Java 8 | https://docs.spring.io/spring-authorization-server/docs/0.4.3/reference/html/getting-started.html |
坐标:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.4.3</version>
</dependency>
OAuth2 For Spring Security
https://mvnrepository.com/artifact/org.springframework.security.oauth/spring-security-oauth2
坐标:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.2.RELEASE</version>
</dependency>
现状:
已经不再维护,最新版本2.5.2也还有多个cve漏洞,由于我们目前要求必须解决各种cve漏洞(一般靠版本升级解决,但这个已经是最新版本了没法升了).
另外,其内部实现中使用了session+cookie机制,当时以为是有状态的,不支持集群部署,后来才知道也是支持用redis之类的(靠spring session项目)。
另外,前后端未分离,定制页面较为复杂
https://spring.io/blog/2022/06/01/spring-security-oauth-reaches-end-of-life
参考第二种实现的源码进行简单实现
最终没办法,第二种因为cve漏洞的问题,加上前后端不分离可能导致以后扩展困难(比如登录要支持多因素认证等,会比较头疼,还是做成前后端分离,交给专业前端比较好),最终决定第二种的源码进行修改。
实际的数据流
应用A:http://10.80.121.46:8086 前后端分离,后端接口都通过http://10.80.121.46:8086 nginx转发
授权服务器:http://10.80.121.46:8083 前后端分离,后端接口都通过http://10.80.121.46:8083 nginx转发
应用A检测到用户未登录
比如,我们这边的前端同事是这么判断:
const token = localStorage.getItem('token')
if (token == null) {
window.location.href = "/v1/oAuth2Client/redirectToAuthorizeUrl";
}
我们以前的项目也有这样的:
if (response.status === 401) {
window.location.href = "/oauth2/authorization/";
}
反正都是通过前端或后端知道用户没登录后,调用本应用的另一个接口。
应用A组装调用授权服务器的url
直接看下面报文,后端组装了一个指向授权服务器(http://10.80.121.46:8083)的授权接口(v1/oauth2/authorize)的url,还带了查询参数,client_id代表应用A自己,redirect_uri表示授权服务器回调自己的地址,response_type=code,表示使用oauth2的授权码流程
GET /v1/oAuth2Client/redirectToAuthorizeUrl HTTP/1.1
Host: 10.80.121.46:8086
Connection: keep-alive
...
HTTP/1.1 302
Server: nginx/1.22.1
...
Location: http://10.80.121.46:8083/v1/oauth2/authorize?client_id=app-A&redirect_uri=http://10.80.121.46:8086/&response_type=code&scope=foo
注意,这里是直接后端返回了302,指示浏览器跳转到授权服务器,此时,如果有授权服务器这个domain下的cookie,是可以携带过去的;一般,我们都会在用户在授权服务器登录完成后,在授权服务器domain下写个cookie,避免每次都要登录。
授权服务器检测到用户未登录
第一次流程,用户浏览器肯定是没有授权服务器domain下的cookie的,此时,我们后端就会把用户302重定向到授权服务器这边的统一登录页面:
GET /v1/oauth2/authorize?client_id=app-A
&redirect_uri=http://10.80.121.46:8086/&response_type=code&scope=foo HTTP/1.1
Host: 10.80.121.46:8083
...
HTTP/1.1 302
Server: nginx/1.22.1
*
Location: http://10.80.121.46:8083/#/oauth-login?appCode=app-A&originUrl=aHR0cDovLzEwLjgwLjEyMS40Njo4MDgzL2FwcC1hdXRob3JpdHktYWRtaW4vdjEvb2F1dGgyL2F1dGhvcml6ZT9jbGllbnRfaWQ9YXBwLWRldi1wbGF0Zm9ybS1hZG1pbiZyZWRpcmVjdF91cmk9aHR0cDovLzEwLjgwLjEyMS40Njo4MDg2LyZyZXNwb25zZV90eXBlPWNvZGUmc2NvcGU9Zm9v
这个认证接口的后端处理也简单,检测请求携带了标识用户已登录的cookie没有,没有的话,重定向到登录页。
登录页携带了一些参数,这里最主要的是originUrl,这是因为,后端做的无状态,在完成登录请求后,还需要继续请求原始接口:
/v1/oauth2/authorize?client_id=app-A
&redirect_uri=http://10.80.121.46:8086/&response_type=code&scope=foo
所以,我这边选择把原始接口base64编码后,传给前端,由前端在完成登录后再次发起调用。
另外,这个登录页,大概下面这样:
http://10.80.121.46:8083/#/oauth-login
登录
POST /v1/oauth2/oAuth2Login HTTP/1.1
Host: 10.80.121.46:8083
Connection: keep-alive
...
{"username":"admin","password":"f80e247e3ead3dd1f3a708b7ce4dcf54"}
这边不重要的参数就省了,就是用户名密码那些,还包括验证码啥的。
响应:
HTTP/1.1 200
...
Access-Control-Allow-Methods: GET, POST, PUT, OPTIONS
Access-Control-Allow-Origin: http://10.80.121.46:8083
Access-Control-Allow-Credentials: true
Set-Cookie: SSO-JSESSIONID=f09b9e61d4114a058cd6f9b6b9ce85d7; Path=/; Domain=10.80.121.46; Max-Age=43200; Expires=Tue, 21 Nov 2023 01:11:42 GMT
登录逻辑:生成个随机数(token),然后作为key,用户信息为value,存redis,然后再就是把token写到domain下,写个cookie;
这块必须用cookie,因为浏览器在从应用A跳过来的时候,只有cookie才能带的过来,我们才知道用户是登录了没的。
前端在收到登录成功的code后,就把上一步的originUrl解码,然后重新发起调用:
/v1/oauth2/authorize?client_id=app-A
&redirect_uri=http://10.80.121.46:8086/&response_type=code&scope=foo
授权接口逻辑
主要就是各种参数校验,如client_id是否在授权服务器注册,各个参数的值是否合法,这块可以参考spring的代码实现。
一切没问题的话,就是生成个随机code,然后把code作为key,其他各种用户信息、认证请求的相关信息为value,存储到redis,然后就可以跳转回应用A了。
跳到应用A的什么地址呢,我们授权请求不是传了个redirect_uri吗,就重定向到哪里,只是会给你拼个code在后面
GET /?code=eEg7t5 HTTP/1.1
Host: 10.80.121.46:8086
...
携带code跳转回应用A
GET /?code=eEg7t5 HTTP/1.1
Host: 10.80.121.46:8086
我这边是跳转回应用A的前端的,前端拿到code,调用应用A的后端接口:利用code去请求授权服务器,获取token。
应用A前端调用后端接口,code换token
POST /v1/oAuth2Client/fetchAccessTokenByAuthorizeCode HTTP/1.1
Host: 10.80.121.46:8086
...
{"code":"eEg7t5"}
HTTP/1.1 200
...
{"code":"0","message":"success","data":{"scope":"all","access_token":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQ3NDAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHRlbmQiOm51bGwsImV4cCI6MTcwMDU3MjMwM30.HxH99Z3Mz4IZfHlC_Ai1th7jURlNs5qsHSMpiQdtGgXDqX8_XNx9GxswB","token_type":"bearer","refresh_token":null,"expires_in":86400}}
后端就是拿着code,去请求授权服务器,拿到了token,我这边是直接把token给了前端存储。
后续的请求,前端都会携带token,后端判断token是否有效即可(大家肯定不希望每次都去授权服务器校验token,所以可以第一次的时候,拿token去授权服务器验证是否有效,并缓存结果;我这边更暴力,因为都是组内的系统,直接弄的jwt token,且token没加密)
根据token获取用户信息
前端拿着token去调用应用A后端接口,获取用户信息;
POST /v1/oAuth2Client/queryUserInfo HTTP/1.1
Host: 10.80.121.46:8086
Connection: keep-alive
Content-Length: 0
AccessToken: eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjQ3NDAsInVzZXJuYW1lIjoiYWRtaW4iLCJleHRlbmQiOm51bGwsImV4cCI6MTcwMDU3MjMwM30.HxH99Z3Mz4IZfHlC_Ai1th7jURlNs5qsHSMpiQdtGgXDqX8_XNx9G
HTTP/1.1 200
...
{"code":"0","message":"请求成功","data":{"id":4740,"username":"admin","departmentName":null,"status":0,"statusName":"正常","roleListInfo":[]}}
用户信息都有了,获取权限信息也是一样的,这里不展示了。
简单的技术总结
我这边自己实现,是没办法,开源的没能满足自己要求,其实还有一点,我们那个用户名是可能重复的,就是说,在统一登录页,输入用户名,可能在后台查到多个用户,只能加上另一个内部的隐性字段才能不重,这也是我们必须自研的原因。我实现的比较简单,不是一个圆的轮子,仅供大家参考(一些异常场景,由于对oauth2的认识也不是特别深,只能以后慢慢完善了)
大家如果自研授权服务器,肯定涉及在授权服务器域名下写cookie,此时注意,后端接口都通过前端的nginx去转,会减少很多跨域相关的问题,信我的没错,我都踩过了。
另外,有时候后端直接重定向有问题时,就可以将要重定向的地址给到前端,由前端去window.location.href跳转也是ok的,也会减少一些跨域问题。
有问题可以留言,谢谢大家。
参考
https://www.cnblogs.com/cjsblog/p/10548022.html
https://mp.weixin.qq.com/s/AW3zkzIYR6kbQVbPlQmpMA 写cookie遇到问题时参考本篇
https://www.springcloud.io/post/2022-04/spring-samesite/
https://www.ituring.com.cn/article/200275
如何实现一套简单的oauth2授权码类型认证,一些思路,供参考的更多相关文章
- 13.详解oauth2授权码流程
13.详解oauth2授权码流程 把登陆系统单独独立出来,可以给自己写的微服务用,也可以给第三方的系统调用我们的服务 显式的和隐式的,两种方式,
- 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_06-SpringSecurityOauth2研究-Oauth2授权码模式-申请令牌
3.3 Oauth2授权码模式 3.3.1 Oauth2授权模式 Oauth2有以下授权模式: 授权码模式(Authorization Code) 隐式授权模式(Implicit) 密码模式(Reso ...
- 微信授权就是这个原理,Spring Cloud OAuth2 授权码模式
上一篇文章Spring Cloud OAuth2 实现单点登录介绍了使用 password 模式进行身份认证和单点登录.本篇介绍 Spring Cloud OAuth2 的另外一种授权模式-授权码模式 ...
- Spring Security OAuth2 授权码模式
背景: 由于业务实现中涉及到接入第三方系统(app接入有赞商城等),所以涉及到第三方系统需要获取用户信息(用户手机号.姓名等),为了保证用户信息的安全和接入方式的统一, 采用Oauth2四种模式之一 ...
- springboot2.x实现oauth2授权码登陆
参考文章:https://blog.csdn.net/qq_27828675/article/details/82466599 一 进行授权页 浏览器输入http://localhost:8081/o ...
- 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_07-SpringSecurityOauth2研究-Oauth2授权码模式-资源服务授权测试
下面要完成 5.6两个步骤 3.3.4 资源服务授权 3.3.4.1 资源服务授权流程 资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资 源,如下图 ...
- Java-JSP页面实现简单登录退出(菜鸟一枚、仅供参考)
1.JSP页面代码 <%@ page language="java" contentType="text/html; charset=UTF-8" pag ...
- OAuth2.0授权码模式
OAuth2.0简单说就是一种授权的协议,OAuth2.0在客户端与服务提供商之间,设置了一个授权层(authorization layer).客户端不能直接登录服务提供商,只能登录授权层,以此将用户 ...
- 学习Spring Security OAuth认证(一)-授权码模式
一.环境 spring boot+spring security+idea+maven+mybatis 主要是spring security 二.依赖 <dependency> <g ...
- OAuth 2.0授权之授权码授权
OAuth 2.0 是一个开放的标准协议,允许应用程序访问其它应用的用户授权的数据(如用户名.头像.昵称等).比如使用微信.QQ.支付宝登录等第三方网站,只需要用户点击授权按钮,第三方网站就会获取到用 ...
随机推荐
- 关于No changes detected
查看app在settings.py文件夹中是否有注册.
- 七 APPIUM Android 定位方式(转)
1.定位元素应用元素 1.1通过id定位元素 Android里面定位的id一般为resrouce-id: 代码可以这样写: WebElement element = driver.findElemen ...
- koa搭建nodejs项目并注册接口
使用nodejs注册接口逻辑处理会比较复杂,直接通过express或者koa能够简化开发流程,这里记录用koa来搭建nodejs项目并注册接口,对koa不太熟悉的话可以参考这一篇.让nodejs开启服 ...
- 【技术积累】Linux中的命令行【理论篇】【五】
arpd命令 命令介绍 arpd命令是Linux系统中的一个网络工具,用于管理和操作ARP(地址解析协议)缓存.ARP协议用于将IP地址映射到MAC地址,以便在局域网中进行通信. 命令说明 arpd命 ...
- vivo 场景下的 H5无障碍适配实践
作者:vivo 互联网前端团队- Zhang Li.Dai Wenkuan 随着信息无障碍的建设越来越受重视,开发人员在无障碍适配中也遇到了越来越多的挑战.本文是笔者在vivo开发H5项目做无障碍适配 ...
- 关于三维模型OSGB格式轻量化在数据存储的重要性浅析
关于三维模型OSGB格式轻量化在数据存储的重要性浅析 三维模型的OSGB格式是一种常见的数据格式,用于存储和传输地理信息系统(GIS)中的三维地图数据.随着技术的不断发展,三维模型的应用越来越广泛,包 ...
- Linux 内核设备驱动程序的IO寄存器访问 (下)
Linux 内核设备驱动程序通过 devm_regmap_init_mmio() 等函数获得 struct regmap 结构对象,该对象包含可用于访问设备寄存器的全部信息,包括定义访问操作如何执行的 ...
- 怎么选择API接口来获取自己想要的数据
在今天的数字时代,数据变得越来越重要,API接口也成为了获取数据的一种重要方式.无论是开发自己的应用程序还是进行市场营销,数据的获取都是非常必要的.但是,如何选择API接口来获取自己想要的数据呢? 以 ...
- 利用RATF框架实现web状态的监控
之前,我们已经说明了如何实现一个我们的接口测试框架RATF,当然这个框架不止可以用于管理我们的接口测试代码,我们还可以用他来对我们的web进行简单粗暴的监控. 原理: 1. 通过使用配置文件,对要监控 ...
- Java实践项目 - 购物车模块
Smiling & Weeping ----世界上美好的东西不太多,立秋傍晚从河对岸吹来的风, 加入购物车 1.数据创建--创建t_cart CREATE TABLE t_cart( cid ...