zoukankan      html  css  js  c++  java
  • 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告诉我一起来完好这个项目。

  • 相关阅读:
    第一个struts程序的配置过程
    博客园文章中图片太大显示不全的解决办法
    将struts的jar包拷贝到WEB-INF/lib导致eclipse中配置好的javadoc失效
    eclipse手动build整个project
    four application:geocoder widget
    数据库 日期格式操作
    third application :Directions widget
    second application:use an arcgis.com webmap
    first application
    Android4.4中不能发送SD卡就绪广播
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7277938.html
Copyright © 2011-2022 走看看