OpenSAML2.X 在SSO系统中的应用
背景
年底的时候有机会开发一个SPA(单页面应用)的项目,那时候须要用到票据的方式能够用Cookie的方式来登录。当是想到了OpenID或者是CAS的方式来做统一认证中心。后来一个安全界的大牛推荐让我用SAML,就走上了一条SAML路。后来因为个人原因离开了那个SPA项目,离开的时候SAML还没有開始做,仅仅是大致上了解一些,后来在工作之余还是把OpenSAML2.X 完好成项目。
项目地址:https://github.com/MrsSunny/SSO-OpenSAML
项目介绍
OpenSAML在网上的资料也不是非常多。在国内用到的应该也不是非常多,自己摸索着做了一下,有可能自己对OpenSAML的理解不够到位
有一起研究的同学发邮箱到liuzhenx@hotmail.com一起学习。假设有错误的地方欢迎指正。 非常感谢。
项目准备
安装Java
安装Maven
安装Eclipse
Maven 配置
配置OpenSAML 依赖
(详细的maven配置能够參考我GitHub上的项目)
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml</artifactId>
<version>2.6.4</version>
</dependency>
使用方式
OpenSAML提供了几种方式来实现SSO之间SAML传输数据。本文主要对HTTP Artifact Binding 做个简介
项目流程
1.用户訪问SP的受保护资源
2.SP检查是否实用户的Session。假设用则直接訪问
3.假设没有Session上下文SP随机生成Artifact,并生成AuthnRequest
假设在Cookie中发现票据信息,把票据信息放到AuthnRequest其中
4.SP建立Artifact与AuthnRequest的关联信息
5.SP重定向到IDP的接受Artifact接口,用Get方式发送Artifact,和SP在IDP中的注冊ID
6.IDP接受Artifact。然后用HTTP POST方式来请求SP的getAuthnRequest接口(參数为Artifact)
7.SP 接受到IDP传过来的Artifact ,依据Artifact 把关联的AuthnRequest返回给IDP
8.IDP接受到getAuthnRequest然后来验证AuthnRequest的有效性,检查 Status Version 等信息。假设Cookie中的票据不为空,则检查票据是否正确。是否在有效期内。假设票据为空,则重定向用户到登录页面来提交信息。
9.假设票据正确或者用户通过输入usernamepassword等信息通过验证。则IDP生成Artifact对象,IDP生成Response对象,并依据用户信息生成断言,同一时候对Response 中的 断言做签名处理,对票据对象做加密和签名处理,并把票据信息写入Cookie,并建立Artifact与Response的关联关系,并重定向浏览器到SP的getArtifact接口
10. SP 接受到Artifact,并通过HTTP POST的方式把Artifact发送到IDP
11. IDP通过Artifact找到关联的Response对象返回给SP
12.SP接受到IDP传输过来的Response对象。首先对Response中的断言做验签操作。假设通过,则允许用户訪问资源。
⚠️:事实上对于SP和IDP而言重要的信息事实上是AuthnRequest和Response。仅仅只是每次传输到浏览器的时候都是传递的是各自的引用,然后SP和IDP再更具饮用来获取 真实的数据。
这样做安全性可能更高一点,可是添加了SP和IDP之间的通信。
⚠️:其中用到的证书都是自己用OpenSSL自己制定。
參考地址:http://blog.csdn.net/howeverpf/article/details/21622545
SP端发送Artifact的JSP页面
<body>
<form action="<%=SysConstants.IDPRECEIVESPARTIFACT_URL%>" method="get" name = "autoForm">
<input type="hidden" name="artifact" value="<%=request.getAttribute(SysConstants.ARTIFACT_KEY)%>" />
<input type="hidden" name="token" value="<%=request.getAttribute(SysConstants.TOKEN_KEY)%>" />
</form>
</body>
<script type="text/javascript">
document.autoForm.submit();
</script>
IDP 端接收Artifact的代码
// 获取Artifact
String artifactBase64 = request.getParameter(SysConstants.ARTIFACT_KEY);
if (null == artifactBase64) {
throw new RuntimeException("SP端传递的Artifact參数错误,该參数不可为空");
}
final ArtifactResolve artifactResolve = samlService.buildArtifactResolve();
final Artifact artifact = (Artifact) samlService.buildStringToXMLObject(artifactBase64);
artifactResolve.setArtifact(artifact);
// 对artifactResolve对象进行签名操作
samlService.signXMLObject(artifactResolve);
String artifactParam = samlService.buildXMLObjectToString(artifactResolve);
String postResult = null;
//依据Artifact信息从SP中获取Auth Request信息
try {
postResult = HttpUtil.doPost(SysConstants.SP_ARTIFACT_RESOLUTION_SERVICE, artifactParam);
} catch (Exception e) {
throw new RuntimeException("訪问SP端的:" + SysConstants.SP_ARTIFACT_RESOLUTION_SERVICE + "服务错误" + e.getMessage());
}
if (null == postResult || "".equals(postResult)) {
return "redirect:/loginPage";
}
final ArtifactResponse artifactResponse = (ArtifactResponse) samlService.buildStringToXMLObject(postResult);
final Status status = artifactResponse.getStatus();
if (null == status) {
throw new RuntimeException("client的Status不能为空");
}
StatusCode statusCode = status.getStatusCode();
if (null == statusCode) {
throw new RuntimeException("无法获取statusCode");
}
String codeValue = statusCode.getValue();
// 推断SAML的StatusCode
if (codeValue == null || !codeValue.equals(StatusCode.SUCCESS_URI)) {
throw new RuntimeException("无法获取codeValue, 认证失败, SP端数据错误");
}
final String inResponseTo = artifactResponse.getInResponseTo();
final String artifactResolveID = artifactResolve.getID();
if (null == inResponseTo || !inResponseTo.equals(artifactResolveID)) {
logger.error("artifact的值和接收端的inResponseTo的值不一致。认证错误");
throw new RuntimeException("artifact的值和接收端的inResponseTo的值不一致,认证错误");
}
final AuthnRequest authnRequest = (AuthnRequest) artifactResponse.getMessage();
if (authnRequest == null) {
throw new RuntimeException("无法获取AuthRequest数据。认证错误");
}
// 获取SP的消费URL。下一步回调须要用到
final String customerServiceUrl = authnRequest.getAssertionConsumerServiceURL();
if (null == customerServiceUrl) {
throw new RuntimeException("无法获取customerServiceUrl,SP端数据错误");
}
request.setAttribute(SysConstants.ACTION_KEY, customerServiceUrl);
HttpSession session = request.getSession(false);
session.setAttribute(SysConstants.ACTION_KEY, customerServiceUrl);
final SAMLVersion samlVersion = authnRequest.getVersion();
// 推断版本号是否支持
if (null == samlVersion || !SAMLVersion.VERSION_20.equals(samlVersion)) {
throw new RuntimeException("SAML版本号错误,仅仅支持2.0");
}
final Issuer issuer = authnRequest.getIssuer();
final String appName = issuer.getValue();
// 推断issure的里面的值是否在SSO系统中注冊过
final App app = appService.findAppByAppName(appName.trim());
if (app == null) {
throw new RuntimeException("不支持当前系统: " + appName);
}
Subject subject = authnRequest.getSubject();
NameID nameID = subject.getNameID();
String ticket = nameID.getValue();
if ("".equals(ticket) || null == ticket) {
return "redirect:/loginPage";
}
try {
ticket = URLDecoder.decode(ticket, SysConstants.CHARSET);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
/**
* 解密ticket
*/
PrivateKey privateKey = samlService.getRSAPrivateKey();
String[] afterDecode = null;
try {
byte[] ticketArray = Base64.decode(ticket);
byte[] decryptTicketArray = RSACoder.INSTANCE.decryptByPrivateKey(privateKey, ticketArray);
String decryptTicket = new String(decryptTicketArray);
afterDecode = decryptTicket.split(SysConstants.TICKET_SPILT);
} catch (Exception e) {
return "redirect:/loginPage";
}
logger.debug("Ticket验证痛过");
// 推断令牌是否过期,假设令牌过期则直接
if (afterDecode == null || afterDecode.length != 3) {
return "redirect:/loginPage";
}
long expireTime = Long.parseLong(afterDecode[2]);
long nowTime = System.currentTimeMillis();
if (expireTime < nowTime) {
logger.debug("Token已过期");
return "redirect:/loginPage";
}
final Artifact idpArtifact = samlService.buildArtifact();
final Response samlResponse = samlService.buildResponse(UUIDFactory.INSTANCE.getUUID());
long id = Long.parseLong(afterDecode[0]);
User user = new User();
user.setId(id);
user.setEmail(afterDecode[1]);
samlService.addAttribute(samlResponse, user);
SSOHelper.INSTANCE.put(idpArtifact.getArtifact(), samlResponse);
request.setAttribute(SysConstants.ARTIFACT_KEY, samlService.buildXMLObjectToString(idpArtifact));
return "/saml/idp/send_artifact_to_sp";
SP接受Artifact的代码
String spArtifact = request.getParameter(SysConstants.ARTIFACT_KEY);
if (null == spArtifact) {
throw new RuntimeException("无法获取IDP端传过来的Artifact");
}
ArtifactResolve artifactResolve = samlService.buildArtifactResolve();
Artifact artifact = (Artifact) samlService.buildStringToXMLObject(spArtifact);
artifactResolve.setArtifact(artifact);
samlService.signXMLObject(artifactResolve);
String requestStr = samlService.buildXMLObjectToString(artifactResolve);
String postResult = null;
try {
postResult = HttpUtil.doPost(SysConstants.IDP_ARTIFACT_RESOLUTION_SERVICE, requestStr);
} catch (Exception e) {
e.printStackTrace();
logger.error("訪问IDP的" + SysConstants.IDP_ARTIFACT_RESOLUTION_SERVICE + "服务错误");
}
if (null == postResult) {
throw new RuntimeException("从" + SysConstants.IDP_ARTIFACT_RESOLUTION_SERVICE + "服务获取的数据为空,请检查IDP端数据格式");
}
ArtifactResponse artifactResponse = (ArtifactResponse) samlService.buildStringToXMLObject(postResult);
SAMLObject samlObject = artifactResponse.getMessage();
if (null == samlObject) {
throw new RuntimeException("无法获取Response信息");
}
Response samlResponse = (Response) samlObject;
List<Assertion> assertions = samlResponse.getAssertions();
if (null == assertions || assertions.size() == 0) {
throw new RuntimeException("无法获取断言,请又一次发起请求!!
!
");
}
Assertion assertion = samlResponse.getAssertions().get(0);
if (assertion == null) {
request.setAttribute(SysConstants.ERROR_LOGIN, true);
} else {
/**
* 验证签名
*/
boolean signSuccess = samlService.validate(assertion);
if (!signSuccess) {
throw new RuntimeException("验证签名错误");
}
HttpSession session = request.getSession(false);
List<AttributeStatement> arrtibuteStatements = assertion.getAttributeStatements();
if (null == arrtibuteStatements || arrtibuteStatements.size() == 0) {
throw new RuntimeException("无法获取属性列表。请又一次发起请求");
}
AttributeStatement attributeStatement = assertion.getAttributeStatements().get(0);
List<Attribute> list = attributeStatement.getAttributes();
if (null == list) {
throw new RuntimeException("无法获取属性列表IDP端错误");
}
User user = new User();
list.forEach(pereAttribute -> {
String name = pereAttribute.getName();
XSString value = (XSString) pereAttribute.getAttributeValues().get(0);
String valueString = value.getValue();
if (name.endsWith("Name")) {
user.setName(valueString);
} else if (name.equals("Id")) {
user.setId(Long.parseLong(valueString));
} else if (name.equals("LoginId")) {
user.setLogin_id(valueString);
} else if (name.equals("Email")) {
user.setEmail(valueString);
}
});
session.setAttribute(SysConstants.LOGIN_USER, user);
putAuthnToSecuritySession("admin", "admin");
request.setAttribute(SysConstants.ERROR_LOGIN, false);
}
学习目标
感觉自己认识的不够全面,希望有同学一起研究一下,特别是SP端发送Auth Request的时候,应该带上什么数据,票据是否在这个阶段发送,以及怎么发送。有兴趣的同学能够把GitHub告诉我一起来完好这个项目。
OpenSAML2.X 在SSO系统中的应用的更多相关文章
- C#.NET 大型企业信息化系统 - 防黑客攻击 - SSO系统加固优化经验分享
好久没写文章了,突然间也不知道写什么好了一样,好多人可能以为我死了,写个文章分享一下.证明一下自己还在,很好的活着吧,刷个存在感. 放弃了很多娱乐.休闲.旅游.写文章.看书.陪伴家人,静心默默的用了接 ...
- iOS 的 APP 在系统中如何适配不同的屏幕的尺寸
iOS 的 APP 在系统中如何适配不同的屏幕的尺寸 标签: 2007年,初代iPhone发布,屏幕的宽高是 320 x 480 像素.下文也是按照宽度,高度的顺序排列.这个分辨率一直到iPhone ...
- SSO系统的实现
当一个网站系统比较大型的时候,我们通常采用面向服务的编程,采用分布式的编程.各个子系统共同来实现一个大的系统,这时候登录注册功能的实现也面临着一些问题. 一.WHAT? SSO是什么? sso是单点登 ...
- Linux系统中tomcat的安装及优化
Linux系统中Tomcat 8 安装 Tomcat 8 安装 官网:http://tomcat.apache.org/ Tomcat 8 官网下载:http://tomcat.apache.org/ ...
- 单点登录--sso系统
SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次主要的登录映射到其他应用中用于同一个用户的登录的机制 ...
- e3mall商城总结11之sso系统的分析、应用以及解决ajax跨域问题
说在前面的话 一.sso系统分析 什么是sso系统 SSO英文全称Single Sign On,单点登录.SSO是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统.它包括可以将这次 ...
- SpringCloud微服务实战——搭建企业级开发框架(四十):使用Spring Security OAuth2实现单点登录(SSO)系统
一.单点登录SSO介绍 目前每家企业或者平台都存在不止一套系统,由于历史原因每套系统采购于不同厂商,所以系统间都是相互独立的,都有自己的用户鉴权认证体系,当用户进行登录系统时,不得不记住每套系统的 ...
- Linux系统中的Device Mapper学习
在linux系统中你使用一些命令时(例如nmon.iostat 如下截图所示),有可能会看到一些名字为dm-xx的设备,那么这些设备到底是什么设备呢,跟磁盘有什么关系呢?以前不了解的时候,我也很纳闷. ...
- 电商系统中的商品模型的分析与设计—续
前言 在<电商系统中的商品模型的分析与设计>中,对电商系统商品模型有一个粗浅的描述,后来有博友对货品和商品的区别以及属性有一些疑问.我也对此做一些研究,再次简单的对商品模型做一个介 ...
随机推荐
- Javascript:前端利器 之 JSDuck
背景 文档的重要性不言而喻,对于像Javascript这种的动态语言来说就更重要了,目前流行的JDoc工具挺多的,最好的当属JSDuck,可是JSDuck在Windows下的安装非常麻烦,这里就写下来 ...
- sql 的 DATE_FORMATE()函数
定义和用法 DATE_FORMAT() 函数用于以不同的格式显示日期/时间数据. 语法 DATE_FORMAT(date,format) date 参数是合法的日期.format 规定日期/时间的输出 ...
- 从CVPR 2014看计算机视觉领域的最新热点
编者按:2014年度计算机视觉方向的顶级会议CVPR上月落下帷幕.在这次大会中,微软亚洲研究院共有15篇论文入选.今年的CVPR上有哪些让人眼前一亮的研究,又反映出哪些趋势?来听赴美参加会议的微软亚洲 ...
- iOS:切换视图时,反向传递数据方法一:通知
通知方式: 1.有一个(单例)通知中心,负责管理iOS中的所有通知 2.需要获取某种通知,必须注册成为观察者(订阅) 3.不再需要取某种通知时,要取消注册. 4.你可以向通知中心发送某种通知,通知中心 ...
- 如何在SharePoint的列表中使用通配符来filter出ListItem?
一个朋友问我这样一个问题, 他想要快速从SharePoint的文档库中filter出来名字中先带有一个Q, 接着一些其他的字符, 后面再跟着有一个数字20这样的文件. 第一个想法就是修改Share ...
- java后台与jsp前台特殊字符处理(字符串编码与解码)
在后台与前台数据交互时如果有特殊字符就很容易出现问题,所以就需要对字符串进行编码传输,在获取后再进行解码: 1.Java后台进行编码与解码 URLEncoder.encode(str,"ut ...
- java学习笔记6--类的继承、Object类
接着前面的学习: java学习笔记5--类的方法 java学习笔记4--类与对象的基本概念(2) java学习笔记3--类与对象的基本概念(1) java学习笔记2--数据类型.数组 java学习笔记 ...
- [Python爬虫] 之八:Selenium +phantomjs抓取微博数据
基本思路:在登录状态下,打开首页,利用高级搜索框输入需要查询的条件,点击搜索链接进行搜索.如果数据有多页,每页数据是20条件,读取页数 然后循环页数,对每页数据进行抓取数据. 在实践过程中发现一个问题 ...
- luigi操作hive表
关于luigi框架下查询hive表的操作 class JoinQuery(HiveQueryTask): date=luigi.DateParameter() def hiveconfs(self): ...
- Kettle中根据一个输入行派生出多个输出行
依然在北京,早上停电了,整个人感觉对不好了,接下来就说一下在使用ETL工具kettle做数据校验的时候遇到的一些问题,一级解决方案. 1:数据校验效果图下图: 原始表数据(需要校验的表数据) 对上表数 ...