单点登录原理及实现

article/2025/9/21 12:00:57

一,背景

单点登录顾名思义就是在多个应用系统中,只需要登录一次,就可以访问其他相互信任的应用系统,免除多次登录的烦恼。比如我们登录了百度账号,再去百度百科,百度文库就不需要再次登录了。

二,原理说明

单点登录主流都是基于共享 cookie 来实现的,下面分别介绍 同域 和 跨域 下两种场景具体怎样实现共享cookie的。

2.1. 同域单点登录

适用场景:都是企业自己的系统,所有系统都使用同一个一级域名通过不同的二级域名来区分。

举个例子:公司有一个一级域名为 xxx.com ,我们有三个系统分别是:门户系统(sso.xxx.com)、应用1(app1.xxx.com)和应用2(app2.xxx.com),需要实现系统之间的单点登录,实现架构如下:

核心原理:

  1. 门户系统设置 Cookie 的 domain 为一级域名也就是 xxx.com,这样就可以共享门户的 Cookie 给所有的使用该域名(xxx.xxx.com)的系统

  2. 使用Spring Session等技术让所有系统共享Session

  3. 这样只要门户系统登录之后无论跳转应用1或者应用2,都能通过门户Cookie中的sessionId读取到Session中的登录信息实现单点登录

2.2. 跨域单点登录

单点登录之间的系统域名不一样,例如第三方系统。由于域名不一样不能共享Cookie了,这样就需要通过一个单独的授权服务(UAA)来做统一登录,并基于共享UAA的Cookie来实现单点登录。

举个例子:有两个系统分别是:应用1(webApp.com)和应用2(xxx.com)需要实现单点登录,另外有一个授权中心(sso.com),实现架构如下:

核心原理:

  1. 访问系统1判断未登录,则跳转到UAA系统请求授权

  2. 在系统域名sso.com下的登录地址中输入用户名/密码完成登录

  3. 登录成功后UAA系统把登录信息保存到Session中,并在浏览器写入域为sso.com的Cookie

  4. 访问系统2判断未登录,则跳转到UAA系统请求授权

  5. 由于是跳转到UAA系统的域名sso.com下,所以能通过浏览器中UAA的Cookie读取到Session中之前的登录信息完成单点登录

三,技术实现

3.1,基于Spring Security实现(前后端不分离)

 Oauth2单点登录除了需要授权中心完成统一登录/授权逻辑之外

各个系统本身(sso客户端)也需要实现以下逻辑:

  1. 拦截请求判断登录状态

  2. 授权中心通过Oauth2授权码模式交互完成登录/单点登录

  3. 保存用户登录信息

以上逻辑只需使用一个 @EnableOAuth2Sso 注解即可实现

@EnableOAuth2Sso
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {@Value("${security.oauth2.sso.login-path:}")private String loginPath;@Resourceprivate LogoutSuccessHandler ssoLogoutSuccessHandler;@Overridepublic void configure(HttpSecurity http) throws Exception {http.authorizeRequests().anyRequest().authenticated().and().csrf().disable().logout().logoutSuccessHandler(ssoLogoutSuccessHandler);if (StrUtil.isNotEmpty(loginPath)) {http.formLogin().loginProcessingUrl(loginPath);}}
}
@Component
public class SsoLogoutSuccessHandler implements LogoutSuccessHandler {@Value("${zlt.logout-uri:''}")private String logoutUri;private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();@Overridepublic void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {OAuth2Authentication oauth2Authentication = (OAuth2Authentication)authentication;OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails)oauth2Authentication.getDetails();String accessToken = details.getTokenValue();redirectStrategy.sendRedirect(request, response, logoutUri+accessToken);}
}

zlt:api-uaa:url: http://127.0.0.1:9900/api-uaa/oauthlogout-uri: ${zlt.api-uaa.url}/remove/token?redirect_uri=http://127.0.0.1:8080&access_token=security:oauth2:sso:login-path: /singleLoginclient:client-id: zltclient-secret: zltaccess-token-uri: ${zlt.api-uaa.url}/tokenuser-authorization-uri: ${zlt.api-uaa.url}/authorizeresource:token-info-uri: ${zlt.api-uaa.url}/check_token

 3.2,基于Spring Security实现(前后端分离)

