Spring Security 解析(六) —— 基于JWT的单点登陆(SSO)开发及原理解析

news2024/11/19 13:39:39

Spring Security 解析(六) —— 基于JWT的单点登陆(SSO)开发及原理解析

在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring Security 、Spring Security Oauth2 等权限、认证相关的内容、原理及设计学习并整理一遍。本系列文章就是在学习的过程中加强印象和理解所撰写的,如有侵权请告知。

单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一。 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 单点登陆本质上也是OAuth2的使用,所以其开发依赖于授权认证服务,如果不清楚的可以看我的上一篇文章。

一、 单点登陆 Demo开发

从单点登陆的定义上来看就知道我们需要新建个应用程序,我把它命名为 security-sso-client。接下的开发就在这个应用程序上了。

一、Maven 依赖

主要依赖 spring-boot-starter-security、spring-security-oauth2-autoconfigure、spring-security-oauth2 这3个。其中 spring-security-oauth2-autoconfigure 是Spring Boot 2.X 才有的。

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><<img src="https://github.com/BUG9/spring-security/network/alert/pom.xml/org.springframework.security.oauth:spring-security-oauth2/open--><version>2.3.5.RELEASE</version></dependency>" style="margin: auto" />

二、单点配置 @EnableOAuth2Sso

单点的基础配置引入是依赖 @EnableOAuth2Sso 实现的,在Spring Boot 2.x 及以上版本 的 @EnableOAuth2Sso 是在 spring-security-oauth2-autoconfigure 依赖里的。我这里简单配置了一下:

@Configuration
@EnableOAuth2Sso
public class ClientSecurityConfig extends WebSecurityConfigurerAdapter {@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().antMatchers("/","/error","/login").permitAll().anyRequest().authenticated().and().csrf().disable();}
} 

因为单点期间可能存在某些问题,会重定向到 /error ,所以我们把 /error 设置成无权限访问。

三、测试接口及页面

测试接口
@RestController
@Slf4j
public class TestController {@GetMapping("/client/{clientId}")public String getClient(@PathVariable String clientId) {return clientId;}

} 
测试页面
 <<img src="http://localhost:8091/client/1">跳转到OSS-client-1</a><a href="http://localhost:8092/client/2">跳转到OSS-client-2</a></body></html>" style="margin: auto" />

四、单点配置文件配置授权信息

由于我们要测试多应用间的单点,所以我们至少需要2个单点客户端,我这边通过Spring Boot 的多环境配置实现。

application.yml 配置

我们都知道单点实现本质就是Oauth2的授权码模式,所以我们需要配置访问授权服务器的地址信息,包括 :

  • security.oauth2.client.user-authorization-uri = /oauth/authorize 请求认证的地址,即获取code 码
  • security.oauth2.client.access-token-uri = /oauth/token 请求令牌的地址
  • security.oauth2.resource.jwt.key-uri = /oauth/token_key 解析jwt令牌所需要密钥的地址,服务启动时会调用 授权服务该接口获取jwt key,所以务必保证授权服务正常
  • security.oauth2.client.client-id = client1 clientId 信息
  • security.oauth2.client.client-secret = 123456 clientSecret 信息

其中有几个配置需要简单解释下:

  • security.oauth2.sso.login-path=/login OAuth2授权服务器触发重定向到客户端的路径 ,默认为 /login,这个路径要与授权服务器的回调地址(域名)后的路径一致
  • server.servlet.session.cookie.name = OAUTH2CLIENTSESSION 解决单机开发存在的问题,如果是非单机开发可忽略其配置
auth-server: http://localhost:9090 # authorization服务地址

security:oauth2:client:user-authorization-uri: ${auth-server}/oauth/authorize #请求认证的地址access-token-uri: ${auth-server}/oauth/token #请求令牌的地址resource:jwt:key-uri: ${auth-server}/oauth/token_key #解析jwt令牌所需要密钥的地址,服务启动时会调用 授权服务该接口获取jwt key,所以务必保证授权服务正常sso:login-path: /login #指向登录页面的路径,即OAuth2授权服务器触发重定向到客户端的路径 ,默认为 /login

