背景

组内人不少,今年陆陆续续研发了不少系统,一般都会包括一个后台管理系统,现在问题是,每个管理系统都有RBAC那一套用户权限体系,实在是有点浪费人力,于是今年我们搞了个统一管理各个应用系统的RBAC的系统,叫做应用权限中心,大致就是:

  1. 各个应用在我们系统注册,并录入应用支持的各类权限(如菜单权限、数据权限、接口权限等);
  2. 统一管理所有用户(包括公司员工、合作伙伴员工等);
  3. 各个接入系统的管理员可以在自己的应用建立角色,赋予其某些权限;接下来,又可以给人员分配这些角色。

在以上数据维护完成后,就可以由我们系统提供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

坐标:

https://mvnrepository.com/artifact/org.springframework.security/spring-security-oauth2-authorization-server

<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授权码类型认证,一些思路,供参考的更多相关文章

  1. 13.详解oauth2授权码流程

    13.详解oauth2授权码流程 把登陆系统单独独立出来,可以给自己写的微服务用,也可以给第三方的系统调用我们的服务 显式的和隐式的,两种方式,

  2. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_06-SpringSecurityOauth2研究-Oauth2授权码模式-申请令牌

    3.3 Oauth2授权码模式 3.3.1 Oauth2授权模式 Oauth2有以下授权模式: 授权码模式(Authorization Code) 隐式授权模式(Implicit) 密码模式(Reso ...

  3. 微信授权就是这个原理,Spring Cloud OAuth2 授权码模式

    上一篇文章Spring Cloud OAuth2 实现单点登录介绍了使用 password 模式进行身份认证和单点登录.本篇介绍 Spring Cloud OAuth2 的另外一种授权模式-授权码模式 ...

  4. Spring Security OAuth2 授权码模式

     背景: 由于业务实现中涉及到接入第三方系统(app接入有赞商城等),所以涉及到第三方系统需要获取用户信息(用户手机号.姓名等),为了保证用户信息的安全和接入方式的统一, 采用Oauth2四种模式之一 ...

  5. springboot2.x实现oauth2授权码登陆

    参考文章:https://blog.csdn.net/qq_27828675/article/details/82466599 一 进行授权页 浏览器输入http://localhost:8081/o ...

  6. 阶段5 3.微服务项目【学成在线】_day16 Spring Security Oauth2_07-SpringSecurityOauth2研究-Oauth2授权码模式-资源服务授权测试

    下面要完成  5.6两个步骤 3.3.4 资源服务授权 3.3.4.1 资源服务授权流程 资源服务拥有要访问的受保护资源,客户端携带令牌访问资源服务,如果令牌合法则可成功访问资源服务中的资 源,如下图 ...

  7. Java-JSP页面实现简单登录退出(菜鸟一枚、仅供参考)

    1.JSP页面代码 <%@ page language="java" contentType="text/html; charset=UTF-8" pag ...

  8. OAuth2.0授权码模式

    OAuth2.0简单说就是一种授权的协议,OAuth2.0在客户端与服务提供商之间,设置了一个授权层(authorization layer).客户端不能直接登录服务提供商,只能登录授权层,以此将用户 ...

  9. 学习Spring Security OAuth认证(一)-授权码模式

    一.环境 spring boot+spring security+idea+maven+mybatis 主要是spring security 二.依赖 <dependency> <g ...

  10. OAuth 2.0授权之授权码授权

    OAuth 2.0 是一个开放的标准协议,允许应用程序访问其它应用的用户授权的数据(如用户名.头像.昵称等).比如使用微信.QQ.支付宝登录等第三方网站,只需要用户点击授权按钮,第三方网站就会获取到用 ...

随机推荐

  1. LeetCode 周赛上分之旅 # 36 KMP 字符串匹配殊途同归

    ️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 BaguTree Pro 知识星球提问. 学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越 ...

  2. 实现地图遮罩 leaflet

    1 前言 在地图中加载的底图是瓦片服务(固定大小的规则矩形),底图的范围很大,铺满了整个div,但是我们的感兴趣的部门可能只是其中一部分,如何在整个屏幕中突出感兴趣的部分-- 地图遮罩(遮挡图像中不感 ...

  3. SpringBoot 笔记

    SpringBoot 笔记 一.Spring Boot 入门 1.Spring Boot 简介 2.微服务 2014,martin fowler 微服务:架构风格(服务微化) 一个应用应该是一组小型服 ...

  4. Linux 内核音频子系统调试

    debugfs 文件系统 debugfs 可以为 Linux 内核各个模块的分析调试,提供许多信息,如音频子系统的 ASoC,以及 tracing 等.debugfs 文件系统可以通过命令行工具挂载, ...

  5. 《CTFshow-Web入门》05. Web 41~50

    @ 目录 web41 题解 原理 web42 题解 原理 web43 题解 原理 web44 题解 原理 web45 题解 原理 web46 题解 原理 web47 题解 web48 题解 web49 ...

  6. Codeforces 1462F The Treasure of The Segments

    题意 给\(n(1\leq n\leq 2*10^5)\)个线段$[l_i,r_i] (1≤l_i≤r_i≤10^9) $,问最少删除几个线段,使得剩下线段中,有至少一个线段与所有线段相交. 分析 对 ...

  7. 内网DNS解析☞dnsmasq

    内网DNS解析☞dnsmasq 目录 内网DNS解析☞dnsmasq 简介: 安装dnsmasq 问题: 1.怎么让172.30.1.* 与172.30.2.* 两个网段能互相访问? 2.firewa ...

  8. LeetCode279:完全平方数,动态规划解法超过46%,作弊解法却超过97%

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 本篇概览 本篇概览 这是道高频面试题,值得一看 首先, ...

  9. Content Security Policy(CSP)应用及说明

    什么是CSP CSP全称Content Security Policy ,可以直接翻译为内容安全策略,说白了,就是为了页面内容安全而制定的一系列防护策略. 通过CSP所约束的的规责指定可信的内容来源( ...

  10. 记一次 .NET 某餐饮小程序 内存暴涨分析

    一:背景 1. 讲故事 前些天有位朋友找到我,说他的程序内存异常高,用 vs诊断工具 加载时间又太久,让我帮忙看一下到底咋回事,截图如下: 确实,如果dump文件超过 10G 之后,市面上那些可视化工 ...