跨域间的前后端分离项目也是基于共享统一授权服务的cookie来实现单点登录的,但是与非前后分离不一样的是存在以下问题需要解决

  1. 没有过滤器/拦截器,需要在前端判断登录状态

  2. 需要自己实现oauth2的授权码模式交互逻辑

  3. 需要解决安全性问题,oauth2的clientSecret参数放在前端不安全

下面是前后端分离项目的三个角色(前端WEB工程、后端API工程、授权中心)间进行登录/单点登录时的交互逻辑架构图

前端WEB工程有几个点需要注意:

  1. 红色线条为重定向跳转

  2. 前端工程可通过是否存在 access_token 判断登录状态

  3. 前端工程跳转UAA之前需记录用户访问的页面地址,方便登录完成后重定向回去

PS:为什么获取access_token需要请求后端API工程去完成,而不是前端WEB工程自己直接请求UAA呢?因为安全性问题!这一步需要传clientSecret参数,而通过后台来配置这个参数就不需要暴露给前端了。

@Slf4j
@RestController
public class ApiController {@Value("${zlt.sso.client-id:}")private String clientId;@Value("${zlt.sso.client-secret:}")private String clientSecret;@Value("${zlt.sso.redirect-uri:}")private String redirectUri;@Value("${zlt.sso.access-token-uri:}")private String accessTokenUri;@Value("${zlt.sso.user-info-uri:}")private String userInfoUri;private final static Map<String, Map<String, Object>> localTokenMap = new HashMap<>();@GetMapping("/token/{code}")public String tokenInfo(@PathVariable String code) throws UnsupportedEncodingException {//获取tokenMap tokenMap = getAccessToken(code);String accessToken = (String) tokenMap.get("access_token");//获取用户信息Map userMap = getUserInfo(accessToken);List<String> roles = getRoles(userMap);Map result = new HashMap(2);String username = (String) userMap.get("username");result.put("username", username);result.put("roles", roles);localTokenMap.put(accessToken, result);return accessToken;}/*** 获取token*/public Map getAccessToken(String code) throws UnsupportedEncodingException {RestTemplate restTemplate = new RestTemplate();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);byte[] authorization = (clientId + ":" + clientSecret).getBytes("UTF-8");BASE64Encoder encoder = new BASE64Encoder();String base64Auth = encoder.encode(authorization);headers.add("Authorization", "Basic " + base64Auth);MultiValueMap<String, String> param = new LinkedMultiValueMap<>();param.add("code", code);param.add("grant_type", "authorization_code");param.add("redirect_uri", redirectUri);param.add("scope", "app");HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(param, headers);ResponseEntity<Map> response = restTemplate.postForEntity(accessTokenUri, request, Map.class);Map result = response.getBody();return result;}/*** 获取用户信息*/public Map getUserInfo(String accessToken) {RestTemplate restTemplate = new RestTemplate();Map result = restTemplate.getForObject(userInfoUri + "?access_token=" + accessToken, Map.class);return (Map) result.get("datas");}private List<String> getRoles(Map userMap) {List<Map<String, String>> roles = (List<Map<String, String>>) userMap.get("roles");List<String> result = new ArrayList<>();if (CollectionUtil.isNotEmpty(roles)) {roles.forEach(e -> {result.add(e.get("code"));});}return result;}@GetMapping("/user")public Map<String, Object> user(HttpServletRequest request) {String token = request.getParameter("access_token");return localTokenMap.get(token);}@GetMapping("/logoutNotify")public void logoutNotify(HttpServletRequest request) {String tokens = request.getParameter("tokens");log.info("=====logoutNotify: " + tokens);if (StrUtil.isNotEmpty(tokens)) {for (String accessToken : tokens.split(",")) {localTokenMap.remove(accessToken);}}}
}

