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告诉我一起来完好这个项目。

  • 相关阅读:
    oracle的安装与plsql的环境配置
    Working with MSDTC
    soapui-java.lang.Exception Failed to load url
    Oracle 一个owner访问另一个owner的table,不加owner
    Call API relation to TLS 1.2
    Call API HTTP header Authorization: Basic
    VS2008 .csproj cannot be opened.The project type is not supported by this installat
    The changes couldn't be completed.Please reboot your computer and try again.
    Create DB Table View Procedure
    DB Change
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/7277938.html
Copyright © 2011-2022 走看看