Spring Security 基于JWT的单点登陆(SSO)开发及原理解析
JDK1.8
Spring boot 2.x
Spring Security 5.x
单点登录(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>
<!--@EnableOAuth2Sso 引入,Spring Boot 2.x 将这个注解移到该依赖包-->
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
</exclusion>
</exclusions>
<version>2.1.7.RELEASE</version>
</dependency>
<!-- 不是starter,手动配置 -->
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<!--请注意下 spring-authorization-oauth2 的版本 务必高于 2.3.2.RELEASE,这是官方的一个bug:
java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
要求必须大于2.3.5 版本,官方解释: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>
二、单点配置 @EnableOAuth2Sso
单点的基础配置引入是依赖 @EnableOAuth2Sso 实现的,在Spring Boot 2.x 及以上版本 的 @EnableOAuth2Sso 是在 spring-security-oauth2-autoconfigure 依赖里的。我这里简单配置了一下:
@Configuration
@EnableOAuth2Sso
public class ClientSecurityConfig extends WebSecurityConfigurerAdapter { @Override
public 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;
} }
测试页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>OSS-client</title>
</head>
<body>
<h1>OSS-client</h1>
<a href="http://localhost:8091/client/1">跳转到OSS-client-1</a>
<a href="http://localhost:8092/client/2">跳转到OSS-client-2</a>
</body>
</html>
四、单点配置文件配置授权信息
由于我们要测试多应用间的单点,所以我们至少需要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: client1
client-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个单点客户端。
三、 个人总结
单点登陆本质上就是授权码模式,所以理解起来还是很容易的,如果非要给个流程图,还是那张授权码流程图:
本文介绍 基于JWT的单点登陆(SSO)开发及原理解析 开发的代码可以访问代码仓库 ,项目的github 地址
https://github.com/BUG9/spring-security
Spring Security 基于JWT的单点登陆(SSO)开发及原理解析的更多相关文章
- Spring Security 解析(六) —— 基于JWT的单点登陆(SSO)开发及原理解析
Spring Security 解析(六) -- 基于JWT的单点登陆(SSO)开发及原理解析 在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把 ...
- 解决Spring Boot 从1.x升级到 2.x 后 单点登陆(SSO)问题
解决Spring Boot 从1.x升级到 2.x 后 单点登陆(SSO)问题 在学习Spring Cloud 时,遇到了授权服务oauth 相关内容时,总是一知半解,因此决定先把Spring S ...
- Spring Security整合JWT,实现单点登录,So Easy~!
前面整理过一篇 SpringBoot Security前后端分离,登录退出等返回json数据,也就是用Spring Security,基于SpringBoot2.1.4 RELEASE前后端分离的情况 ...
- 基于Spring Security和 JWT的权限系统设计
写在前面 关于 Spring Security Web系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求.在Java EE领域,成熟的安全框架解决方案一般有 A ...
- 用Spring Security, JWT, Vue实现一个前后端分离无状态认证Demo
简介 完整代码 https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom 运行展示 后端 主要展示 Spring Security ...
- Springboot集成Spring Security实现JWT认证
我最新最全的文章都在南瓜慢说 www.pkslow.com,欢迎大家来喝茶! 1 简介 Spring Security作为成熟且强大的安全框架,得到许多大厂的青睐.而作为前后端分离的SSO方案,JWT ...
- Spring Security和 JWT两大利器来打造一个简易的权限系统。
写在前面 关于 Spring Security Web系统的认证和权限模块也算是一个系统的基础设施了,几乎任何的互联网服务都会涉及到这方面的要求.在Java EE领域,成熟的安全框架解决方案一般有 A ...
- Spring Security 集成CAS实现单点登录
参考:http://elim.iteye.com/blog/2270446 众所周知,Cas是对单点登录的一种实现.本文假设读者已经了解了Cas的原理及其使用,这些内容在本文将不会讨论.Cas有Ser ...
- spring boot rest 接口集成 spring security(2) - JWT配置
Spring Boot 集成教程 Spring Boot 介绍 Spring Boot 开发环境搭建(Eclipse) Spring Boot Hello World (restful接口)例子 sp ...
- Spring Security + OAuth2 + JWT 基本使用
Spring Security + OAuth2 + JWT 基本使用 前面学习了 Spring Security 入门,现在搭配 oauth2 + JWT 进行测试. 1.什么是 OAuth2 OA ...
随机推荐
- spool参数详解
SQL*PLUS维护系统变量,也称SET变量,利用它可为SQL*PLUS交互建立一个特殊的环境,如:设置NUMBER数据的显示宽度;设置每页的行数;设置列的宽度等.可用SET命令改变这些系统变量,也可 ...
- 佛祖保佑永无 BUG 代码注释
// // _oo0oo_ // o8888888o // 88" . "88 // (| -_- |) // 0\ = /0 // ___/`---'\___ // .' \\| ...
- git reset 之后切换到原来的commit
git reset的语法: git reset [--hard|soft|mixed|merge|keep] [<commit>或HEAD] 作用:将当前分支reset到指定的commit ...
- Blocked aria-hidden on an element because its descendant retained focus.
背景 vue 2.6.10 报错:Blocked aria-hidden on an element because its descendant retained focus. The focus ...
- vant+vue控制列表展开
<van-list v-model="loading" :finished="finished" finished-text="没有更多了&qu ...
- Impala学习--Impala概述,Impala系统架构
Imapla概述 Impala是Cloudera公司的一个实时海量查询产品.是对于已有Hive产品的补充.Impala采用了和Hive相同的类SQL接口,但并没有采用MapRed框架执行任务,而是采用 ...
- MySQL8.0之特性
MySQL 8.0 正式版 8.0.11 已发布,官方表示 MySQL 8 要比 MySQL 5.7 快 2 倍,还带来了大量的改进和更快的性能.下面我们将简要介绍下 MySQL 8.0 中值得关注的 ...
- JavaScript 绑定this
1.临时改变函数调用时this的指向 方法:call()与apply(),第一个参数为此次调用时的this指向,如果不传,则则等同于指定全局对象,后面的参数为函数原本的参数 区别:apply()方法传 ...
- 生成式AI如何辅助医药行业智能营销
生成式AI如何辅助医药行业智能营销 生成式AI在医药行业的智能营销中发挥着日益重要的作用,它通过多种方式辅助医药企业提升市场洞察能力.优化营销策略.增强客户互动和体验,从而推动销售增长和品牌价值的提升 ...
- .NET 模拟&编辑平滑曲线
本文介绍不依赖贝塞尔曲线,如何绘制一条平滑曲线,用于解决无贝塞尔控制点的情况下绘制曲线.但数据点不在贝塞尔曲线的场景. 在上一家公司我做过一个平滑曲线编辑工具,用于轮椅调整加减速曲线.基于几个用户可控 ...