server:servlet:session:cookie:name: OAUTH2CLIENTSESSION# 解决Possible CSRF detected - state parameter was required but no state could be found问题
spring:profiles:active: client1 

application-client1.yml 配置

application-client2 和 application-client1是一样的,只是端口号和client信息不一样而已,这里就不再重复贴出了。

server:
port: 8091

security:
oauth2:client:client-id: client1client-secret: 123456 

五、单点测试

效果如下:

从效果图中我们可以发现,当我们第一次访问client2 的接口时,跳转到了授权服务的登陆界面,完成登陆后成功跳转回到了client2 的测试接口,并且展示了接口返回值。此时我们访问client1 的 测试接口时直接返回(表面现象)了接口返回值。这就是单点登陆的效果,好奇心强的同学一定会在心里问道:它是如何实现的? 那么接下来我们就来揭开其面纱。

二、 单点登陆原理解析

一、@EnableOAuth2Sso

我们都知道 @EnableOAuth2Sso 是实现单点登陆的最核心配置注解,那么我们来看下 @EnableOAuth2Sso 的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableOAuth2Client
@EnableConfigurationProperties(OAuth2SsoProperties.class)
@Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class,	ResourceServerTokenServicesConfiguration.class })
public @interface EnableOAuth2Sso {

} 

其中我们关注4个配置文件的引用: ResourceServerTokenServicesConfiguration 、OAuth2SsoDefaultConfiguration 、 OAuth2SsoProperties 和 @EnableOAuth2Client:

  • OAuth2SsoDefaultConfiguration 单点登陆的核心配置,内部创建了 SsoSecurityConfigurer 对象, SsoSecurityConfigurer 内部 主要是配置 OAuth2ClientAuthenticationProcessingFilter 这个单点登陆核心过滤器之一。* ResourceServerTokenServicesConfiguration 内部读取了我们在 yml 中配置的信息* OAuth2SsoProperties 配置了回调地址url ,这个就是 security.oauth2.sso.login-path=/login 匹配的* @EnableOAuth2Client 标明单点客户端,其内部 主要 配置了 OAuth2ClientContextFilter 这个单点登陆核心过滤器之一#### 二、 OAuth2ClientContextFilter

OAuth2ClientContextFilter 过滤器类似于 ExceptionTranslationFilter , 它本身没有做任何过滤处理,只要当 chain.doFilter() 出现异常后 做出一个重定向处理。 但别小看这个重定向处理,它可是实现单点登陆的第一步,还记得第一次单点时会跳转到授权服务器的登陆页面么?而这个功能就是 OAuth2ClientContextFilter 实现的。我们来看下其源码:

public void doFilter(ServletRequest servletRequest,
			ServletResponse servletResponse, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;
		request.setAttribute(CURRENT_URI, calculateCurrentUri(request)); // 1、记录当前地址(currentUri)到HttpServletRequest

		try {
			chain.doFilter(servletRequest, servletResponse);
		} catch (IOException ex) {
			throw ex;
		} catch (Exception ex) {
			// Try to extract a SpringSecurityException from the stacktrace
			Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex);
			UserRedirectRequiredException redirect = (UserRedirectRequiredException) throwableAnalyzer
					.getFirstThrowableOfType(
							UserRedirectRequiredException.class, causeChain);			if (redirect != null) {// 2、判断当前异常 UserRedirectRequiredException 对象 是否为空
				redirectUser(redirect, request, response); // 3、重定向访问 授权服务 /oauth/authorize 
			} else {
				if (ex instanceof ServletException) {
					throw (ServletException) ex;
				}
				if (ex instanceof RuntimeException) {
					throw (RuntimeException) ex;
				}
				throw new NestedServletException("Unhandled exception", ex);
			}
		}
	} 

Debug看下:

整个 filter 分三步:

  • 1、记录当前地址(currentUri)到HttpServletRequest
  • 2、判断当前异常 UserRedirectRequiredException 对象 是否为空
  • 3、重定向访问 授权服务 /oauth/authorize

