From 52d4c1c586b54c41b17be6ef137d67270ec0fd78 Mon Sep 17 00:00:00 2001 From: codingXiaxw Date: Thu, 15 Aug 2024 16:58:03 +0800 Subject: [PATCH] =?UTF-8?q?JSY-42593=20add=EF=BC=9A=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E4=B8=80=E4=B8=AAsp=E5=92=8Cidp=E8=BF=9B=E8=A1=8Csso=E7=9A=84d?= =?UTF-8?q?emo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 8 + build.gradle | 30 +++ gradle/wrapper/gradle-wrapper.properties | 5 + idp_metadata.xml | 39 ++++ rp-certificate.crt | 21 +++ rp-private.key | 27 +++ sp-metadata.xml | 39 ++++ .../com/fanruan/sso/bean/SSOSamlBean.java | 142 +++++++++++++++ .../fanruan/sso/bean/SSOSamlResultBean.java | 75 ++++++++ .../fanruan/sso/controller/SSOController.java | 59 ++++++ .../com/fanruan/sso/filter/SAMLFilter.java | 20 ++ .../com/fanruan/sso/service/SSOService.java | 22 +++ .../sso/service/impl/SSOServiceImpl.java | 172 ++++++++++++++++++ .../com/fanruan/sso/utils/OpenSAMLUtils.java | 33 ++++ .../fanruan/sso/utils/XMLAnalysisUtils.java | 104 +++++++++++ 15 files changed, 796 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 idp_metadata.xml create mode 100644 rp-certificate.crt create mode 100644 rp-private.key create mode 100644 sp-metadata.xml create mode 100644 src/main/java/com/fanruan/sso/bean/SSOSamlBean.java create mode 100644 src/main/java/com/fanruan/sso/bean/SSOSamlResultBean.java create mode 100644 src/main/java/com/fanruan/sso/controller/SSOController.java create mode 100644 src/main/java/com/fanruan/sso/filter/SAMLFilter.java create mode 100644 src/main/java/com/fanruan/sso/service/SSOService.java create mode 100644 src/main/java/com/fanruan/sso/service/impl/SSOServiceImpl.java create mode 100644 src/main/java/com/fanruan/sso/utils/OpenSAMLUtils.java create mode 100644 src/main/java/com/fanruan/sso/utils/XMLAnalysisUtils.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..37560ad --- /dev/null +++ b/build.gradle @@ -0,0 +1,30 @@ +buildscript { + ext { + springBootVersion = '2.5.14' + } +} + +plugins { + id 'java' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + + implementation 'com.onelogin:java-saml:2.9.0' + implementation(platform("org.springframework.boot:spring-boot-dependencies:" + springBootVersion)) + implementation 'org.springframework.boot:spring-boot-starter-web' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.7.2' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/idp_metadata.xml b/idp_metadata.xml new file mode 100644 index 0000000..222a260 --- /dev/null +++ b/idp_metadata.xml @@ -0,0 +1,39 @@ + + + + + + + MIID3DCCAsSgAwIBAgIUB2/7G0jrXZEpZqCOClAOw9tM0gswDQYJKoZIhvcNAQEF +BQAwRTEQMA4GA1UECgwHRmFucnVhbjEVMBMGA1UECwwMT25lTG9naW4gSWRQMRow +GAYDVQQDDBFPbmVMb2dpbiBBY2NvdW50IDAeFw0yNDA4MDEwMzM3MzZaFw0yOTA4 +MDEwMzM3MzZaMEUxEDAOBgNVBAoMB0ZhbnJ1YW4xFTATBgNVBAsMDE9uZUxvZ2lu +IElkUDEaMBgGA1UEAwwRT25lTG9naW4gQWNjb3VudCAwggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCWV9aev80yVah8cbhq3JYSn30GiJQFXPXX09zPzztO +FcvnKsskx5oRj0DXVuhGwPEaQ4b2wMahMNHoGVVuMFAs1xPa55lxcY4XirEhY/nW +i9CYb0SiLZnb+W382byr+nqYbYCvPASu5ifRtM1adwngfcd7w1JbkylzlzuXStFl +qpNGKVWPYVwb3I3mCmeppThYWakrvXQcy1VFHJ2LHehoVCQsaf2UxgZVazwV22wG +UF7e3grTc2+dsTTIUuF04jLir34N++PE5RufI1irADj4WhdaFI7st1YaWCBZSe5Z +UR298IlatrKQ088mfWQc4oHHznRO1ffoHUmmL31uh5O9AgMBAAGjgcMwgcAwDAYD +VR0TAQH/BAIwADAdBgNVHQ4EFgQU0Xtr2IIM7Kdw2priuuGKDM9G2WswgYAGA1Ud +IwR5MHeAFNF7a9iCDOyncNqa4rrhigzPRtlroUmkRzBFMRAwDgYDVQQKDAdGYW5y +dWFuMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxGjAYBgNVBAMMEU9uZUxvZ2luIEFj +Y291bnQgghQHb/sbSOtdkSlmoI4KUA7D20zSCzAOBgNVHQ8BAf8EBAMCB4AwDQYJ +KoZIhvcNAQEFBQADggEBAIJ6vM7/WZ4yELkC5ql3sQCE+NilPSXgksk44ZF+MqNA +Cc4KjYbU9eFCKhrzOxXIX4+rw2A/M1EOUpj1T88wKnQtzzwciglao3uvPLP9mUgT +KiBqHaHV42piwy9bdwf/yckgZmo0DnwOSO9mhHcrKBVdFMIfh6iojaC34Diex72O +bjY3NDw/Lky/+5KCvCX8L7rQJzdB6uksE3ei4gV6wSBxpP/4qOao8BJh9gCUAAUg +gSEFbbmB2CiL4p134uUpDfLt6F7aaOQ+K9CLCrVhHFc8+KT/ubotOe6L6REbfowC +bsCvmdbnq3JRPuI/jbT8EYk4PRrWnmRQ6G0zdxkFk7U= + + + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + + + + diff --git a/rp-certificate.crt b/rp-certificate.crt new file mode 100644 index 0000000..6280f25 --- /dev/null +++ b/rp-certificate.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDcDCCAlgCCQDVWvep1uXiejANBgkqhkiG9w0BAQsFADB6MUUwQwYDVQQIDDxv +cGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkgLW91dCBycC1jZXJ0 +aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoMB2ZhbnJ1YW4xEDAO +BgNVBAsMB2ZhbnJ1YW4wHhcNMjQwODA4MTEyNjIzWhcNMjUwODA4MTEyNjIzWjB6 +MUUwQwYDVQQIDDxvcGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkg +LW91dCBycC1jZXJ0aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoM +B2ZhbnJ1YW4xEDAOBgNVBAsMB2ZhbnJ1YW4wggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDS0iT7OyG3Y1+3mLfXeoAFZLgiwUel143W5gdlbXSHbuJ0xNrE +vaGRaUj0vZCdVPL6bAtQiJGQGAEgYpp9ZeaPexCrOE92LhHGZADbcVT2B954ni+f +LKG7J9FnZcRDOBEqYhyVqTvG6uN0TIIkRwKFTjsSPdRQyqV6uNW4y+r8RrjIOAMS +K5CWKZbqiiAcb5EqmeYQfWNLjVCn+E199R1LIlEnrMz4+kHEIOTTzNQht30utx5f +aiBlLQ9XVyVPWHV0iNzGs2qxHUA07bZyEuiG2t+79OAQWKzjqTK5GTvAWEbZ6IaO +FVi970pG7E14T+rivZGqJrLWvv8MvW4BE0v7AgMBAAEwDQYJKoZIhvcNAQELBQAD +ggEBAFAZg0TGNSpnIKR1MW4Y0K+2LyslBlMTirrQY21MobS/S+WM8QE+qZvyPUpp +ilaXnquCptM1MtX/9kPRlRA5v8bEWNZQA6bs3RkH5FC5j+TKcrPti7yAaTcMxw7h +S/e9e3HZ9ZeU2b1M87Gs1uGTWJ4LyH5vKfHRpNlhOj+rv4k8UeCce/ER0z4OBmwE +0OtY7xWEP5arF6iVyntpYPbxujuxui1orfsUl5DEOUvKA3VHG5fsgUkhP/KoxFTS +6ETrC5qy7HCk9J88HX9ovxY/bj/SWwAGx3wNaG+NZz2pQyD6NaBOSRvBC2ZwFlWr +TgtYc4URcnVH2DOkamR9hFecVA4= +-----END CERTIFICATE----- diff --git a/rp-private.key b/rp-private.key new file mode 100644 index 0000000..d1ed488 --- /dev/null +++ b/rp-private.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0tIk+zsht2Nft5i313qABWS4IsFHpdeN1uYHZW10h27idMTa +xL2hkWlI9L2QnVTy+mwLUIiRkBgBIGKafWXmj3sQqzhPdi4RxmQA23FU9gfeeJ4v +nyyhuyfRZ2XEQzgRKmIclak7xurjdEyCJEcChU47Ej3UUMqlerjVuMvq/Ea4yDgD +EiuQlimW6oogHG+RKpnmEH1jS41Qp/hNffUdSyJRJ6zM+PpBxCDk08zUIbd9Lrce +X2ogZS0PV1clT1h1dIjcxrNqsR1ANO22chLohtrfu/TgEFis46kyuRk7wFhG2eiG +jhVYve9KRuxNeE/q4r2Rqiay1r7/DL1uARNL+wIDAQABAoIBAFq4h6V8+rNaFhSB +qYsWcgTgJMT/+38IVUdG0aP6CA1H0DeDhsjm+aIDdpuq/5JUvgK5f9z2B/3D9qgj +Bmwz75WK1c94eelXRdrjqeLE0FTXagrpt4B9ylMpfVlLV7u9YtWkRry4iLq+1YZ0 +bgSCcjk/QvhElxr1OWSoKYBOcB6C39o96Obzek6cgdUNRZpP1pnyNpYMAnrHUOnR +a+iFMFt3q7Pn815EG4Rg4Emgmd9v9c/xAonrqoXddZSsCfUTo1duqfoxJnU21O3q +spcofbX25sULYL1gpM4p1V92hHaSNZHdZhxMlUuSCwBk1PJEGLrFEDiVZXXDcH8m +B8ghZrECgYEA/H/gz0rkNW6zk1nMKT/ozJUb0XqTVZBrJuq7L0vmOUuNUMalbkzA +GlDUM7r852yeiwx4S+XgTRvWRsqau/cuX1B+Z4qaLjWi7RUrQqI9iZS3HFbdX21R +pXz03iCPONL+X4Mzh6Z7NIKf8ptn4syMXJan1M5XrsM8bPAS1oRyuf8CgYEA1b5Z +Pkzk6N6GqGVzfNQ8G3l4620rbJBvc1itNTK4TX1W9MOKIHj/zoYopgk+2snQQtUd +2Urg+3LzJ7vegBmt8Mlpjs6Y27Iz4gyurWHAYEkBK/weqExOz6QB1lYFaO3DPHcx ++SJc2tSmoBEvcLmEBfvJ0uIvN637BmyPg7umVgUCgYEAgwV8SzRqXMuXxTNIfHMc +QuRwre9z+mdZIrWU8gLpcPuiVbLubuDGoiElK76wswmq7y5GUePz0y9Jriw9xKGL +34uuO94xCR9t7qYYb5guZHDV34+3iWf5gOzpR0YP64WY10kGeTJLJkFN7B719jr7 +7qOCbSuxVg8bENA2hjfuLFMCgYEAp60PrYP8/4Gx+WC83GxSSutcJLQboKseA0rJ +djY3xvJQyOqs7RR++LDeKoKOQGyZaBRvugq3vApNHhqPTcbXYVFf8Zu45oBBm09/ +qJxKoj4jITJDiptyKAntNwt8avg6dLC9D0gZt8GihWd14+Rk4ZzIkxrFF9TwW/XG +D/2hW1ECgYBQtU5PYiaMb7zTVQVVS7aVbym6QInvyZ03O64Y3y+cLf+PFG3F6q/4 +SRtsmILe+sBY8MdQj0gvGBguMRTgprahS38mbQLdyGCmJIsKdqk9IHv4xU9sHU+n +xoznawU1UDlxxWrfBpaYVb4CkxaDjL5FvWHv74ZgZ1+Zh3e0gCuIzQ== +-----END RSA PRIVATE KEY----- diff --git a/sp-metadata.xml b/sp-metadata.xml new file mode 100644 index 0000000..421a167 --- /dev/null +++ b/sp-metadata.xml @@ -0,0 +1,39 @@ +MIIDcDCCAlgCCQDVWvep1uXiejANBgkqhkiG9w0BAQsFADB6MUUwQwYDVQQIDDxv +cGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkgLW91dCBycC1jZXJ0 +aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoMB2ZhbnJ1YW4xEDAO +BgNVBAsMB2ZhbnJ1YW4wHhcNMjQwODA4MTEyNjIzWhcNMjUwODA4MTEyNjIzWjB6 +MUUwQwYDVQQIDDxvcGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkg +LW91dCBycC1jZXJ0aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoM +B2ZhbnJ1YW4xEDAOBgNVBAsMB2ZhbnJ1YW4wggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDS0iT7OyG3Y1+3mLfXeoAFZLgiwUel143W5gdlbXSHbuJ0xNrE +vaGRaUj0vZCdVPL6bAtQiJGQGAEgYpp9ZeaPexCrOE92LhHGZADbcVT2B954ni+f +LKG7J9FnZcRDOBEqYhyVqTvG6uN0TIIkRwKFTjsSPdRQyqV6uNW4y+r8RrjIOAMS +K5CWKZbqiiAcb5EqmeYQfWNLjVCn+E199R1LIlEnrMz4+kHEIOTTzNQht30utx5f +aiBlLQ9XVyVPWHV0iNzGs2qxHUA07bZyEuiG2t+79OAQWKzjqTK5GTvAWEbZ6IaO +FVi970pG7E14T+rivZGqJrLWvv8MvW4BE0v7AgMBAAEwDQYJKoZIhvcNAQELBQAD +ggEBAFAZg0TGNSpnIKR1MW4Y0K+2LyslBlMTirrQY21MobS/S+WM8QE+qZvyPUpp +ilaXnquCptM1MtX/9kPRlRA5v8bEWNZQA6bs3RkH5FC5j+TKcrPti7yAaTcMxw7h +S/e9e3HZ9ZeU2b1M87Gs1uGTWJ4LyH5vKfHRpNlhOj+rv4k8UeCce/ER0z4OBmwE +0OtY7xWEP5arF6iVyntpYPbxujuxui1orfsUl5DEOUvKA3VHG5fsgUkhP/KoxFTS +6ETrC5qy7HCk9J88HX9ovxY/bj/SWwAGx3wNaG+NZz2pQyD6NaBOSRvBC2ZwFlWr +TgtYc4URcnVH2DOkamR9hFecVA4= +MIIDcDCCAlgCCQDVWvep1uXiejANBgkqhkiG9w0BAQsFADB6MUUwQwYDVQQIDDxv +cGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkgLW91dCBycC1jZXJ0 +aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoMB2ZhbnJ1YW4xEDAO +BgNVBAsMB2ZhbnJ1YW4wHhcNMjQwODA4MTEyNjIzWhcNMjUwODA4MTEyNjIzWjB6 +MUUwQwYDVQQIDDxvcGVuc3NsIHJlcSAtbmV3IC1rZXkgcnAtcHJpdmF0ZS5rZXkg +LW91dCBycC1jZXJ0aWZpY2F0ZS5jc3IxDTALBgNVBAcMBHd1eGkxEDAOBgNVBAoM +B2ZhbnJ1YW4xEDAOBgNVBAsMB2ZhbnJ1YW4wggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQDS0iT7OyG3Y1+3mLfXeoAFZLgiwUel143W5gdlbXSHbuJ0xNrE +vaGRaUj0vZCdVPL6bAtQiJGQGAEgYpp9ZeaPexCrOE92LhHGZADbcVT2B954ni+f +LKG7J9FnZcRDOBEqYhyVqTvG6uN0TIIkRwKFTjsSPdRQyqV6uNW4y+r8RrjIOAMS +K5CWKZbqiiAcb5EqmeYQfWNLjVCn+E199R1LIlEnrMz4+kHEIOTTzNQht30utx5f +aiBlLQ9XVyVPWHV0iNzGs2qxHUA07bZyEuiG2t+79OAQWKzjqTK5GTvAWEbZ6IaO +FVi970pG7E14T+rivZGqJrLWvv8MvW4BE0v7AgMBAAEwDQYJKoZIhvcNAQELBQAD +ggEBAFAZg0TGNSpnIKR1MW4Y0K+2LyslBlMTirrQY21MobS/S+WM8QE+qZvyPUpp +ilaXnquCptM1MtX/9kPRlRA5v8bEWNZQA6bs3RkH5FC5j+TKcrPti7yAaTcMxw7h +S/e9e3HZ9ZeU2b1M87Gs1uGTWJ4LyH5vKfHRpNlhOj+rv4k8UeCce/ER0z4OBmwE +0OtY7xWEP5arF6iVyntpYPbxujuxui1orfsUl5DEOUvKA3VHG5fsgUkhP/KoxFTS +6ETrC5qy7HCk9J88HX9ovxY/bj/SWwAGx3wNaG+NZz2pQyD6NaBOSRvBC2ZwFlWr +TgtYc4URcnVH2DOkamR9hFecVA4= +urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified \ No newline at end of file diff --git a/src/main/java/com/fanruan/sso/bean/SSOSamlBean.java b/src/main/java/com/fanruan/sso/bean/SSOSamlBean.java new file mode 100644 index 0000000..7164965 --- /dev/null +++ b/src/main/java/com/fanruan/sso/bean/SSOSamlBean.java @@ -0,0 +1,142 @@ +package com.fanruan.sso.bean; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SSOSamlBean { + + /** + * idp endpoint + */ + private String ssoEndpoint; + + /** + * idp算法 + */ + private String signatureAlg; + + /** + * idp公钥 + */ + private String signatureCrt; + + /** + * idp issuer + */ + private String issuer; + + /** + * idp slo + */ + private String sloEndpoint; + + /** + * 是否开启单点登录 + */ + private boolean turnOn; + + /** + * sp 证书 + */ + private String spCrt; + + /** + * sp 私钥 + */ + private String spPrivateKey; + + /** + * 唯一标识, + * 如果是odm的sso,则为odm的id + * 如果是企业设置的sso,则为九数云的企业id + */ + private String registrationId; + + + public SSOSamlBean(String ssoEndpoint, String signatureAlg, String signatureCrt, String issuer, String sloEndpoint, + String registrationId, boolean turnOn, String spCrt, String spPrivateKey) { + this.ssoEndpoint = ssoEndpoint; + this.signatureAlg = signatureAlg; + this.signatureCrt = signatureCrt; + this.issuer = issuer; + this.sloEndpoint = sloEndpoint; + this.registrationId = registrationId; + this.turnOn = turnOn; + this.spCrt = spCrt; + this.spPrivateKey = spPrivateKey; + } + + public String getSpCrt() { + return spCrt; + } + + public void setSpCrt(String spCrt) { + this.spCrt = spCrt; + } + + public String getSpPrivateKey() { + return spPrivateKey; + } + + public void setSpPrivateKey(String spPrivateKey) { + this.spPrivateKey = spPrivateKey; + } + + public boolean isTurnOn() { + return turnOn; + } + + public void setTurnOn(boolean turnOn) { + this.turnOn = turnOn; + } + + public String getRegistrationId() { + return registrationId; + } + + public void setRegistrationId(String registrationId) { + this.registrationId = registrationId; + } + + public String getSsoEndpoint() { + return ssoEndpoint; + } + + public void setSsoEndpoint(String ssoEndpoint) { + this.ssoEndpoint = ssoEndpoint; + } + + public String getSignatureAlg() { + return signatureAlg; + } + + public void setSignatureAlg(String signatureAlg) { + this.signatureAlg = signatureAlg; + } + + public String getSignatureCrt() { + return signatureCrt; + } + + public void setSignatureCrt(String signatureCrt) { + this.signatureCrt = signatureCrt; + } + + public String getIssuer() { + return issuer; + } + + public void setIssuer(String issuer) { + this.issuer = issuer; + } + + public String getSloEndpoint() { + return sloEndpoint; + } + + public void setSloEndpoint(String sloEndpoint) { + this.sloEndpoint = sloEndpoint; + } +} diff --git a/src/main/java/com/fanruan/sso/bean/SSOSamlResultBean.java b/src/main/java/com/fanruan/sso/bean/SSOSamlResultBean.java new file mode 100644 index 0000000..74ac4c4 --- /dev/null +++ b/src/main/java/com/fanruan/sso/bean/SSOSamlResultBean.java @@ -0,0 +1,75 @@ +package com.fanruan.sso.bean; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SSOSamlResultBean { + + /** + * sso endpoint + */ + private String ssoEndpoint; + + /** + * sp metadata + */ + private String spMetadata; + + /** + * sp sso地址 + */ + private String spIss; + + /** + * sp 断言消费地址 + */ + private String spAcs; + + /** + * sp 登录地址 + */ + private String spSlo; + + public String getSsoEndpoint() { + return ssoEndpoint; + } + + public void setSsoEndpoint(String ssoEndpoint) { + this.ssoEndpoint = ssoEndpoint; + } + + public String getSpMetadata() { + return spMetadata; + } + + public void setSpMetadata(String spMetadata) { + this.spMetadata = spMetadata; + } + + public String getSpIss() { + return spIss; + } + + public void setSpIss(String spIss) { + this.spIss = spIss; + } + + public String getSpAcs() { + return spAcs; + } + + public void setSpAcs(String spAcs) { + this.spAcs = spAcs; + } + + public String getSpSlo() { + return spSlo; + } + + public void setSpSlo(String spSlo) { + this.spSlo = spSlo; + } +} diff --git a/src/main/java/com/fanruan/sso/controller/SSOController.java b/src/main/java/com/fanruan/sso/controller/SSOController.java new file mode 100644 index 0000000..fe003f0 --- /dev/null +++ b/src/main/java/com/fanruan/sso/controller/SSOController.java @@ -0,0 +1,59 @@ +package com.fanruan.sso.controller; + +import com.fanruan.hihidata.action.aspect.PortalRoleCheck; +import com.fanruan.hihidata.action.aspect.RateLimit; +import com.fanruan.hihidata.action.aspect.Scope; +import com.fanruan.hihidata.action.reponse.HiRespond; +import com.fanruan.hihidata.config.role.CorpVersionRoleType; +import com.fanruan.hihidata.service.sso.SSOService; +import com.fanruan.hihidata.service.utils.OpenSAMLUtils; +import com.fr.decision.webservice.annotation.LoginStatusChecker; +import com.fr.third.org.apache.commons.lang3.StringUtils; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RestController +public class SSOController { + + @Autowired + private SSOService ssoService; + + @ResponseBody + @GetMapping(value = "/sso/saml/{registrationId}/sp/metadata", produces = MediaType.APPLICATION_XML_VALUE) + public void getSpMetadata(@PathVariable("registrationId") String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + String SpMetadata = ssoService.generateSpMetadata(registrationId); + OpenSAMLUtils.downloadByStringContent(req, res, "sp-metadata.xml", SpMetadata); + } + + @ResponseBody + @GetMapping(value = "/sso/saml/{registrationId}/iss", produces = MediaType.TEXT_HTML_VALUE) + public String iss(@PathVariable("registrationId") String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + ssoService.iss(registrationId, req, res); + return StringUtils.EMPTY; + } + + @ResponseBody + @PostMapping(value = "/sso/saml/{registrationId}/acs") + public String acs(@PathVariable("registrationId") String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + return ssoService.acs(registrationId, req, res); + } + + @ResponseBody + @GetMapping(value = "/sso/saml/{registrationId}/slo") + public String slo(@PathVariable("registrationId") String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + ssoService.slo(registrationId, req, res); + return StringUtils.EMPTY; + } + +} diff --git a/src/main/java/com/fanruan/sso/filter/SAMLFilter.java b/src/main/java/com/fanruan/sso/filter/SAMLFilter.java new file mode 100644 index 0000000..cd7a2fe --- /dev/null +++ b/src/main/java/com/fanruan/sso/filter/SAMLFilter.java @@ -0,0 +1,20 @@ +package com.fanruan.sso.filter; + + + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class SAMLFilter { + private final SamlIgnores ignores = new SamlIgnores(); + private boolean initialized = false; + @Override + public void doFilter(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain) throws IOException, ServletException { + // do something + filterChain.doFilter(req, res); + } + +} \ No newline at end of file diff --git a/src/main/java/com/fanruan/sso/service/SSOService.java b/src/main/java/com/fanruan/sso/service/SSOService.java new file mode 100644 index 0000000..576294f --- /dev/null +++ b/src/main/java/com/fanruan/sso/service/SSOService.java @@ -0,0 +1,22 @@ +package com.fanruan.sso.service; + +import com.fanruan.hihidata.bean.sso.SSOSamlBean; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface SSOService { + + void iss(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception; + + String acs(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception; + + void slo(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception; + + String logout(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception; + + String generateSpMetadata(String registrationId) throws Exception; + + SSOSamlBean getSamlSpConfig(String registrationId) throws Exception; + +} diff --git a/src/main/java/com/fanruan/sso/service/impl/SSOServiceImpl.java b/src/main/java/com/fanruan/sso/service/impl/SSOServiceImpl.java new file mode 100644 index 0000000..751b8f6 --- /dev/null +++ b/src/main/java/com/fanruan/sso/service/impl/SSOServiceImpl.java @@ -0,0 +1,172 @@ +package com.fanruan.sso.service.impl; + +import com.fanruan.sso.bean.SSOSamlBean; +import com.fanruan.sso.service.SSOService; +import com.fanruan.sso.utils.XMLAnalysisUtils; +import com.fasterxml.jackson.databind.JsonNode; +import com.onelogin.saml2.Auth; +import com.onelogin.saml2.authn.AuthnRequestParams; +import com.onelogin.saml2.logout.LogoutRequestParams; +import com.onelogin.saml2.settings.Saml2Settings; +import com.onelogin.saml2.settings.SettingsBuilder; +import com.onelogin.saml2.util.Util; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import org.springframework.web.util.WebUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +@Service +public class SSOServiceImpl implements SSOService { + + private static final String UPPERCASE_RULER = "uppercase"; + private static final String LOWERCASE_RULER = "lowercase"; + private static final String RELAY_STATE = "RelayState"; + + @Override + public String generateSpMetadata(String registrationId) throws Exception { + Saml2Settings settings = getConfig(getSamlSpConfig(registrationId)); + settings.setSPValidationOnly(true); + String metadata = settings.getSPMetadata(); + List errors = Saml2Settings.validateMetadata(metadata); + if (errors.isEmpty()) { + return metadata; + } else { + StringBuilder errorsBuilder = new StringBuilder(); + for (String error : errors) { + errorsBuilder.append(error); + } + return errorsBuilder.toString(); + } + } + + @Override + public void slo(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + Saml2Settings settings = getConfig(getSamlSpConfig(registrationId)); + Auth auth = new Auth(settings, req, res); + auth.processSLO(); + LoginService.getInstance().crossDomainLogout(req, res, "callback").createCrossDomainResponse(); + auth.logout(); + } + + @Override + public String logout(String registrationId, HttpServletRequest req, HttpServletResponse res) throws Exception { + Saml2Settings settings = getConfig(getSamlSpConfig(registrationId)); + Auth auth = new Auth(settings, req, res); + return auth.logout(StringUtils.EMPTY, new LogoutRequestParams(), true); + } + + @Override + public String acs(String registrationId, HttpServletRequest request, HttpServletResponse response) throws Exception { + Saml2Settings settings = getConfig(getSamlSpConfig(registrationId)); + Auth auth = new Auth(settings, request, response); + auth.processResponse(); + String lastResponseXML = auth.getLastResponseXML(); + FineLoggerFactory.getLogger().info("registrationId is {}, The xml is: {}", registrationId, lastResponseXML); + + // 先校验下responese的合法性 + // String username = XMLAnalysisUtils.getUserName(lastResponseXML); + String memberName = XMLAnalysisUtils.getMemberName(lastResponseXML); + String corpId = XMLAnalysisUtils.getCorpId(lastResponseXML); + String relayState = WebUtils.getHTTPRequestParameter(request, RELAY_STATE); + if (Objects.isNull(user)) { + // 走注册逻辑 + String newUserId = RandomUtils.generateUUIDString(); + User newUser = corpCell.initDecisionMember(newUserId, mobile); + return ssoLogin(corpService, request, response, newUser, corpId, relayState, registrationId); + } + return ssoLogin(corpService, request, response, user, corpId, relayState, registrationId); + } + + @NotNull + private String ssoLogin(CorpService corpService, HttpServletRequest request, HttpServletResponse response, User newUser, String corpId, + String relayState, String registrationId) throws Exception { + corpService.reLogin(request, response, newUser, corpId); + String serviceUrl = TemplateUtils.renderParameter4Tpl(HiCommonConstants.MAIN_PAGE_HOME, HiServletURLProvider.getServletUrlMap()); + String loginUrl = TemplateUtils.renderParameter4Tpl(HiCommonConstants.LOCAL_LOGIN, HiServletURLProvider.getServletUrlMap()); + request.getSession().setAttribute(IdApiConstants.SSO_LOGIN, registrationId); + if (StringUtils.isNotEmpty(relayState)) { + response.sendRedirect(serviceUrl); + return StringUtils.EMPTY; + } + response.sendRedirect(loginUrl); + return StringUtils.EMPTY; + } + + public Saml2Settings getConfig(SSOSamlBean ssoSamlBean) throws Exception { + Map samlData = new HashMap<>(); + + String prefix = TemplateUtils.renderParameter4Tpl(HiCommonConstants.MAIN_PAGE_URL, HiServletURLProvider.getServletUrlMap()); + String spAcsUrl = OemContext.getFullDomain() + prefix + "/sso/saml/" + ssoSamlBean.getRegistrationId() + "/acs"; + String spEntityId = OemContext.getFullDomain() + prefix + "/sso/saml/" + ssoSamlBean.getRegistrationId() + "/iss"; + samlData.put("onelogin.saml2.sp.entityid", spEntityId); + samlData.put("onelogin.saml2.sp.assertion_consumer_service.url", spAcsUrl); + + //IDP配置 + String idpEntityId = ssoSamlBean.getIssuer(); + String idpSignInUrl = ssoSamlBean.getSsoEndpoint(); + String idpPublicKey = ssoSamlBean.getSignatureCrt(); + String idpLogOutUrl = ssoSamlBean.getSloEndpoint(); + + samlData.put("onelogin.saml2.idp.single_sign_on_service.url", idpSignInUrl); + samlData.put("onelogin.saml2.idp.entityid", idpEntityId); + X509Certificate idpX509CertInstance = Util.loadCert((idpPublicKey).trim()); + samlData.put("onelogin.saml2.idp.x509cert", idpX509CertInstance); + + String cert = ssoSamlBean.getSpCrt(); + X509Certificate spX509CertInstance = Util.loadCert(cert.trim()); + samlData.put("onelogin.saml2.sp.x509cert", spX509CertInstance); + + String privateKey = ssoSamlBean.getSpPrivateKey(); + PrivateKey spPrivateKey = Util.loadPrivateKey(privateKey.trim()); + samlData.put("onelogin.saml2.sp.privatekey", spPrivateKey); + + samlData.put("onelogin.saml2.security.authnrequest_signed", true); + samlData.put("onelogin.saml2.security.logoutrequest_signed", true); + //签名断言和加密断言的功能都默认开启了,增加安全性 + samlData.put("onelogin.saml2.security.want_assertions_signed", true); + samlData.put("onelogin.saml2.security.want_assertions_encrypted", true); + samlData.put("onelogin.saml2.security.want_nameid_encrypted", true); + samlData.put("onelogin.saml2.idp.single_logout_service.url", idpLogOutUrl); + + SettingsBuilder builder = new SettingsBuilder(); + Saml2Settings settings = builder.fromValues(samlData).build(); + + return settings; + } + + @Override + public SSOSamlBean getSamlSpConfig(String registrationId) throws Exception { + JsonNode jsonNode = oemService.find(registrationId); + String spCrt = OemContext.getValue(jsonNode, OemContext.CRT_TEXT); + String spKey = OemContext.getValue(jsonNode, OemContext.KEY_TEXT); + + JsonNode ssoConfig = jsonNode.get(OemContext.SSO); + String ssoEndpoint = OemContext.getValue(ssoConfig, OemContext.SSO_ENDPOINT); + String signatureAlg = OemContext.getValue(ssoConfig, OemContext.SSO_SIG_ALG); + String signatureCrt = OemContext.getValue(ssoConfig, OemContext.SSO_SIG_CRT); + String issuer = OemContext.getValue(ssoConfig, OemContext.SSO_ISSUER); + String sloEndpoint = OemContext.getValue(ssoConfig, OemContext.SLO_ENDPOINT); + boolean turnOn = ssoConfig.get("turnOn").asBoolean(); + return new SSOSamlBean(ssoEndpoint, signatureAlg, signatureCrt, issuer, sloEndpoint, registrationId, turnOn, spCrt, spKey); + } + + @Override + public void iss(String registrationId, HttpServletRequest request, HttpServletResponse response) throws Exception { + Saml2Settings settings = getConfig(getSamlSpConfig(registrationId)); + Auth auth = new Auth(settings, request, response); + // 获取IDP和 重定向内容 + String url = auth.login(WebUtils.getOriginalURL(request), new AuthnRequestParams(false, false, false), true); + response.setStatus(302); + response.setHeader("Location", url); + } +} diff --git a/src/main/java/com/fanruan/sso/utils/OpenSAMLUtils.java b/src/main/java/com/fanruan/sso/utils/OpenSAMLUtils.java new file mode 100644 index 0000000..dc7325d --- /dev/null +++ b/src/main/java/com/fanruan/sso/utils/OpenSAMLUtils.java @@ -0,0 +1,33 @@ +package com.fanruan.sso.utils; + + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class OpenSAMLUtils { + + public static void downloadByStringContent(HttpServletRequest request, + HttpServletResponse response, + String fileName, String content) + throws IOException { + //设置向浏览器端传送的文件格式 + response.setContentType("application/octet-stream;charset=utf-8"); + response.setCharacterEncoding("utf-8"); + Browser browser = Browser.resolve(request); + fileName = browser.getEncodedFileName4Download(fileName); + response.setHeader("Content-disposition", "attachment; filename=" + fileName); + try (OutputStream out = response.getOutputStream(); BufferedInputStream inp = new BufferedInputStream(new ByteArrayInputStream(content.getBytes("utf-8")));) { + int len = 0; + byte[] buf = new byte[1024]; + while ((len = inp.read(buf)) > 0) { + out.write(buf, 0, len); + } + } catch (Exception e) { + // 输出log + } + } +} diff --git a/src/main/java/com/fanruan/sso/utils/XMLAnalysisUtils.java b/src/main/java/com/fanruan/sso/utils/XMLAnalysisUtils.java new file mode 100644 index 0000000..6edbc6b --- /dev/null +++ b/src/main/java/com/fanruan/sso/utils/XMLAnalysisUtils.java @@ -0,0 +1,104 @@ +package com.fanruan.sso.utils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Objects; + + +public class XMLAnalysisUtils { + + private static final String ASSERTION_NODE = "Assertion"; + private static final String SUBJECT_NODE = "Subject"; + private static final String NAME_ID_NODE = "NameID"; + private static final String MEMBER_NAME_NODE = "MemberName"; + private static final String MOBILE_NODE = "Mobile"; + private static final String CORP_ID_NODE = "CorpId"; + private static final String ATTRIBUTE_STATEMENT_NODE = "AttributeStatement"; + private static final String ATTRIBUTE_VALUE_NODE = "AttributeValue"; + private static final String MATCHING_PARAMETER_NAME = "Name"; + private static final String AUTHN_STATEMENT_NODE = "AuthnStatement"; + private static final String ATTRIBUTE_SESSION_INDEX = "SessionIndex"; + public static final String CACHE_SUFFIX = "_@_"; + + public static String getMemberName(String xml) throws Exception { + if (StringUtils.isNotEmpty(xml)) { + Document doc = DocumentHelper.parseText(xml); + Element rootElement = doc.getRootElement(); + Element assertionElement = rootElement.element(ASSERTION_NODE); + Element statementElement = assertionElement.element(ATTRIBUTE_STATEMENT_NODE); + if (Objects.nonNull(statementElement)) { + List list = statementElement.elements(); + for (Object ele : list) { + Element element = (Element) ele; + if (StringUtils.equals(MEMBER_NAME_NODE.toLowerCase(), element.attributeValue(MATCHING_PARAMETER_NAME).toLowerCase())) { + String memberName = element.element(ATTRIBUTE_VALUE_NODE).getText(); + FineLoggerFactory.getLogger().info("The parsed member name is[{}]", memberName); + return memberName; + } + } + } + } + return StringUtils.EMPTY; + } + + public static String getMobile(String xml) throws Exception { + if (StringUtils.isNotEmpty(xml)) { + Document doc = DocumentHelper.parseText(xml); + Element rootElement = doc.getRootElement(); + Element assertionElement = rootElement.element(ASSERTION_NODE); + Element statementElement = assertionElement.element(ATTRIBUTE_STATEMENT_NODE); + if (Objects.nonNull(statementElement)) { + List list = statementElement.elements(); + for (Object ele : list) { + Element element = (Element) ele; + if (StringUtils.equals(MOBILE_NODE.toLowerCase(), element.attributeValue(MATCHING_PARAMETER_NAME).toLowerCase())) { + String mobile = element.element(ATTRIBUTE_VALUE_NODE).getText(); + FineLoggerFactory.getLogger().info("The parsed mobile is[{}]", mobile); + return mobile; + } + } + } + } + return StringUtils.EMPTY; + } + + public static String getCorpId(String xml) throws Exception { + if (StringUtils.isNotEmpty(xml)) { + Document doc = DocumentHelper.parseText(xml); + Element rootElement = doc.getRootElement(); + Element assertionElement = rootElement.element(ASSERTION_NODE); + Element statementElement = assertionElement.element(ATTRIBUTE_STATEMENT_NODE); + if (Objects.nonNull(statementElement)) { + List list = statementElement.elements(); + for (Object ele : list) { + Element element = (Element) ele; + if (StringUtils.equals(CORP_ID_NODE.toLowerCase(), element.attributeValue(MATCHING_PARAMETER_NAME).toLowerCase())) { + String corpId = element.element(ATTRIBUTE_VALUE_NODE).getText(); + FineLoggerFactory.getLogger().info("The parsed corpId is[{}]", corpId); + return corpId; + } + } + } + } + return StringUtils.EMPTY; + } + + public static String getUserName(String xml) throws DocumentException { + if (StringUtils.isNotEmpty(xml)) { + Document doc = DocumentHelper.parseText(xml); + Element rootElement = doc.getRootElement(); + Element assertionElement = rootElement.element(ASSERTION_NODE); + Element subjectElement = assertionElement.element(SUBJECT_NODE); + Element nameElement = subjectElement.element(NAME_ID_NODE); + String nameID = nameElement.getText(); + if (StringUtils.isNotEmpty(nameID)) { + FineLoggerFactory.getLogger().info("The parsed username is[{}]", nameID); + return nameID; + } + } + throw new UnsupportedOperationException("Can not get NameID!"); +// return userName; + } +} +