 3.3,基于Security实现OIDC单点登录

OIDC 是 OpenID Connect 的简称,OIDC=(Identity, Authentication) + OAuth 2.0。它在 OAuth2 上构建了一个身份层,是一个基于 OAuth2 协议的身份认证标准协议。我们都知道 OAuth2 是一个授权协议,它无法提供完善的身份认证功能,OIDC 使用 OAuth2 的授权服务器来为第三方客户端提供用户的身份认证,并把对应的身份认证信息传递给客户端,且完全兼容 OAuth2。

 OAuth2 提供了 Access Token 来解决授权第三方 客户端 访问受保护资源的问题;OIDC 在这个基础上提供了 ID Token 来解决第三方客户端标识用户身份认证的问题。OIDC 的核心在于 OAuth2 的授权流程中,一并提供用户的身份认证信息 ID Token 给到第三方 客户端ID Token 使用 JWT 格式来包装。

OIDC协议授权返回示例:

{"resp_code": 200,"resp_msg": "ok","datas": {"access_token": "d1186597-aeb4-4214-b176-08ec09b1f1ed","token_type": "bearer","refresh_token": "37fd65d8-f017-4b5a-9975-22b3067fb30b","expires_in": 3599,"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vemx0MjAwMC5jbiIsImlhdCI6MTYyMTY5NjU4MjYxNSwiZXhwIjoxNjIxNjk2NjQyNjE1LCJzdWIiOiIxIiwibmFtZSI6IueuoeeQhuWRmCIsImxvZ2luX25hbWUiOiJhZG1pbiIsInBpY3R1cmUiOiJodHRwOi8vcGtxdG1uMHAxLmJrdC5jbG91ZGRuLmNvbS_lpLTlg48ucG5nIiwiYXVkIjoiYXBwIiwibm9uY2UiOiJ0NDlicGcifQ.UhsJpHYMWRmny45K0CygXeaASFawqtP2-zgWPDnn0XiBJ6yeiNo5QAwerjf9NFP1YBxuobRUzzhkzRikWGwzramNG9na0NPi4yUQjPNZitX1JzlIA8XSq4LNsuPKO7hS1ALqqiAEHS3oUqKAsjuE-ygt0fN9iVj2LyL3-GFpql0UAFIHhew_J7yIpR14snSh3iLVTmSWNknGu2boDvyO5LWonnUjkNB3XSGD0ukI3UEEFXBJWyOD9rPqfTDOy0sTG_-9wjDEV0WbtJf4FyfO3hPu--bwtM_U0kxRbfLnOujFXyVUStiCKG45wg7iI4Du2lamPJoJCplwjHKWdPc6Zw"}
}

可以看到与普通的 OAuth2 相比返回的信息中除了有 access_token 之外还多出了 id_token 属性。

ID Token 是一个安全令牌,由授权服务器提供的包含用户信息的 JWT 格式的数据结构,得益于 JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得 ID Token 可以安全的传递给第三方客户端程序并且容易被验证。

id_token包含以下内容

{"iss": "http://zlt2000.cn","iat": 1621696582615,"exp": 1621696642615,"sub": "1","name": "管理员","login_name": "admin","picture": "http://xxx/头像.png","aud": "app","nonce": "t49bpg"
}

「iss」:令牌颁发者

「iat」:令牌颁发时间戳

「exp」:令牌过期时间戳

「sub」:用户id

「name」:用户姓名

「login_name」:用户登录名

「picture」:用户头像

「aud」:令牌接收者,OAuth应用ID

「nonce」:随机字符串,用来防止重放攻击

与 JWT 的 Access Token 区别

是否可以直接使用 JWT 方式的 Access Token 并在 Payload 中加入用户信息来代替 ID Token 呢?

虽然在 Access Token 中可以加入用户的信息,并且是防篡改的,但是用户的每次请求都需要携带着 Access Token,这样不但增加了带宽,而且很容易泄露用户的信息。

与 UserInfo 端点的区别

通常 OIDC 协议都需要另外提供了一个 Get /userinfo 的 Endpoint,需要通过 Access Token 调用该 Endpoint 来获取详细的用户信息,这个方法和 ID Token 同样都可以获取用户信息,那两者有什么区别呢?

相比较于 Get /userinfo 的接口使用 ID Token 可以减少远程 API 调用的额外开销;使用那个主要是看 「需求」,当你只需要获取用户的基本信息直接使用 ID Token 就可以了,并不需要每次都通过 Access Token 去调用 Get /userinfo 获取详细的用户信息。

OIDC 单点登录流程

大部分的流程与 OAuth2 的授权码模式相同这里就不多讲述了,其中下面两个步骤需要说明一下:

  • 解析 ID Token 的公钥可以是预先提供给第三方系统也可以是提供接口获取。

  • 「自动注册用户」 指的是第一次单点登录的时候,由于用户信息不存在需要在本系统中生成该用户数据;例如你从未在 CSDN 中注册也可以使用微信来登录该网站。

实现

先说一下扩展最终的目标是需要达到以下效果:

  • 授权码模式:/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code

  • OIDC 模式:/oauth/authorize?client_id={client_id}&redirect_uri={redirect_uri}&response_type=code id_token

目标是要通过在 response_type 中的传值来控制是否使用 OIDC 模式,如果使用则在 response_type 中增加 id_token 的值。

由于需要在 OAuth2 返回的内容中添加 ID Token 属性,所以实现这个扩展的关键就是需要通过 Security 的 TokenEnhancer 来为 Token 添加自定义字段;

定义 TokenEnhancer 的 Bean 来扩展 Token:

 @Bean@Order(1)public TokenEnhancer tokenEnhancer(@Autowired(required = false) KeyProperties keyProperties, IClientService clientService, TokenStoreProperties tokenStoreProperties) {return (accessToken, authentication) -> {Set<String> responseTypes = authentication.getOAuth2Request().getResponseTypes();Map<String, Object> additionalInfo = new HashMap<>(3);String accountType = AuthUtils.getAccountType(authentication.getUserAuthentication());if (StrUtil.isNotEmpty(accountType)) {additionalInfo.put(SecurityConstants.ACCOUNT_TYPE_PARAM_NAME, accountType);}if (responseTypes.contains(SecurityConstants.ID_TOKEN)|| "authJwt".equals(tokenStoreProperties.getType())) {Object principal = authentication.getPrincipal();//增加id参数if (principal instanceof SysUser) {SysUser user = (SysUser)principal;if (responseTypes.contains(SecurityConstants.ID_TOKEN)) {//生成id_tokensetIdToken(additionalInfo, authentication, keyProperties, clientService, user);}if ("authJwt".equals(tokenStoreProperties.getType())) {additionalInfo.put("id", user.getId());}}}((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);return accessToken;};}

 通过授权的 response_type 参数来判断是否需要生成 id_token。

 /*** 生成id_token* @param additionalInfo 存储token附加信息对象* @param authentication 授权对象* @param keyProperties 密钥* @param clientService 应用service*/private void setIdToken(Map<String, Object> additionalInfo, OAuth2Authentication authentication, KeyProperties keyProperties, IClientService clientService, SysUser user) {String clientId = authentication.getOAuth2Request().getClientId();Client client = clientService.loadClientByClientId(clientId);if (client.getSupportIdToken()) {String nonce = authentication.getOAuth2Request().getRequestParameters().get(IdTokenClaimNames.NONCE);long now = System.currentTimeMillis();long expiresAt = System.currentTimeMillis() + client.getIdTokenValiditySeconds() * 1000;String idToken = OidcIdTokenBuilder.builder(keyProperties).issuer(SecurityConstants.ISS).issuedAt(now).expiresAt(expiresAt).subject(String.valueOf(user.getId())).name(user.getNickname()).loginName(user.getUsername()).picture(user.getHeadImgUrl()).audience(clientId).nonce(nonce).build();additionalInfo.put(SecurityConstants.ID_TOKEN, idToken);}}

http://chatgpt.dhexx.cn/article/kj0ERCdZ.shtml

相关文章

CGAL:学习CGAL

背景 CGAL是一个非常有用的库&#xff0c;但是学习起来非常痛苦。为此&#xff0c;我们计划通过一些小的demo&#xff0c;逐渐学习CGAL的使用方法。目前&#xff0c;由于我们还缺少对CGAL的整体把握&#xff0c;所以demo没有连贯性&#xff0c;且难度飘忽不定。不过随着我们的…

CGAL例程:点云数据三维重建

作者&#xff1a;西蒙吉罗多 链接&#xff1a;CGAL 5.4 - Manual: Surface Reconstruction from Point Cloudshttps://doc.cgal.org/latest/Manual/tuto_reconstruction.html 目录 2 我应该使用哪种算法&#xff1f; 3 管道概览 4 读取点云数据 5 点云预处理 5.1 异常值…

CGAL编译与配置

从来没有自己编译过第三方库&#xff0c;每次看到cmake上那些红色的错误就头疼&#xff0c;从来都是伸手党&#xff0c;不过这次没有要到编译好的CGAL&#xff0c;只能硬着头皮自己来。当编译完看到自己的例子跑通&#xff0c;才发现并没有想象中的复杂。 (此方法在win7和win1…

CGAL的使用

1 C++类的知识 因为CGAL是用C++实现的,所以需要先了解一下C++编程。C++是面向对象的编程,这也是C++对C语言改进的最重要的部分。C++也被叫做是"带类的 C"。简单讲一下类的构成,成员函数以及对象的定义和使用。 1.1 C++类的构成 首先从C的结构体说起。C中的结构体我想…

cgal配置以及一些资料

Win7下VS2008编译CGAL3.9 &#xff08;转&#xff1a;http://blog.csdn.net/wsh6759/article/details/6977847&#xff09; CGAL是比较经典的计算几何库&#xff0c;算法经典&#xff0c;稳定高效。 本文介绍编译CGAl情况&#xff0c; 前期准备&#xff1a; BOOST&#x…

CGAL学习记录

CGAL学习记录 前言CGAL 介绍CGAL Linux安装CGAL Windos安装CGAL 安装错误及解决办法CGAL 安装后测试CGAL I/O读写 FunctionsCGAL OFF数据格式CGAL OFF STL相互转换CGAL 表面细化CGAL 表面平滑CGAL 表面补洞CGAL 自相交检测CGAL 提取中心线 前言 原先使用vtk有些小地方不是很理…

CGAL Cookbook --CGAL简介

##前言 ## 接触CGAL已经有半年了&#xff0c;从最初的厌恶&#xff08;对于一个初学者来说&#xff0c;CGAL确实有点难度&#xff0c;它要求初学者有一定基础&#xff09;到后来的喜欢。现在觉得CGAL简直完美极了&#xff0c;虽然它存在一些BUG但是这并不妨碍我爱上CGAL。它的…

[CGAL] CGAL的编译与使用

文章目录 方法一&#xff1a;自己安装依赖库安装Boost安装CGAL安装Qt编译示例在VS中使用CGAL库引用boost引用gmp引用CGALHelloworld 报错处理在cmake配置时报错&#xff1a;未能找到Boost编译示例&#xff0c;未找到GMP编译Mesh_3例子报错&#xff1a;未能找到Eigen3编译demo/P…

CGAL学习之路(三):CGAL读写点云

文章目录 1 CGAL创建点云1.1 insert方式1.2 迭代器方式 2 CGAL读点云2.1 读取XYZ点云2.2 读取PLY点云2.3 ifstream读取XYZ \ PLY点云 3 CGAL输出点坐标3.1 输出点云所有坐标3.2 输出某一点的坐标3.3 输出XYZ坐标 4 CGAL保存点云&#xff08;XYZ | PLY&#xff09;5 添加法向量字…

【C++】CGAL学习笔记

一、HELLO WORLD 1. 官方文档&#xff1a;CGAL-TUTORIALS 2. 所有CGAL头文件都在子目录中。所有CGAL类和函数都在命名空间中。类以大写字母开头&#xff0c;全局函数以小写字母开头&#xff0c;常量全部大写。 3. 几何图元&#xff0c;如点、线等都定义在内核Kernel中 #inc…

自动化测试方案设计和实现

编辑推荐: 本文主要介绍了几种测试类型需求&#xff0c;以及自动化测试方案设计和实现&#xff0c;希望对您的学习有所帮助。 本文来自于知乎&#xff0c;由火龙果软件Alice编辑、推荐。 如果对软件测试、接口、自动化、性能测试、测试开发、面试经验交流。感兴趣可以8101198…

测试方案模板

&#xff08;iwebshop项目&#xff09;测试方案 &#xff08;仅供参考&#xff09; 文档版本控制 文档版本号 日期 作者 审核人 说明 V1.0 2017/11/24 陈.. 创建文档 1. 概述 【软件的错误是不可避免的&#xff0c;所以必须经过严格的测试。通过对…

测试方案/测试计划/测试报告,经常弄混要怎么区分?

目录 前言 1、测试方案和测试计划的区别 2、测试方案和测试计划什么时候编写 3、测试方案 4、测试计划 5、测试报告 前言 测试方案和测试计划&#xff0c;测试报告几乎都是每个测试人员都必须掌握的。但有时经常搞混&#xff0c;特别是测试方案和测试计划。 1、测试方案…

自动化测试方案

自动化测试体系方案 方案1全编写代码流程 UI自动化&#xff1a; 使用python或java&#xff0c;配合selenium库及pytest框架做UI自动化测试。&#xff08;通过selenium的webdriver驱动&#xff0c;驱使浏览器&#xff09; 1. WebDriver API&#xff08;基于Java、Python&…

SpringBoot - 应用程序测试方案

文章目录 PreSpring Boot 中的测试解决方案测试 Spring Boot 应用程序初始化测试环境SpringBootTestSpringBootTest - webEnvironment RunWith 注解与 SpringRunner 执行测试用例使用 DataJpaTest 注解测试数据访问组件Service层和Controller的测试使用 Environment 测试配置信…

测试计划和测试方案有什么区别?

一、测试计划 1、测试计划是什么&#xff1f; 测试计划是组织管理层面的文件&#xff0c;从组织管理的角度对一次测试活动进行规划。对测试全过程的测试范围、组织、资源、原则等进行规定和约束&#xff0c;并制定测试全过程各个阶段的任务分配以及时间进度安排&#xff0c;并…

测试方案的设计及模板

测试方案设计及模板 测试方案设计概括xx测试方案_模板1.引言2.测试策略3.测试设计4.测试资源5.输出文档6.修订记录推荐书籍 测试方案设计概括 xx测试方案_模板 1.引言 1.1目的 根据需要实现的需求与软件的设计架构&#xff0c;设计满足测试目标的方案&#xff0c;用来指导测试…

软件测试方案设计

文章目录 1、软件框架2、测试方案设计2.1、测试覆盖2.2、功能测试和压力测试2.3、自动化测试2.4、持续集成 1、软件框架 站在软件的角度&#xff0c;一个系统通常可以分为以下四个层次&#xff1a; 应用软件层(app layer)。用户重点自己开发的应用代码&#xff0c;例如我们的运…

又发现个新的全网资源搜索神器

平常用谷歌百度搜资源一个个翻很费劲&#xff0c;这里分享几个最新可用的全网网盘资源搜索神器&#xff0c;在公众号苏生不惑后台回复神器 获取软件下载地址。 混合盘 这个app聚合搜索多个网盘资源&#xff0c;可从数十个网盘搜索网站中检索出你需要的资源&#xff0c;app非常简…

magnetX,资源搜索神器!老司机快上车!

magnetX 中文版是一款非常好用的资源搜索神器&#xff0c;界面干净简洁并且不会有弹窗。而且可以通过关键字搜索&#xff0c;或者网址过滤这两种方式快速找到你想要的资源&#xff0c;并且还能配合WebTorrent在线播放&#xff0c;还可以配合下载工具进行一键下载资源&#xff0…