三、 OAuth2ClientAuthenticationProcessingFilter

OAuth2ClientContextFilter 过滤器 其要完成的工作就是 通过获取到的code码调用 授权服务 /oauth/token 接口获取 token 信息,并将获取到的token 信息解析成 OAuth2Authentication 认证对象。起源如下:

@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {

		OAuth2AccessToken accessToken;
		try {
			accessToken = restTemplate.getAccessToken(); //1、调用授权服务获取token 
		} catch (OAuth2Exception e) {
			BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e);
			publish(new OAuth2AuthenticationFailureEvent(bad));
			throw bad; 
		}
		try {
			OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue()); // 2、解析token信息为 OAuth2Authentication 认证对象并返回
			if (authenticationDetailsSource!=null) {
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue());
				request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType());
				result.setDetails(authenticationDetailsSource.buildDetails(request));
			}
			publish(new AuthenticationSuccessEvent(result));
			return result;
		}
		catch (InvalidTokenException e) {
			BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e);
			publish(new OAuth2AuthenticationFailureEvent(bad));
			throw bad; 
		}

	} 

整个 filter 2点功能:

  • restTemplate.getAccessToken(); //1、 调用授权服务获取token
  • tokenServices.loadAuthentication(accessToken.getValue()); // 2、 解析token信息为 OAuth2Authentication 认证对象并返回

完成上面步骤后就是一个正常的security授权认证过程,这里就不再讲述,有不清楚的同学可以看下我写的相关文章。

四、 AuthorizationCodeAccessTokenProvider

在讲述 OAuth2ClientContextFilter 时有一点没讲,那就是 UserRedirectRequiredException 是 谁抛出来的。 在讲述 OAuth2ClientAuthenticationProcessingFilter 也有一点没讲到,那就是它是如何判断出 当前 /login 是属于 需要获取code码的步骤还是去获取 token 的步骤( 当然是判断/login 是否带有code 参数,这里主要讲明是谁来判断的)。 这2个点都设计到了 AuthorizationCodeAccessTokenProvider 这个类。这个类是何时被调用的? 其实 OAuth2ClientAuthenticationProcessingFilter 隐藏在 restTemplate.getAccessToken(); 这个方法内部 调用的 accessTokenProvider.obtainAccessToken() 这里。 我们来看下OAuth2ClientAuthenticationProcessingFilter 的 obtainAccessToken() 方法内部源码:

public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest request)
			throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException,
			OAuth2AccessDeniedException {

		AuthorizationCodeResourceDetails resource = (AuthorizationCodeResourceDetails) details;

		if (request.getAuthorizationCode() == null) {//1、 判断当前参数是否包含code码 
			if (request.getStateKey() == null) {
				throw getRedirectForAuthorization(resource, request); //2、 不包含则抛出 UserRedirectRequiredException 异常
			}
			obtainAuthorizationCode(resource, request);
		}
		return retrieveToken(request, resource, getParametersForTokenRequest(resource, request),
				getHeadersForTokenRequest(request)); // 3 、 包含则调用获取token 

	} 

整个方法内部分3步:

  • 1、 判断当前参数是否包含code码
  • 2、 不包含则抛出 UserRedirectRequiredException 异常
  • 3、 包含继续获取token

最后可能有同学会问,为什么第一个客户端单点要跳转到授权服务登陆页面去登陆, 而当问第二个客户端却没有,其实 2次 客户端单点的流程都是一样的,都是授权码模式,但为什么客户端2 却不需要登陆呢? 其实是因为Cookies/Session的原因,因为我们访问同2个客户端基本上都是在同一个浏览器中进行的。 不信的同学可以试试2个浏览器分别访问2个单点客户端。

三、 个人总结

单点登陆本质上就是授权码模式,所以理解起来还是很容易的,如果非要给个流程图,还是那张授权码流程图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/163339.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[极客大挑战 2019]Secret File

目录 信息收集 解题思路 信息收集 先看源码&#xff0c;发现一个php文件 <a id"master" href"./Archive_room.php" style"background-color:#000000;height:70px;width:200px;color:black;left:44%;cursor:default;">Oh! You found me&…

9.2 容器库概览

文章目录所有容器的共性&#xff1a;迭代器迭代器的范围容器类型成员begin和end成员容器的定义和初始化与顺序容器大小相关的构造函数赋值和swapassignedswap容器大小操作关系运算符所有容器的共性&#xff1a; 表格一&#xff1a; 类型别名说明iterator迭代器const_iterator…

用R语言理解全微分

文章目录6 全微分梯度的概念全微分前情提要 R语言微积分极限π,e,γ\pi, e, \gammaπ,e,γ洛必达法则连续性和导数数值导数差商与牛顿插值方向导数 6 全微分 梯度的概念 对于任意函数f(x0,x1,⋯,xn)f(x_0,x_1,\cdots,x_n)f(x0​,x1​,⋯,xn​)&#xff0c;其梯度为 ∇f(∂f∂…

解决从BIOS选择从U盘启动但是系统仍然从硬盘启动的问题

我怀疑是BIOS失去了记忆能力&#xff0c;不能记住我的选择&#xff0c;所以仍然按默认从硬盘启动。 解决&#xff1a;重置BIOS即可 下面用物理方法重置BIOS。 在主板上找到这三根针&#xff0c;将上面的黑色套子拔出&#xff0c;然后插入旁边的另外两根针&#xff0c;例如开始…

基于python知识图谱医疗领域问答系统实现

直接上结果展示: “让人类永远保持理智,确实是一种奢求” ,机器人莫斯,《流浪地球》 项目概况 本项目为一个使用深度学习方法解析问题,知识图谱存储、查询知识点,基于医疗垂直领域的对话系统的后台程序 运行效果:

aws beanstalk 结合packer创建自定义平台

参考资料 https://github.com/aws-samples/eb-custom-platforms-samples#updating-packer-templateElastic Beanstalk 自定义平台 今天使用eb平台创建环境的时候&#xff0c;发现有名为packer的选项&#xff0c;查询文档发现aws beanstalk支持自定义平台&#xff0c;这功能几…

4. 使用预训练的PyTorch网络进行图像分类

4. 使用预训练的PyTorch网络进行图像分类 这篇博客将介绍如何使用PyTorch预先训练的网络执行图像分类。利用这些网络只需几行代码就可以准确地对1000个常见对象类别进行分类。这些图像分类网络是开创性的、最先进的图像分类网络&#xff0c;包括VGG16、VGG19、Inception、Dens…

windows下 pytorch的安装(gpu版本以及cpu版本)

一. 查看是否有gpu 打开cmd 输入nvidia-smi 是以下这种情况的就是有gpu 没有gpu的话就会报错 下载安装cuda以及cudnn&#xff08;安装cpu版本可以跳过此步骤直接进行pytorch的安装&#xff09; 下载cuda 看清楚两个箭头指的地方 一个是11.3.0 一个是日期 后面下载cudnn的时…

ProEssentials Pro 9.8.0.32 Crack

ProEssentials .Net图表组件用于对您的科学、工程和金融图表进行评估和选择&#xff01; Winforms 图表, WPF 图表, C/MFC/VCL 图表. Gigasoft拥有20多年帮助企业开发大型客户端和嵌入式图表项目的经验 为何选择ProEssentials&#xff1f; 我们真诚地希望您能针对您的具体实施…

day03 链表 | 203、移除链表元素 707、设计链表 206、反转链表

题目 203、移除链表元素 删除链表中等于给定值 val 的所有节点。 示例 1&#xff1a; 输入&#xff1a;head [1,2,6,3,4,5,6], val 6 输出&#xff1a;[1,2,3,4,5] 示例 2&#xff1a; 输入&#xff1a;head [], val 1 输出&#xff1a;[] 示例 3&#xff1a; 输入&am…

Pytorch Kaggle实战:House Prices - Advanced Regression Techniques

通过Kaggle比赛&#xff0c;将所学知识付诸实践 目录 1、下载和缓存数据集 2、访问和读取数据集 3、数据预处理 3、训练 4、K折交叉验证 5、模型选择 6、提交Kaggle预测 1、下载和缓存数据集 建立字典DATA_HUB,它可以将数据集名称的字符串映射到数据集相关的二元组上&am…

网络抓包-抓包工具tcpdump的使用与数据分析

1.测试背景 本次测试选用两台不同的服务器&#xff0c;ip分别为.233和.246,233服务器为客户端&#xff0c;246服务器为服务端。利用tcp协议就行socket通信。socket网络编程部分示例代码为基本的通信代码&#xff0c;需要了解tcp网络通讯的基本协议与过程。服务器上采用tcpdump…

【学习笔记】【Pytorch】八、池化层

【学习笔记】【Pytorch】八、池化层学习地址主要内容一、最大池化操作示例二、nn.MaxPool2d类的使用1.使用说明2.代码实现三、池化公式学习地址 PyTorch深度学习快速入门教程【小土堆】. 主要内容 一、最大池化操作示例 二、nn.MaxPool2d类的使用 作用&#xff1a;对于输入信…

Min_25筛

概述 Min_25是日本一个ACM选手的ID&#xff0c;这个筛法是他发明的&#xff0c;所以称之为Min_25筛。它能在亚线性复杂度求出一类积性函数的 fff 的前缀和&#xff0c;前提 是这个积性函数在质数和质数的幂位置的函数值比较好求。借助埃拉托色尼筛的思想 将原问题转化成与质因…

华为PIM-SM 动态RP实验配置

目录 建立PIM SM邻居 配置DR 配置动态RP 组成员端DR上配置IGMP 配置PIM安全 配置SPT切换 配置Anycast RP 配置接口的IP地址&#xff0c;并配置路由协议使得全网互通 建立PIM SM邻居 AR5操作 multicast routing-enable 开启组播路由转发功能 int g0/0/0 pim sm …

MacOS对文件夹加密的方法

背景 MacOS没有那种类似于windows那种对文件夹加解密的软件&#xff0c;MacOS自带有一种加解密&#xff0c;但是其实使用体验上跟windows那种很不一样。 win上的加解密都很快&#xff0c;就好像仅仅对文件夹进行加解密&#xff08;我估计是安全性较低的&#xff0c;因为加密过…

【JavaSE】异常的初步认识

目录 1、初步认识异常 1、算数异常 2、空指针异常 3、数组越界异常 2、异常的结构体系 3、异常的分类 1、编译时异常/受查异常 2、运行时异常/非受查异常 4、异常的处理 1、处理异常的编程方式&#xff08;防御式编程&#xff09; 1、事前防御性&#xff08;LBYL&a…

【软件测试】软件测试基础知识

1. 什么是软件测试 软件测试就是验证软件产品特性是否满足用户的需求 2. 调试与测试的区别 目的不同 调试&#xff1a;发现并解决软件中的缺陷测试&#xff1a;发现软件中的缺陷 参与角色不同 调试&#xff1a;开发人员测试&#xff1a;测试人员&#xff0c;开发人员等&a…

软件测试复习04:动态测试——黑盒测试

作者&#xff1a;非妃是公主 专栏&#xff1a;《软件测试》 个性签&#xff1a;顺境不惰&#xff0c;逆境不馁&#xff0c;以心制境&#xff0c;万事可成。——曾国藩 文章目录等价划分法边值分析法错误推测法因果图法示例习题等价划分法 等价类&#xff1a;一个几何&#xf…

如何快速搭建自己的阿里云服务器(宝塔)并且部署springboot+vue项目(全网最全)

&#x1f4e2;欢迎点赞&#x1f44d;收藏⭐留言&#x1f4dd;如有错误敬请指正&#xff01; 文章目录&#x1f4e2;欢迎点赞&#x1f44d;收藏⭐留言&#x1f4dd;如有错误敬请指正&#xff01;一、前言二、准备工作1、新手申请2、安全组设置3、修改实例4.这里可以 直接用阿里云…