why ADFS
之所以和ADFS 'sayhello'是公司要求,實現內網項目在外網下的SSO登錄訪問
當第一次看到ADFS時,第一想到是公司內部哪個工程師搞得一個架構,取英文縮寫ADFS,大概用於身份認證,提到到認證方式,想到目前市面主流的oauth2,Jwt,OpenID等,基於SAML2.0的ADFS服務器集成方案是啥,如果是內部框架也沒有太詳細的部署方案啊,去搜索saml概念,發現是網上有介紹,看一些saml介紹發現ADFS也是有介紹的.那么正式進入學習狀態:
搞懂需求上寫的啥至少前提要搞懂三種概念,
2.SAML概念
3.ADFS概念
如果用了多年windows系統對windows域與工作組概念還模糊的查看windows域與工作組概念
學習新東西啃專業名詞理解概念及原理是不可缺少的一步,以上概念如果你讀了一遍還是蒙圈狀態,那就多讀幾遍,書讀百遍,其義自見.要不然不知道怎么動手,下面就大概記錄下web端大概的集成思路
集成方案說明
系統使用ADFS主要的目的:目前系統使用LDAP協議的AD域模式登錄,是在內網下運行,如果實現系統的外網訪問,保證用戶身份驗證的安全性,選擇采用基於SAML的ADFS聯合身份驗證方案,具體的集成方案說明如下:
這張圖取自: https://www.oasis-open.org/committees/download.php/11511/sstc-saml-tech-overview-2.0-draft-03.pdf
以上是用戶認證的簡單流程圖,圖中共有三個角色:
Server Provider(SP):服務提供者,提供服務和資源
Identity Provider(IDP):身份鑒別服務器,認證用戶並生成saml斷言,即ADFS服務器
Client:用戶訪問服務的客戶端,比如瀏覽器
此圖片說明了以下步驟:
用戶嘗試訪問SP提供的服務。
SP根據配置生成一個 SAML格式的身份驗證請求數據包。配置包括ADFS服務器地址,證書,IDP返回SP消息數據的URL地址,身份認證協議等信息,通過瀏覽器重定向到ADFS服務器進行SAML身份認證.
IDP解碼SAML請求,並對用戶進行身份驗證,IDP要求提供有效登錄憑據以驗證用戶身份。
IDP生成一個 SAML 響應,其中包含經過驗證的用戶的用戶名等信息。按照 SAML 2.0 規范,此響應將使用IDP的 DSA/RSA 公鑰和私鑰進行數字簽名。
IDP將信息返回到用戶的瀏覽器。根據配置的返回路徑,瀏覽器將該信息轉發到 SP。
SP使用IDP的公鑰驗證 SAML 響應。如果成功驗證該響應,SP則會將用戶重定向到目標網址。
用戶成功登陸系統。
集成配置說明
web端啟用基於SAML的SSO,使用OneLogin的開源SAML Java工具包,提供SAML身份驗證,應用程序啟用單點登錄(SSO)。
1.引入依賴
核心包(com.onelogin:JavaSAML-core)
用於處理AuthNRequest、SAMLResponse、LogoutRequest、LogoutResponse和元數據的類和方法。此外,它還包含類來加載工具箱和HttpRequest類的設置,HttpRequest類是一個框架無關的HTTP請求表示。
工具包(com.onelogin:JavaSAML)
包含一個帶有Auth類的,用於處理javax.servlet.http對象,javax.servlet.http對象。
github地址:https://github.com/onelogin/java-saml
Java-SAML具有以下依賴關系:
org.apache.santuario:xmlsec
joda-time:joda-time
org.apache.commons:commons-lang3
commons-codec:commons-codec
testing:
org.hamcrest:hamcrest-core and org.hamcrest:hamcrest-library
junit:junit
org.mockito:mockito-core
logging:
org.slf4j:slf4j-api
ch.qos.logback:logback-classic
For CI:
org.jacoco:jacoco-maven-plugin
具體請參看官網說明,官網地址:
https://developers.onelogin.com/saml/java
2.onelogin.saml.properties配置
所有設置都在此文件中定義。
首先,我們需要配置SP的信息,IDP的信息,在某些情況下,還需要高級安全問題的配置,如簽名和加密。
SAML JAVA Toolkit已經封裝的相對比較完善,所以關鍵點在於我們對自己的Java項目進行正確的配置就可以接入
實際項目配置的onelogin.saml.properties如下:注解項除外保持默認值即可 # If 'strict' is True, then the Java Toolkit will reject unsigned # or unencrypted messages if it expects them signed or encrypted # Also will reject the messages if not strictly follow the SAML onelogin.saml2.strict = false # Enable debug mode (to print errors) onelogin.saml2.debug = false # Service Provider Data that we are deploying # # Identifier of the SP entity (must be a URI) onelogin.saml2.sp.entityid = https://cat-dev.###.com/cat/jsp/sso/metadata.jsp # Specifies info about where and how the <AuthnResponse> message MUST be # returned to the requester, in this case our SP. # URL Location where the <Response> from the IDP will be returned onelogin.saml2.sp.assertion_consumer_service.url = https://cat-dev.###.com/cat/r # SAML protocol binding to be used when returning the <Response> # message. Onelogin Toolkit supports for this endpoint the # HTTP-POST binding only onelogin.saml2.sp.assertion_consumer_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST # Specifies info about where and how the <Logout Response> message MUST be # returned to the requester, in this case our SP. onelogin.saml2.sp.single_logout_service.url = # SAML protocol binding to be used when returning the <LogoutResponse> or sending the <LogoutRequest> # message. Onelogin Toolkit supports for this endpoint the # HTTP-Redirect binding only onelogin.saml2.sp.single_logout_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect # Specifies constraints on the name identifier to be used to # represent the requested subject. # Take a look on lib/Saml2/Constants.php to see the NameIdFormat supported onelogin.saml2.sp.nameidformat = urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified # Usually x509cert and privateKey of the SP are provided by files placed at # the certs folder. But we can also provide them with the following parameters # test----------------------------------------------------------- onelogin.saml2.sp.x509cert = -----BEGIN CERTIFICATE-----\nMIICpDCCAYwCCQCaHqptRwiRkDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTgwNjEzMDYwNjE4WhcNMjgwNjEwMDYwNjE4WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQChL1RZp0wr7OgaAUk/n4woZgIv0i3ub7nLwgulK6TFYUUo66gKy/Pvpafwn4UklW0xv0Yfr2inyMjbTvmVAtUKWloPrEwvCw1N4w1Fo9KiPUkUt9mrAkiBgp7tbXE34wDT2qJZpx+3ne70nfIJcY3GUYx/FksKvo6s9yJ+GW4nZkMZEkAHD3AZwIF0OZwRicezbSsPOukE6Poc9q7bwoAjDrW8Ab3ll7U4F14ErKU/eesZ5lmTOKaEwZRkuq/1XwVQdFHySdUQuxdDsSNKbVOVBGL6r6ZAcyxX8KBunU6A3E55UWA2I4mg+rFRAEuXm6BAUOdkkS8yPyMkf+IeoIA7AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFR7Zmoug2EBjRtgF8GjpNAg94rZEBB6vWkNJPsFE6Bc9v7jOULNUuJUF+jvVntSrvs024ONHbAqD/2j/uKpO6Np7LUDEy/lY6t3IpZAmymrWjoVnm1/VZYm2VRZmOSlrkIuZLwu3LWwd39zxL4mDLbZTTQHz3SdiZP6MGIUgZy5NERCmJnLwPrrgHu0JJqZtXiwqAQUlyT1cZXwiOulwQd/3TDwxPPY/zTbclZw3u9WskLgjO0M0mKlg20rZxLpiTSUBDHMGsETKW5k6Q2fwwRreKROUG0zL62Jlj4DIIvW6vDNL8NQSnhohLOfXQMaSZeiH1XUuxC1ZR4fq+yCl7o=\n-----END CERTIFICATE----- # Requires Format PKCS#8 BEGIN PRIVATE KEY # If you have PKCS#1 BEGIN RSA PRIVATE KEY convert it by openssl pkcs8 -topk8 -inform pem -nocrypt -in sp.rsa_key -outform pem -out sp.pem # test----------------------------------------------------------- onelogin.saml2.sp.privatekey = -----BEGIN PRIVATE KEY-----\nMIIEowIBAAKCAQEAoS9UWadMK+zoGgFJP5+MKGYCL9It7m+5y8ILpSukxWFFKOuoCsvz76Wn8J+FJJVtMb9GH69op8jI2075lQLVClpaD6xMLwsNTeMNRaPSoj1JFLfZqwJIgYKe7W1xN+MA09qiWacft53u9J3yCXGNxlGMfxZLCr6OrPcifhluJ2ZDGRJABw9wGcCBdDmcEYnHs20rDzrpBOj6HPau28KAIw61vAG95Ze1OBdeBKylP3nrGeZZkzimhMGUZLqv9V8FUHRR8knVELsXQ7EjSm1TlQRi+q+mQHMsV/Cgbp1OgNxOeVFgNiOJoPqxUQBLl5ugQFDnZJEvMj8jJH/iHqCAOwIDAQABAoIBADXOK9UlsJq1IaGUrlPruYi+zJoUCjse1qG669I+KGmvF7waNmUsQgjMfqwnQ/W7X9EMbackEcZ4kvwKd+wTHvSuxoOW23OUt+M5GPQXRLfdx2iAGswoHfYFmXHeZ73lLCCMSketLzxHHz5O/z3BxzbdgA3objJu/AenE7+OU6QYykjCn1zMi5yuYLPV8NytZCga4iGw8XS+H5KBfno4OQYPjyow+Q4itSlOJXdc6KFcbqzZXb5tZ+Q82J8qRUjQBgAqyQgOXTfDj/1IdwEn0yHlnjcT9tr+dNPAgeu8cknd9zcxl6xvYE1eYg620X0q5H3q0JJ2k+LRtxI6+Hr3BYkCgYEAzj/N1k53QfcOyCXq8afIGrFw51zeo3rv32AfkKSn6feN2Gy+9I3n9iOK2KTzemAjA3WbEboKK7l1rjUbU+3Svr3b7X1AnonyfBq2yvKWB4TVWkIWlpCy5pLRduY8OiJRtg0SJT8DPJMxESnkyZkqtZJL8LmT8Dw/eYVVY3ZakXcCgYEAyBC8G+8yMb+sgBfl7TAfTcLXk5n8F8nkpWKAcW4VFXtodrAXcLBClrJ+/BUkt/sqZA1IMjbUR094owGEyopJ1cnghRhmZx9dpJfwzgVPb53TOHTLrxm4A18jARaoLekfZ/PBhQOQiTE8akbPHbuwixcQEBS5jwgYJN2YM9A9mF0CgYBciUv1ByeCtTIworKS0dB6CXq6k3RgrNvKwPnoj7e2xZcir0fNuY2FZdT59qg3E8Mh3jZA8dN2YrNmAfXM5jtT0SNHnpbLiuD8xY+V5tlhbju7T0OLMkjSIrVQP2RuQM+geqTViTwOhYvSQ5WezdXXuVfRHbI+awmfoC77fTKNaQKBgQDCb6kyCOUifmMa1p8KRoOV4m/7LmNXh0qlBTdJhjANgbOD7h3J0jPVG8LYIYBfIkYPmOz6iFkEuRLIcThqU73wfdOr5ovXWx96UISi5XxPQPa/3pr6ISe6dyKg8zEd9XwlXjxMlqtI+kX6D7lI71ljxFVDG7E/diFo6sf6Sz8hrQKBgFyrZqsBgz0R6d9V0R0JWNpdt+WShl5Hb0HrG844ZJQZKZbEwJFDxi0I5GomCydfCEnyBS2+iLYdaNkAYPVjDynb5CZVPzy6shCE6T2GLG9Cc/IXhPMvNQz6RHx1HyM7TegIgQY4hh9ZHtDe8ROZrAucpOG0NHaT+gRuXe4bnedd\n-----END PRIVATE KEY----- # Identity Provider Data that we want connect with our SP # # Identifier of the IDP entity (must be a URI) # test----------------------------------------------------------- onelogin.saml2.idp.entityid = https://sso.###.com/FederationMetadata/2007-06/FederationMetadata.xml # SSO endpoint info of the IDP. (Authentication Request protocol) # URL Target of the IDP where the SP will send the Authentication Request Message # test------------------------------------------------------------- onelogin.saml2.idp.single_sign_on_service.url = https://sso.###.com/adfs/ls # SAML protocol binding to be used when returning the <Response> # message. Onelogin Toolkit supports for this endpoint the # HTTP-Redirect binding only onelogin.saml2.idp.single_sign_on_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect # SLO endpoint info of the IDP. # URL Location of the IDP where the SP will send the SLO Request # test----------------------------------------------------------- onelogin.saml2.idp.single_logout_service.url = # Optional SLO Response endpoint info of the IDP. # URL Location of the IDP where the SP will send the SLO Response. If left blank, same URL as onelogin.saml2.IDP.single_logout_service.url will be used. # Some IDPs use a separate URL for sending a logout request and response, use this property to set the separate response url # test----------------------------------------------------------- onelogin.saml2.idp.single_logout_service.response.url = # SAML protocol binding to be used when returning the <Response> # message. Onelogin Toolkit supports for this endpoint the # HTTP-Redirect binding only onelogin.saml2.idp.single_logout_service.binding = urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect # Public x509 certificate of the IDP # test----------------------------------------------------------- onelogin.saml2.idp.x509cert = MIIC6DCCAdCgAwIBAgIQZiRU+RFa9rRJO57iE1ib9DANBgkqhkiG9w0BAQsFADAwMS4wLAYDVQQDEyVBREZTIFNpZ25pbmcgLSBkZXZvcHMueGlhb2hvbmdzaHUuY29tMB4XDTE3MDcyNTEzMDIxMFoXDTE4MDcyNTEzMDIxMFowMDEuMCwGA1UEAxMlQURGUyBTaWduaW5nIC0gZGV2b3BzLnhpYW9ob25nc2h1LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALfwBsXi/9Hlor/Q8W9ljtRk/Pqv91OPdS5K9f4SyQKs1idFi1fR94uCnbC3HxZeCo4/3iCYoiwpG7PZnuC/EIDIQ0Pdu2b7jOcgmTww6k2CsyZPMZxBWaFPWofwL7kJ+SEUxz0Vd93TV2J/V09Gy6QR6lTBtz2CJZXpkTSYuLnQMfkDiHJKeFsrBkiH5bqVyEriv7rgiFajb8gZdTiEEbWEqGgps++17oCpmz9dMi0Vjz31ij2YHWPh3Y7EJ/aVGBHcFD1KgIqfWeDxJ0e2EjIqM8bdDAq6MsAWll1++BIkKBMUdoFvu7S8jT5aszUnScQzEPCXJL0Cs6rmBhEDoCkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAhjhQbeaMJEYPC3ZwhfFAZMuuCOcFg3MgSh1fMDQ1iqtJ1uX7EpkX9NG0bJfFmyvU/5L/wjV41byg3N0HdHuO1VsFTV4q2XzFZJRBPb7Ta6OIQYw+qIX4RH18TSObguv3GgT7AbWHS8HDAmivmTWoL2fJL9sgIMnv/iL/EyG8iXf1fyZBP6Kr60d3xmPFyhlJ76/hBzqRGW6WrdAFunD1vIWr0u/zIs2GbY6A9CcLmpCTAgVTtytGmt+2iTm7trck1l8qOCQCkqX6iH6swHULLLZTUo6STQdTdsbtymf9GE396l6Pc3JwNXKG0Eq89/3l8o1vmyv7ikW1FdQUWTOs7w== # Instead of use the whole x509cert you can use a fingerprint # (openssl x509 -noout -fingerprint -in "IDP.crt" to generate it, # or add for example the -sha256 , -sha384 or -sha512 parameter) # # If a fingerprint is provided, then the certFingerprintAlgorithm is required in order to # let the toolkit know which Algorithm was used. Possible values: sha1, sha256, sha384 or sha512 # 'sha1' is the default value. # onelogin.saml2.idp.certfingerprint = # onelogin.saml2.idp.certfingerprint_algorithm = sha1 # Security settings # # Indicates that the nameID of the <samlp:logoutRequest> sent by this SP # will be encrypted. onelogin.saml2.security.nameid_encrypted = false # Indicates whether the <samlp:AuthnRequest> messages sent by this SP # will be signed. [The Metadata of the SP will offer this info] onelogin.saml2.security.authnrequest_signed = false # Indicates whether the <samlp:logoutRequest> messages sent by this SP # will be signed. onelogin.saml2.security.logoutrequest_signed = false # Indicates whether the <samlp:logoutResponse> messages sent by this SP # will be signed. onelogin.saml2.security.logoutresponse_signed = false # Sign the Metadata # Empty means no signature, or comma separate the keyFileName and the certFileName onelogin.saml2.security.want_messages_signed = # Indicates a requirement for the <samlp:Response>, <samlp:LogoutRequest> and # <samlp:LogoutResponse> elements received by this SP to be signed. onelogin.saml2.security.want_assertions_signed = false # Indicates a requirement for the Metadata of this SP to be signed. # Right now supported null (in order to not sign) or true (sign using SP private key) onelogin.saml2.security.sign_metadata = # Indicates a requirement for the Assertions received by this SP to be encrypted onelogin.saml2.security.want_assertions_encrypted = false # Indicates a requirement for the NameID received by this SP to be encrypted onelogin.saml2.security.want_nameid = false # Indicates a requirement for the NameID received by this SP to be encrypted onelogin.saml2.security.want_nameid_encrypted = false # Authentication context. # Set Empty and no AuthContext will be sent in the AuthNRequest # You can set multiple values (comma separated them) onelogin.saml2.security.requested_authncontext = urn:oasis:names:tc:SAML:2.0:ac:classes:Password # Allows the authn comparison parameter to be set, defaults to 'exact' onelogin.saml2.security.onelogin.saml2.security.requested_authncontextcomparison = exact # Indicates if the SP will validate all received xmls. # (In order to validate the xml, 'strict' and 'wantXMLValidation' must be true). onelogin.saml2.security.want_xml_validation = true # Algorithm that the toolkit will use on signing process. Options: # 'http://www.w3.org/2000/09/xmldsig#rsa-sha1' # 'http://www.w3.org/2000/09/xmldsig#dsa-sha1' # 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' # 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384' # 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512' onelogin.saml2.security.signature_algorithm = http://www.w3.org/2000/09/xmldsig#rsa-sha1
3.啟動SSO
Auth auth = new Auth(request, response); auth.login();
根據安全設置‘onelogin.saml2.security.authnrequest_signed’發送已簽名或無簽名的AuthNRequest。
IDP然后將SAML響應返回給用戶的客戶端。然后,使用此信息轉發到SP。
4.SP元數據
此代碼將根據我們在配置文件中提供的信息提供我們的SP的XML元數據。
Auth auth = new Auth(); Saml2Settings settings = auth.getSettings(); settings.setSPValidationOnly(true); String metadata = settings.getSPMetadata(); List<String> errors = Saml2Settings.validateMetadata(metadata); if (errors.isEmpty()) { out.println(metadata); } else { response.setContentType("text/html; charset=UTF-8"); for (String error : errors) { out.println("<p>"+error+"</p>"); } }
getSPMetdata將根據設置的安全參數返回簽名或不簽名的元數據。onelogin.saml2.security.sign_metadata。
5.屬性消費者服務
此代碼處理IDP通過用戶客戶端轉發給SP的SAML響應。
Auth auth = new Auth(request, response); auth.processResponse(); if (!auth.isAuthenticated()) { out.println("Not authenticated"); } List<String> errors = auth.getErrors(); if (!errors.isEmpty()) { out.println(StringUtils.join(errors, ", ")); if (auth.isDebugActive()) { String errorReason = auth.getLastErrorReason(); if (errorReason != null && !errorReason.isEmpty()) { out.println(auth.getLastErrorReason()); } } } else { Map<String, List<String>> attributes = auth.getAttributes(); String nameId = auth.getNameId(); String nameIdFormat = auth.getNameIdFormat(); String sessionIndex = auth.getSessionIndex(); String nameidNameQualifier = auth.getNameIdNameQualifier(); String nameidSPNameQualifier = auth.getNameIdSPNameQualifier(); session.setAttribute("attributes", attributes); session.setAttribute("nameId", nameId); session.setAttribute("nameIdFormat", nameIdFormat); session.setAttribute("sessionIndex", sessionIndex); session.setAttribute("nameidNameQualifier", nameidNameQualifier); session.setAttribute("nameidSPNameQualifier", nameidSPNameQualifier); String relayState = request.getParameter("RelayState"); if (relayState != null && relayState != ServletUtils.getSelfRoutedURLNoQuery(request)) { response.sendRedirect(request.getParameter("RelayState")); } else { if (attributes.isEmpty()) { out.println("You don't have any attributes"); } else { Collection<String> keys = attributes.keySet(); for(String name :keys){ out.println(name); List<String> values = attributes.get(name); for(String value :values) { out.println(" - " + value); } } } } }
SAML響應被處理,然后檢查以確保沒有錯誤。它還驗證用戶是否經過身份驗證,然后將用戶數據存儲在會話中。在這一點上,有兩種可能的選擇:
1.如果沒有提供RelayState,我們可以在這個視圖中顯示用戶數據,或者以我們想要的方式顯示用戶數據。
2.如果提供RelayState,則會發生重定向。注意,在重定向之前將用戶數據保存在會話中,以便在RelayState視圖中獲得可用的用戶數據。
為了檢索屬性,使用:
Map<String, List<String>> attributes = auth.getAttributes();
通過這種方法,獲得了一個Map,其中包含了IDP在SAML響應斷言中提供的所有用戶數據。
每個屬性名都可以用作獲取值的鍵。在嘗試獲取屬性之前,請檢查用戶是否經過身份驗證。如果未對用戶進行身份驗證,則將返回一個空Map。例如,如果我們在auth.processResponse之前調用getAttributes,getAttributes()將返回一個空Map。
6.注銷服務
此代碼處理注銷請求和注銷響應。
Auth auth = new Auth(request, response); auth.processSLO(); List<String> errors = auth.getErrors(); if (errors.isEmpty()) { out.println("Sucessfully logged out"); } else { for(String error : errors) { out.println(error); } }
如果sp接收到注銷請求,則驗證請求,關閉會話,並將注銷響應發送到IDP。
參考: