主流的单点登录有 CAS ,Auth2.0, SAML 等,用户可以登录一次就直接使用多个SP(Service Provider 业务提供方)的服务,免去了每个应用都要登录的烦恼,
CAS ,Auth2.0 代码都有不做赘述, 本次讲微软的 SAML协议单点登录, 传统的单点登录 都是使用Filter 实现的, 底层都做了分装 很难去拆开, 如 spring-security 框架 就有对 SAML 的集成, 但 我们不能使用这种方式, 对接一个单点登录我们不可能引入一个框架, 并且无法实现 一个系统为多个单点登录提供服务(一个SP无法为多个IDP提供服务);
参考资料: https://docs.microsoft.com/zh-cn/azure/active-directory/saas-apps/saml-toolkit-tutorial
https://help.aliyun.com/document_detail/160431.html?scm=20140722.184.2.173#h2-78q-k9u-yay
IDP (Identity Provider身份提供方,及 微软的身份认证服务)
SP(Service Provider 业务提供方, 自己开发的系统系统…)
浏览器(用户操作的浏览器)
具体流程:
用户访问SP一个受保护的资源路径 /user/getUserInfo
SP 接收到用户请求后判断用户是否登录, 如果未登录泽生成 SAML请求(走 sso登录流程), 如果登录过直接访问
SP生成 SAML请求, 用base64编码 发送给 浏览器 302 重定向到IDP进行登录
-- 这是一个完整的 sp生成给 idp登录的 saml request , 其中 SAMLRequest 是saml请求
https://login.microsoftonline.com/***/saml2?SAMLRequest=****
解码的时候先使用 URL解码后再使用base64 解码 结果
<samlp:AuthnRequest
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="****" Version="2.0" IssueInstant="2022-09-06T07:21:14Z" Destination="https://login.microsoftonline.com/***/saml2" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="http://localhost:8080/loginSuccess">
<saml:Issuer>urn:***:cloudcomputingsaml:Issuer>
samlp:AuthnRequest>
IDP接收到SAML 请求解析请求并指引用户登录, 登录成功后 IDP向浏览器 返回 SAMLResponse
<form method="post" action="https://sp.example.com/SAML2/SSO/POST" ...>
<input type="hidden" name="SAMLResponse" value="response" />
<input type="hidden" name="RelayState" value="opaque" />
...
<input type="submit" value="Submit" />
form>
表单中的 SAMLResponse 为base64编码的 saml服务器返回的结果 示例
<samlp:Response ID="_5a61d58d-b36d-4bdd-b92b-5c88663fcd5b" Version="2.0" IssueInstant="2022-09-06T08:20:27.536Z" Destination="http://localhost:8080/loginSuccess" InResponseTo="ONELOGIN_95b541d2-7c89-4733-925e-72d40e12dfa4"
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
<Issuer
xmlns="urn:oasis:names:tc:SAML:2.0:assertion">https://sts.windows.net/***/
Issuer>
<samlp:Status>
<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
samlp:Status>
<Assertion ID="_f6f587cb-9449-4d21-9325-c0fed47bfb00" IssueInstant="2022-09-06T08:20:27.521Z" Version="2.0"
xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
<Issuer>https://sts.windows.net/**/Issuer>
<Signature
xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="#_f6f587cb-9449-4d21-9325-c0fed47bfb00">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>**=DigestValue>
Reference>
SignedInfo>
<SignatureValue>RtA/UEFZ/Spvz/2xWyIG97j3/u9d/**/Bg==SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>***7X509Certificate>
X509Data>
KeyInfo>
Signature>
<Subject>
<NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">abcc@***.comNameID>
<SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<SubjectConfirmationData InResponseTo="ONELOGIN_95b541d2-7c89-4733-925e-72d40e12dfa4" NotOnOrAfter="2022-09-06T09:20:27.427Z" Recipient="http://localhost:8080/loginSuccess"/>
SubjectConfirmation>
Subject>
<Conditions NotBefore="2022-09-06T08:15:27.427Z" NotOnOrAfter="2022-09-06T09:20:27.427Z">
<AudienceRestriction>
<Audience>urn:***:cloudcomputingAudience>
AudienceRestriction>
Conditions>
<AttributeStatement>
<Attribute Name="http://schemas.microsoft.com/identity/claims/tenantid">
<AttributeValue>***AttributeValue>
Attribute>
<Attribute Name="http://schemas.microsoft.com/identity/claims/objectidentifier">
<AttributeValue>***AttributeValue>
Attribute>
<Attribute Name="http://schemas.microsoft.com/identity/claims/displayname">
<AttributeValue>aAttributeValue>
Attribute>
<Attribute Name="http://schemas.microsoft.com/identity/claims/identityprovider">
<AttributeValue>https://sts.windows.net/**/AttributeValue>
Attribute>
<Attribute Name="http://schemas.microsoft.com/claims/authnmethodsreferences">
<AttributeValue>http://schemas.microsoft.com/ws/2008/06/identity/authenticationmethod/passwordAttributeValue>
Attribute>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname">
<AttributeValue>bAttributeValue>
Attribute>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname">
<AttributeValue>cAttributeValue>
Attribute>
<Attribute Name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name">
<AttributeValue>abcc@***.onmicrosoft.comAttributeValue>
Attribute>
AttributeStatement>
<AuthnStatement AuthnInstant="2022-09-06T02:31:48.985Z" SessionIndex="_f6f587cb-9449-4d21-9325-c0fed47bfb00">
<AuthnContext>
<AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordAuthnContextClassRef>
AuthnContext>
AuthnStatement>
Assertion>
samlp:Response>
浏览器将IDP返回的 SAML响应转发给 SP的 ACS( assertion consumer service) 地址处理
SP接收到返回的SAML 返回结果后 解码, 并按照自己系统的方式登录, 然后返回给客户之前访问还没有权限的资源列表
原理差不多理解了第一种这个就简单了 不在赘述
/user/sso/saml/getTestSsoPage
/user/sso/saml/generateSamlRequest
请求参数
callbackAddress 回调接口, 必须是绝对路径, 选填, 当登录成功后会访问这个路径, 如果不填写此参数 登录成功后 返回用户浏览器一个固定的默认页面
/user/sso/saml/SSOCallback
由浏览器自动发起调用, 解析saml请求
创建用户并完成本系统的登录
登录成功后如果有回调地址泽调用回调地址
退出登录可以使用我们之前的退出登录但是要求绝对路径配置在 idp服务
我们使用 onelogin 框架进行生成saml 请求 和解析结果
<dependency>
<groupId>com.onelogingroupId>
<artifactId>java-samlartifactId>
<version>2.9.0version>
dependency>
# Service Provider Data that we are deploying
# Identifier of the SP entity (must be a URI)
# sp 唯一标识 在idp 页面查找
onelogin.saml2.sp.entityid = sp:my:system
# Specifies info about where and how the message MUST be
# returned to the requester, in this case our SP.
# URL Location where the from the IdP will be returned
# 回调地址
onelogin.saml2.sp.assertion_consumer_service.url = http://localhost:8080/user/sso/saml/SSOCallback
# Specifies info about Logout service
# URL Location where the from the IdP will be returned or where to send the
onelogin.saml2.sp.single_logout_service.url = https://testtt/user/sso/saml/SSOCallback
# sp 发起请求加密证书 可选 是否开启请求加密, 当开启加密时 saml 请求会多两个参数 Signature 和 SigAlg
onelogin.saml2.security.authnrequest_signed = false
# 加密算法
onelogin.saml2.security.signature_algorithm = http://www.w3.org/2000/09/xmldsig#rsa-sha1
# 秘钥
onelogin.saml2.sp.x509cert = -----BEGIN CERTIFICATE-----\nMIICQjCCAaugAwIBAgIBADANBgkqhkiG9w0BAQ0FADA+MQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDjAMBgNVBAoMBUtZVk9TMRIwEAYDVQQDDAlLWVZPU1RFU1QwHhcNMTYwOTMwMDkxNTE1WhcNMTcwOTMwMDkxNTE1WjA+MQswCQYDVQQGEwJ1czELMAkGA1UECAwCQ0ExDjAMBgNVBAoMBUtZVk9TMRIwEAYDVQQDDAlLWVZPU1RFU1QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMgHww0rFliHv/8yAfeWTgz3OIDbYGj13AWKxhKHHElmzViYYj6AcGX7iF406RVAFYDPabquY0Axopvx6oSQzelt7eolRp8prCnJpQw0G99Bet6ke/qrRC4kj7FieEOF8PTVju9qfsIDJ12iLYOz6V2L/5KiXj1NKAeUipeU7pMjAgMBAAGjUDBOMB0GA1UdDgQWBBTV9oLgjYy9I7nnx3AgdER4HXJ7bjAfBgNVHSMEGDAWgBTV9oLgjYy9I7nnx3AgdER4HXJ7bjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAIqGGqznJxUtVvrVLigBOWRS8ZeIgIOoTPyS/8+N/eRNEsgg/fVinfHddRRl9aO3x1xxeYqydJX/LM7C8CcY/XFMBJK3BNBV9+RIxsFwSruGIt4Zlj7mjiNl0AAUCnI9yCDX3YMVQkgobEhgOFKVNvFJ8N2LJYinfV8V54nqh8/H\n-----END CERTIFICATE-----
onelogin.saml2.sp.privatekey = -----BEGIN PRIVATE KEY-----MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAMgHww0rFliHv/8yAfeWTgz3OIDbYGj13AWKxhKHHElmzViYYj6AcGX7iF406RVAFYDPabquY0Axopvx6oSQzelt7eolRp8prCnJpQw0G99Bet6ke/qrRC4kj7FieEOF8PTVju9qfsIDJ12iLYOz6V2L/5KiXj1NKAeUipeU7pMjAgMBAAECgYAB9aGBhYSIhXvqAhFm3YASPHG/QWbmYFIxc7SRBeXPCkTEs2ly90silJ43XglSffg39NAyVfb+7/2JzYrglZKOzGffqLYdy8xJ5NqDrXrGO8JiS0F83e4zE1DptHBy4p8G38lkJN6iz0Fcpq4quOy9ChwoTW1x6ncLQ4ejBOqSwQJBAOOOxPE+nVapxnjo1DBYerUVlOHYL2RCluwB1SB6Ry5m90rYqm0j6kHqSj7am75dwj9/ZmS15Em7nepcSO9/LpMCQQDhCDDuvNbLAB5GNG4/ZWa6Z6jOsIFXVCFiB3jT3/MkiJi7QRBACNPPhnvYOzgSeHsPaa2aROvKau0GVSqJr1MxAkB5q19v4du8d6AwC8VQaC6L3hMxwiZsxBHv9HbhG6Atlk5IzZoqKtbZEY1LGBXH7lerHdJArOR37AHeSiORMn5TAkBZtvpLM94ucI2hT6XkgHjEOC+Et7nZJyFoA7KYReCZ4BuEDBx+awaG5gbZ3kIsmvv02RztNC0NNjPpImsjGVMBAkA09SgzzFkzhyrQWaVaYTiQRklNx5kM2flroxfJaH8xN7R7K3NSbUOU/88VCOsrHqu+AR2WzB7cHIXpya4Zq/Ef-----END PRIVATE KEY-----
onelogin.saml2.sp.x509certNew=
# Identity Provider Data that we want connect with our SP
# Identifier of the IdP entity (must be a URI)
# idp 配置文件 地址
onelogin.saml2.idp.entityid = https://sts.windows.net/***/
# SSO endpoint info of the IdP. (Authentication Request protocol)
# URL Target of the IdP where the SP will send the Authentication Request Message
# idp 登录地址
onelogin.saml2.idp.single_sign_on_service.url = https://login.microsoftonline.com/***/saml2
# SLO endpoint info of the IdP.
# URL Location of the IdP where the SP will send the SLO Request
# idp 退出登录地址
onelogin.saml2.idp.single_logout_service.url = https://login.microsoftonline.com/***/saml2
# Public x509 certificate of the IdP
# IDP 证书在 idp页面下载
onelogin.saml2.idp.x509cert = -----BEGIN CERTIFICATE-----\nMIIC8DCCAdigAwIBAgIQOv78vXFsy7VDSxCzswFDfzANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQDEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yMjA5MDExMDEwMTZaFw0yNTA5MDExMDEwMTVaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQgU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAttc0hL2JyfT1EvktPQjtDHmlikmuMTtXCrsJtYV3fK2xTwrP/KEBjdzAYbQQ5AYCJm61+eIAjKZH+hV7waOw0hL6ohE9/MHuZeuuovjWREsb48/BV2IKFV1nJ3Nsu7anDdJnJbpQ8x9jnuHx9OLfnH+3vC6x6xNzBHuKOb1ndXmE/SJshWh4GDmAE5fct1lrcR+jxGacxiMaWBVxxv2rBqJrsUKJmwR23MvAvUKS2mm6J/dZeUvW5LEI2S/k8QtJjIfOaf+yzqy2oUbex7We4vODCuZCtKpBLufTe/m8xY5tKbyIkpTccy42M9oQP1tOImf7bSAbGNc3pv5HpHVsMQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBiYdc/oxKfa1zrWzrA3Uu4hM4HfV94x/NT95byf5NUSR3nBmf2/87cIRxtHD0+CA4XB6vSGEsFzIM0AfUDV385LQ0f8OnW774aNxvtKy9HMNXWhddaJ9N/xSOAtBPo/eh+sSbzAx0j0MOe4KnmBNBq9Gh8ZbCJzhHgzvuleqymlUVmtHEDB8LOJwHcx+tlnLVeqlNRCRCH6ihROBssnc/lyj9w1metbvhXGUJLUMbiGHLsnpEX0xSh2ZIf8QBNo54P9QXBj8Pwfh8I99j+gWsnDlndjRlAiPe8qvMP9LikdZQj4WpmNbGOC67lQN6d++S0RgvOXRGxHifHAP609eR7\n-----END CERTIFICATE-----
附件资料参考 "配置AAD指南.pdf" 文件
基本 SAML 配置
标识符(实体 ID) sp:my:system
回复 URL (断言使用者服务 URL) https://www.my.com
登录 URL https://testtt/user/sso/saml/SSOCallback
IDP 给我们的信息
登录 URL
Azure AD 标识符
注销 URL
应用联合元数据 URL
证书(base64)
证书(Raw)
联合元数据 XML
上文中所涉及的域名全部改为线上域名 必须https
/**
* @Description:
* @Author: sun yalong
* @Version: 1.0
*/
@RequestMapping("/user/sso/saml")
@RestController
public class SAMLSSOController {
private static final String PROTECTED_RESOURCES_URL = "PROTECTED_RESOURCES_URL";
private static final String CALLBACK_ADDRESS = "callbackAddress";
/**
* 返回saml 登录请求的页面
*
* @param request
* @return
*/
@GetMapping("getTestSsoPage")
public String loginPage(HttpServletRequest request){
return "点击生成 SAML 登录请求设置";
}
/**
* 生成请求并重定向到 sso 单点登录
*
* @param request
* @param response
* @throws Exception
*/
@RequestMapping({"generateSamlRequest"})
public void genSamlRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 将受保护的资源url 先保存起来等登录成功后直接重定向, 要求绝对路径
String protectedResourcesUrl = request.getParameter(CALLBACK_ADDRESS);
if (StringUtils.isNotBlank(protectedResourcesUrl)) {
request.getSession().setAttribute(PROTECTED_RESOURCES_URL,protectedResourcesUrl);
}
// 生成saml 请求 并完成重定向至 idp服务器
String url = getUrl(request, response);
response.setStatus(302);
response.setHeader("Location",url);
}
/**
* 登录成功回调函数
*
* @param request
* @param response
* @return
* @throws Exception
*/
@RequestMapping({"SSOCallback"})
public String loginSuccess(HttpServletRequest request, HttpServletResponse response) throws Exception {
Auth auth = new Auth("config/config.min.properties", request, response);
// Auth(Saml2Settings settings, HttpServletRequest request, HttpServletResponse response)
auth.processResponse();
String lastResponseXML = auth.getLastResponseXML();
System.out.println(lastResponseXML);
// 解析 idp返回的xml
// 判断系统中是否有这个用户 无泽创建
// 登录本系统 参考 com.everhomes.user.service.ShenZhenUniversityServiceImpl#authorize
// 重定向到用户之前访问的资源
String protectedResourcesUrl = (String) request.getSession().getAttribute(PROTECTED_RESOURCES_URL);
if(StringUtils.isNotBlank(protectedResourcesUrl)){
request.getSession().removeAttribute(PROTECTED_RESOURCES_URL);
response.setStatus(302);
response.setHeader("Location",protectedResourcesUrl);
return null;
}
return lastResponseXML;
}
public String getUrl(HttpServletRequest request, HttpServletResponse response)throws Exception{
Saml2Settings settings = new SettingsBuilder().fromFile("config/config.min.properties").build();
// Map params = new HashMap<>();
// Saml2Settings settings = new SettingsBuilder().fromValues(params).build();
Auth auth = new Auth(settings, request, response);
// 获取IDP和 重定向内容
String target = auth.login("", new AuthnRequestParams(false, false, false), true);
// 生成xml
// String authNRequestStr = getSAMLRequestFromURL(target);
return target;
}
/**
* 获取 saml 登录请求的 url
*
* @param url
* @return
* @throws URISyntaxException
* @throws UnsupportedEncodingException
*/
private String getSAMLRequestFromURL(String url) throws URISyntaxException, UnsupportedEncodingException {
String xml = "";
URI uri = new URI(url);
String query = uri.getQuery();
String[] pairs = query.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (pair.substring(0, idx).equals("SAMLRequest")) {
xml = Util.base64decodedInflated(pair.substring(idx + 1));
}
}
return xml;
}
}
onelogin: https://github.com/onelogin/java-saml/tags
https://help.aliyun.com/document_detail/160431.html?scm=20140722.184.2.173#h2-78q-k9u-yay