Browse Source

提交开源任务材料

master
LAPTOP-SB56SG4Q\86185 3 years ago
parent
commit
43e45b6556
  1. BIN
      JSD-9373-需求确认书.docx
  2. BIN
      JSD-9373配置使用文档-Production.docx
  3. BIN
      JSD-9373配置使用文档-测试环境.docx
  4. 5
      README.md
  5. 151
      fr-sp-saml/pom.xml
  6. 36
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/Application.java
  7. 54
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/config/MvcConfig.java
  8. 524
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/config/WebSecurityConfig.java
  9. 82
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/controllers/LandingController.java
  10. 61
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/controllers/SSOController.java
  11. 52
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/CurrentUserHandlerMethodArgumentResolver.java
  12. 53
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/SAMLEntryPointSub.java
  13. 57
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/SAMLUserDetailsServiceImpl.java
  14. 117
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/MySecureProtocolSocketFactory.java
  15. 44
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/MyX509TrustManager.java
  16. 220
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/RSA.java
  17. 28
      fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/stereotypes/CurrentUser.java
  18. 21
      fr-sp-saml/src/main/resources/application.properties
  19. BIN
      fr-sp-saml/src/main/resources/saml/samlKeystore.jks
  20. BIN
      fr-sp-saml/src/main/resources/saml/server.cer
  21. BIN
      fr-sp-saml/src/main/resources/saml/server.keystore
  22. 13
      fr-sp-saml/src/main/resources/saml/update-certifcate.sh
  23. 7
      fr-sp-saml/src/main/resources/static/css/bootstrap.min.css
  24. 1
      fr-sp-saml/src/main/resources/static/css/bootstrap.min.css.map
  25. 98
      fr-sp-saml/src/main/resources/static/css/spring-saml-sp.css
  26. BIN
      fr-sp-saml/src/main/resources/static/img/favicon.ico
  27. BIN
      fr-sp-saml/src/main/resources/static/img/nyan-cat.png
  28. BIN
      fr-sp-saml/src/main/resources/static/img/saml-flow.png
  29. BIN
      fr-sp-saml/src/main/resources/static/img/spring-boot-saml.png
  30. 7
      fr-sp-saml/src/main/resources/static/js/bootstrap.min.js
  31. 1
      fr-sp-saml/src/main/resources/static/js/bootstrap.min.js.map
  32. 45
      fr-sp-saml/src/main/resources/templates/layout.html
  33. 43
      fr-sp-saml/src/main/resources/templates/pages/discovery.html
  34. 53
      fr-sp-saml/src/main/resources/templates/pages/index.html
  35. 31
      fr-sp-saml/src/main/resources/templates/pages/landing.html
  36. BIN
      lib/finekit-10.0.jar
  37. 27
      plugin.xml
  38. 39
      src/main/java/com/fr/plugin/xxxx/saml/LocaleFinder.java
  39. 34
      src/main/java/com/fr/plugin/xxxx/saml/SamlMonitor.java
  40. 60
      src/main/java/com/fr/plugin/xxxx/saml/config/SamlConfig.java
  41. 200
      src/main/java/com/fr/plugin/xxxx/saml/request/LoginFilter.java
  42. 196
      src/main/java/com/fr/plugin/xxxx/saml/util/RSA.java
  43. 7
      src/main/resources/com/fr/plugin/xxxx/saml/locale/lang.properties
  44. 7
      src/main/resources/com/fr/plugin/xxxx/saml/locale/lang_zh_CN.properties
  45. BIN
      基于SAML协议与Azure AD集成实现fr单点登录.docx

BIN
JSD-9373-需求确认书.docx

Binary file not shown.

BIN
JSD-9373配置使用文档-Production.docx

Binary file not shown.

BIN
JSD-9373配置使用文档-测试环境.docx

Binary file not shown.

5
README.md

@ -1,3 +1,6 @@
# open-JSD-9373
JSD-9373 SAML+Azure AD单点登录
JSD-9373 SAML+Azure AD单点登录\
免责说明:该源码为第三方爱好者提供,不保证源码和方案的可靠性,也不提供任何形式的源码教学指导和协助!\
仅作为开发者学习参考使用!禁止用于任何商业用途!\
为保护开发者隐私,开发者信息已隐去!若原开发者希望公开自己的信息,可联系hugh处理。

151
fr-sp-saml/pom.xml

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.vdenotaris.spring</groupId>
<artifactId>fr-sp-saml</artifactId>
<version>1.0</version>
<!-- <packaging>jar</packaging>-->
<packaging>war</packaging>
<!-- Project description -->
<name>Spring Boot sample SAML 2.0 Service Provider</name>
<description>A sample SAML 2.0 Service Provider built on Spring Boot.</description>
<url>https://github.com/vdenotaris/spring-boot-security-saml-sample</url>
<developers>
<developer>
<id>vdenotaris</id>
<name>Vincenzo De Notaris</name>
<email>dev@vdenotaris.com</email>
<timezone>CET/CEST</timezone>
</developer>
</developers>
<contributors>
<contributor>
<name>Vladimír Schäfer</name>
</contributor>
<contributor>
<name>Alexey Syrtsev</name>
</contributor>
</contributors>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
</license>
</licenses>
<inceptionYear>2021</inceptionYear>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<start-class>com.vdenotaris.spring.boot.security.saml.web.Application</start-class>
<jackson.version>2.13.0</jackson.version>
<log4j2.version>2.17.0</log4j2.version>
</properties>
<!-- Inherit defaults from Spring Boot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<!-- <version>2.6.1</version>-->
<!-- <relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
</parent>
<repositories>
<repository>
<id>Shibboleth</id>
<name>Shibboleth</name>
<url>https://build.shibboleth.net/nexus/content/repositories/releases/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>nz.net.ultraq.thymeleaf</groupId>
<artifactId>thymeleaf-layout-dialect</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.extensions</groupId>
<artifactId>spring-security-saml2-core</artifactId>
<version>1.0.10.RELEASE</version>
<exclusions>
<exclusion>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
</dependencies>
<scm>
<connection>scm:git:git@github.com:vdenotaris/spring-boot-security-saml-sample.git</connection>
<url>scm:git:git@github.com:vdenotaris/spring-boot-security-saml-sample.git</url>
<developerConnection>scm:git:git@github.com:vdenotaris/spring-boot-security-saml-sample.git</developerConnection>
</scm>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.2</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

36
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/Application.java

@ -0,0 +1,36 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}

54
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/config/MvcConfig.java

@ -0,0 +1,54 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.config;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.vdenotaris.spring.boot.security.saml.web.core.CurrentUserHandlerMethodArgumentResolver;
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Autowired
CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("pages/index");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/static/**")) {
registry.addResourceHandler("/static/**")
.addResourceLocations("/static/");
}
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
argumentResolvers.add(currentUserHandlerMethodArgumentResolver);
}
}

524
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/config/WebSecurityConfig.java

@ -0,0 +1,524 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.config;
import com.vdenotaris.spring.boot.security.saml.web.core.SAMLEntryPointSub;
import com.vdenotaris.spring.boot.security.saml.web.core.SAMLUserDetailsServiceImpl;
import com.vdenotaris.spring.boot.security.saml.web.ssl.MySecureProtocolSocketFactory;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
import org.apache.velocity.app.VelocityEngine;
import org.opensaml.saml2.metadata.provider.HTTPMetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProvider;
import org.opensaml.saml2.metadata.provider.MetadataProviderException;
import org.opensaml.xml.parse.ParserPool;
import org.opensaml.xml.parse.StaticBasicParserPool;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.saml.*;
import org.springframework.security.saml.context.SAMLContextProviderImpl;
import org.springframework.security.saml.key.JKSKeyManager;
import org.springframework.security.saml.key.KeyManager;
import org.springframework.security.saml.log.SAMLDefaultLogger;
import org.springframework.security.saml.metadata.*;
import org.springframework.security.saml.parser.ParserPoolHolder;
import org.springframework.security.saml.processor.*;
import org.springframework.security.saml.storage.EmptyStorageFactory;
import org.springframework.security.saml.util.VelocityFactory;
import org.springframework.security.saml.websso.*;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CsrfFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import java.util.*;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements InitializingBean, DisposableBean {
private Timer backgroundTaskTimer;
private MultiThreadedHttpConnectionManager multiThreadedHttpConnectionManager;
@Value("${keyStorePath}")
private String keyStorePath;
@Value("${storePass}")
private String storePass;
@Value("#{${privatePasswordMap}}")
private Map<String, String> privatePasswordMap;
@Value("${defaultPrivateKey}")
private String defaultPrivateKey;
@Value("${spEntityId}")
private String spEntityId;
@Value("${metadataURL}")
private String metadataURL;
@Value("${maxAuthenticationAge}")
private long maxAuthenticationAge;
public void init() {
this.backgroundTaskTimer = new Timer(true);
this.multiThreadedHttpConnectionManager = new MultiThreadedHttpConnectionManager();
}
public void shutdown() {
this.backgroundTaskTimer.purge();
this.backgroundTaskTimer.cancel();
this.multiThreadedHttpConnectionManager.shutdown();
}
@Autowired
private SAMLUserDetailsServiceImpl samlUserDetailsServiceImpl;
// Initialization of the velocity engine
@Bean
public VelocityEngine velocityEngine() {
return VelocityFactory.getEngine();
}
// XML parser pool needed for OpenSAML parsing
@Bean(initMethod = "initialize")
public StaticBasicParserPool parserPool() {
return new StaticBasicParserPool();
}
@Bean(name = "parserPoolHolder")
public ParserPoolHolder parserPoolHolder() {
return new ParserPoolHolder();
}
// Bindings, encoders and decoders used for creating and parsing messages
@Bean
public HttpClient httpClient() {
//声明
ProtocolSocketFactory fcty = new MySecureProtocolSocketFactory();
//加入相关的https请求方式
Protocol.registerProtocol("https", new Protocol("https", fcty, 443));
return new HttpClient(this.multiThreadedHttpConnectionManager);
}
// SAML Authentication Provider responsible for validating of received SAML
// messages
@Bean
public SAMLAuthenticationProvider samlAuthenticationProvider() {
SAMLAuthenticationProvider samlAuthenticationProvider = new SAMLAuthenticationProvider();
samlAuthenticationProvider.setUserDetails(samlUserDetailsServiceImpl);
samlAuthenticationProvider.setForcePrincipalAsString(false);
return samlAuthenticationProvider;
}
// Provider of default SAML Context
@Bean
public SAMLContextProviderImpl contextProvider() {
SAMLContextProviderImpl samlContextProvider = new SAMLContextProviderImpl();
samlContextProvider.setStorageFactory(new EmptyStorageFactory());
return samlContextProvider;
}
// Initialization of OpenSAML library
@Bean
public static SAMLBootstrap sAMLBootstrap() {
return new SAMLBootstrap();
}
// Logger for SAML messages and events
@Bean
public SAMLDefaultLogger samlLogger() {
return new SAMLDefaultLogger();
}
// SAML 2.0 WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumer webSSOprofileConsumer() {
WebSSOProfileConsumerImpl webSSOProfileConsumer = new WebSSOProfileConsumerImpl();
// 设定本地浏览器缓存时长
webSSOProfileConsumer.setMaxAuthenticationAge(this.maxAuthenticationAge);
return webSSOProfileConsumer;
}
// SAML 2.0 Holder-of-Key WebSSO Assertion Consumer
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOprofileConsumer() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 Web SSO profile
@Bean
public WebSSOProfile webSSOprofile() {
return new WebSSOProfileImpl();
}
// SAML 2.0 Holder-of-Key Web SSO profile
@Bean
public WebSSOProfileConsumerHoKImpl hokWebSSOProfile() {
return new WebSSOProfileConsumerHoKImpl();
}
// SAML 2.0 ECP profile
@Bean
public WebSSOProfileECPImpl ecpprofile() {
return new WebSSOProfileECPImpl();
}
@Bean
public SingleLogoutProfile logoutprofile() {
return new SingleLogoutProfileImpl();
}
// Central storage of cryptographic keys
@Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource(keyStorePath);
return new JKSKeyManager(storeFile, storePass, privatePasswordMap, defaultPrivateKey);
}
@Bean
public WebSSOProfileOptions defaultWebSSOProfileOptions() {
WebSSOProfileOptions webSSOProfileOptions = new WebSSOProfileOptions();
webSSOProfileOptions.setIncludeScoping(false);
return webSSOProfileOptions;
}
// Entry point to initialize authentication, default values taken from
// properties file
@Bean
public SAMLEntryPointSub samlEntryPoint() {
SAMLEntryPointSub samlEntryPoint = new SAMLEntryPointSub();
samlEntryPoint.setDefaultProfileOptions(defaultWebSSOProfileOptions());
return samlEntryPoint;
}
// Setup advanced info about metadata
@Bean
public ExtendedMetadata extendedMetadata() {
ExtendedMetadata extendedMetadata = new ExtendedMetadata();
extendedMetadata.setIdpDiscoveryEnabled(true);
extendedMetadata.setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
extendedMetadata.setSignMetadata(true);
extendedMetadata.setEcpEnabled(true);
// 增加https连接的验证
extendedMetadata.setSslHostnameVerification("allowAll");
return extendedMetadata;
}
// IDP Discovery Service
@Bean
public SAMLDiscovery samlIDPDiscovery() {
SAMLDiscovery idpDiscovery = new SAMLDiscovery();
idpDiscovery.setIdpSelectionPath("/saml/discovery");
return idpDiscovery;
}
@Bean
@Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider()
throws MetadataProviderException {
String idpSSOCircleMetadataURL = this.metadataURL;
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(
this.backgroundTaskTimer, httpClient(), idpSSOCircleMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
// extendedMetadataDelegate.setMetadataTrustCheck(true);
extendedMetadataDelegate.setMetadataTrustCheck(false);
extendedMetadataDelegate.setMetadataRequireSignature(false);
backgroundTaskTimer.purge();
return extendedMetadataDelegate;
}
// Configure HTTP Client to accept certificates from the keystore for HTTPS verification
// 新增加bean,SSL异常问题
// @Bean
// public TLSProtocolConfigurer tlsProtocolConfigurerHandler(){
// TLSProtocolConfigurer tlsProtocolConfigurer = new TLSProtocolConfigurer();
//// tlsProtocolConfigurer.setSslHostnameVerification("default");
// tlsProtocolConfigurer.setSslHostnameVerification("allowAll");
// return tlsProtocolConfigurer;
// }
// IDP Metadata configuration - paths to metadata of IDPs in circle of trust
// is here
// Do no forget to call iniitalize method on providers
@Bean
@Qualifier("metadata")
public CachingMetadataManager metadata() throws MetadataProviderException {
List<MetadataProvider> providers = new ArrayList<MetadataProvider>();
providers.add(ssoCircleExtendedMetadataProvider());
return new CachingMetadataManager(providers);
}
// Filter automatically generates default SP metadata
@Bean
public MetadataGenerator metadataGenerator() {
MetadataGenerator metadataGenerator = new MetadataGenerator();
metadataGenerator.setEntityId(this.spEntityId);
metadataGenerator.setExtendedMetadata(extendedMetadata());
metadataGenerator.setIncludeDiscoveryExtension(false);
metadataGenerator.setKeyManager(keyManager());
return metadataGenerator;
}
// The filter is waiting for connections on URL suffixed with filterSuffix
// and presents SP metadata there
@Bean
public MetadataDisplayFilter metadataDisplayFilter() {
return new MetadataDisplayFilter();
}
// Handler deciding where to redirect user after successful login
@Bean
public SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler() {
SavedRequestAwareAuthenticationSuccessHandler successRedirectHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successRedirectHandler.setDefaultTargetUrl("/landing");
return successRedirectHandler;
}
// Handler deciding where to redirect user after failed login
@Bean
public SimpleUrlAuthenticationFailureHandler authenticationFailureHandler() {
SimpleUrlAuthenticationFailureHandler failureHandler =
new SimpleUrlAuthenticationFailureHandler();
failureHandler.setUseForward(true);
failureHandler.setDefaultFailureUrl("/error");
return failureHandler;
}
@Bean
public SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter() throws Exception {
SAMLWebSSOHoKProcessingFilter samlWebSSOHoKProcessingFilter = new SAMLWebSSOHoKProcessingFilter();
samlWebSSOHoKProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOHoKProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOHoKProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOHoKProcessingFilter;
}
// Processing filter for WebSSO profile messages
@Bean
public SAMLProcessingFilter samlWebSSOProcessingFilter() throws Exception {
SAMLProcessingFilter samlWebSSOProcessingFilter = new SAMLProcessingFilter();
samlWebSSOProcessingFilter.setAuthenticationManager(authenticationManager());
samlWebSSOProcessingFilter.setAuthenticationSuccessHandler(successRedirectHandler());
samlWebSSOProcessingFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
return samlWebSSOProcessingFilter;
}
@Bean
public MetadataGeneratorFilter metadataGeneratorFilter() {
return new MetadataGeneratorFilter(metadataGenerator());
}
// Handler for successful logout
@Bean
public SimpleUrlLogoutSuccessHandler successLogoutHandler() {
SimpleUrlLogoutSuccessHandler successLogoutHandler = new SimpleUrlLogoutSuccessHandler();
successLogoutHandler.setDefaultTargetUrl("/");
return successLogoutHandler;
}
// Logout handler terminating local session
@Bean
public SecurityContextLogoutHandler logoutHandler() {
SecurityContextLogoutHandler logoutHandler =
new SecurityContextLogoutHandler();
logoutHandler.setInvalidateHttpSession(true);
logoutHandler.setClearAuthentication(true);
return logoutHandler;
}
// Filter processing incoming logout messages
// First argument determines URL user will be redirected to after successful
// global logout
@Bean
public SAMLLogoutProcessingFilter samlLogoutProcessingFilter() {
return new SAMLLogoutProcessingFilter(successLogoutHandler(),
logoutHandler());
}
// Overrides default logout processing filter with the one processing SAML
// messages
@Bean
public SAMLLogoutFilter samlLogoutFilter() {
return new SAMLLogoutFilter(successLogoutHandler(),
new LogoutHandler[] { logoutHandler() },
new LogoutHandler[] { logoutHandler() });
}
// Bindings
private ArtifactResolutionProfile artifactResolutionProfile() {
final ArtifactResolutionProfileImpl artifactResolutionProfile =
new ArtifactResolutionProfileImpl(httpClient());
artifactResolutionProfile.setProcessor(new SAMLProcessorImpl(soapBinding()));
return artifactResolutionProfile;
}
@Bean
public HTTPArtifactBinding artifactBinding(ParserPool parserPool, VelocityEngine velocityEngine) {
return new HTTPArtifactBinding(parserPool, velocityEngine, artifactResolutionProfile());
}
@Bean
public HTTPSOAP11Binding soapBinding() {
return new HTTPSOAP11Binding(parserPool());
}
@Bean
public HTTPPostBinding httpPostBinding() {
return new HTTPPostBinding(parserPool(), velocityEngine());
}
@Bean
public HTTPRedirectDeflateBinding httpRedirectDeflateBinding() {
return new HTTPRedirectDeflateBinding(parserPool());
}
@Bean
public HTTPSOAP11Binding httpSOAP11Binding() {
return new HTTPSOAP11Binding(parserPool());
}
@Bean
public HTTPPAOS11Binding httpPAOS11Binding() {
return new HTTPPAOS11Binding(parserPool());
}
// Processor
@Bean
public SAMLProcessorImpl processor() {
Collection<SAMLBinding> bindings = new ArrayList<SAMLBinding>();
bindings.add(httpRedirectDeflateBinding());
bindings.add(httpPostBinding());
bindings.add(artifactBinding(parserPool(), velocityEngine()));
bindings.add(httpSOAP11Binding());
bindings.add(httpPAOS11Binding());
return new SAMLProcessorImpl(bindings);
}
/**
* Define the security filter chain in order to support SSO Auth by using SAML 2.0
*
* @return Filter chain proxy
* @throws Exception
*/
@Bean
public FilterChainProxy samlFilter() throws Exception {
List<SecurityFilterChain> chains = new ArrayList<SecurityFilterChain>();
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/login/**"),
samlEntryPoint()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/logout/**"),
samlLogoutFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/metadata/**"),
metadataDisplayFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSO/**"),
samlWebSSOProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SSOHoK/**"),
samlWebSSOHoKProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/SingleLogout/**"),
samlLogoutProcessingFilter()));
chains.add(new DefaultSecurityFilterChain(new AntPathRequestMatcher("/saml/discovery/**"),
samlIDPDiscovery()));
return new FilterChainProxy(chains);
}
/**
* Returns the authentication manager currently used by Spring.
* It represents a bean definition with the aim allow wiring from
* other classes performing the Inversion of Control (IoC).
*
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* Defines the web based security configuration.
*
* @param http It allows configuring web based security for specific http requests.
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.authenticationEntryPoint(samlEntryPoint());
http
.addFilterBefore(metadataGeneratorFilter(), ChannelProcessingFilter.class)
.addFilterAfter(samlFilter(), BasicAuthenticationFilter.class)
.addFilterBefore(samlFilter(), CsrfFilter.class);
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/saml/**").permitAll()
.antMatchers("/css/**").permitAll()
.antMatchers("/img/**").permitAll()
.antMatchers("/js/**").permitAll()
.anyRequest().authenticated();
http
.logout()
.disable(); // The logout procedure is already handled by SAML filters.
}
/**
* Sets a custom authentication provider.
*
* @param auth SecurityBuilder used to create an AuthenticationManager.
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(samlAuthenticationProvider());
}
@Override
public void afterPropertiesSet() throws Exception {
init();
}
@Override
public void destroy() throws Exception {
shutdown();
}
}

82
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/controllers/LandingController.java

@ -0,0 +1,82 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.controllers;
import com.vdenotaris.spring.boot.security.saml.web.ssl.RSA;
import com.vdenotaris.spring.boot.security.saml.web.stereotypes.CurrentUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
@Controller
public class LandingController {
@Value("${frServerUrl}")
private String frServerUrl;
@Value("${paramName}")
public String paramName;
@Value("${publicKey}")
public String publicKey;
// Logger
private static final Logger LOG = LoggerFactory.getLogger(LandingController.class);
@RequestMapping("/landing")
public void landing(@CurrentUser User user, HttpServletRequest request, HttpServletResponse response) throws IOException, InvalidKeySpecException, NoSuchAlgorithmException {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null)
LOG.debug("Current authentication instance from security context is null");
else
LOG.debug("Current authentication instance from security context: " + this.getClass().getSimpleName());
Object redirectUri = request.getSession().getAttribute("redirectUri");
String url = this.frServerUrl;
if (redirectUri != null) {
url = (String) redirectUri;
}
url = setURLParam(url, encryptUsername(user.getUsername()));
LOG.debug("fr-sp-saml-LandingController--Landing Redirect URL:" + url);
response.sendRedirect(url);
}
/**
* 处理请求url加入token参数
*
* @param url
* @return
*/
private String setURLParam(String url, String paramValue) {
if (url.contains("?")) {
return url + "&" + paramName + "=" + paramValue + "&state=saml";
}
return url + "?" + paramName + "=" + paramValue + "&state=saml";
}
private String encryptUsername(String username) throws InvalidKeySpecException, NoSuchAlgorithmException {
return RSA.publicEncrypt(System.currentTimeMillis() + "#" + username, RSA.getPublicKey(publicKey));
}
}

61
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/controllers/SSOController.java

@ -0,0 +1,61 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.controllers;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.saml.metadata.MetadataManager;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.Set;
@Controller
@RequestMapping("/saml")
public class SSOController {
// Logger
private static final Logger LOG = LoggerFactory.getLogger(SSOController.class);
@Autowired
private MetadataManager metadata;
@RequestMapping(value = "/discovery", method = RequestMethod.GET)
public String idpSelection(Model model) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null)
LOG.debug("Current authentication instance from security context is null");
else
LOG.debug("Current authentication instance from security context: " + this.getClass().getSimpleName());
if (auth == null || (auth instanceof AnonymousAuthenticationToken)) {
Set<String> idps = metadata.getIDPEntityNames();
for (String idp : idps)
LOG.info("Configured Identity Provider for SSO: " + idp);
model.addAttribute("idps", idps);
return "pages/discovery";
} else {
LOG.warn("The current user is already logged.");
return "redirect:/landing";
}
}
}

52
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/CurrentUserHandlerMethodArgumentResolver.java

@ -0,0 +1,52 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.core;
import java.security.Principal;
import org.springframework.core.MethodParameter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.vdenotaris.spring.boot.security.saml.web.stereotypes.CurrentUser;
@Component
public class CurrentUserHandlerMethodArgumentResolver implements
HandlerMethodArgumentResolver {
public boolean supportsParameter(MethodParameter methodParameter) {
return methodParameter.getParameterAnnotation(CurrentUser.class) != null
&& methodParameter.getParameterType().equals(User.class);
}
public Object resolveArgument(MethodParameter methodParameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
if (this.supportsParameter(methodParameter)) {
Principal principal = (Principal) webRequest.getUserPrincipal();
return (User) ((Authentication) principal).getPrincipal();
} else {
return WebArgumentResolver.UNRESOLVED;
}
}
}

53
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/SAMLEntryPointSub.java

@ -0,0 +1,53 @@
/*
* Copyright (C), 2018-2021
* Project: dev
* FileName: SAMLEntryPointSub
* Author: Louis
* Date: 2021/4/20 21:48
*/
package com.vdenotaris.spring.boot.security.saml.web.core;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.saml.SAMLEntryPoint;
import org.springframework.security.web.FilterInvocation;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
/**
* <Function Description><br>
* <SAMLEntryPointSub>
*
* @author Louis
* @since 1.0.0
*/
public class SAMLEntryPointSub extends SAMLEntryPoint {
private static final Logger LOG = LoggerFactory.getLogger(SAMLEntryPointSub.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(request, response, chain);
if (!this.processFilter(fi.getRequest())) {
chain.doFilter(request, response);
} else {
this.commence(fi.getRequest(), fi.getResponse(), (AuthenticationException) null);
}
// 存储跳转路径
String redirectUri = request.getParameter("redirectUri");
if (StringUtils.isNotBlank(redirectUri)) {
try {
HttpSession session = fi.getHttpRequest().getSession(true);
session.setAttribute("redirectUri", redirectUri);
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
}

57
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/core/SAMLUserDetailsServiceImpl.java

@ -0,0 +1,57 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.core;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.saml.SAMLCredential;
import org.springframework.security.saml.userdetails.SAMLUserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class SAMLUserDetailsServiceImpl implements SAMLUserDetailsService {
// Logger
private static final Logger LOG = LoggerFactory.getLogger(SAMLUserDetailsServiceImpl.class);
public Object loadUserBySAML(SAMLCredential credential)
throws UsernameNotFoundException {
// The method is supposed to identify local account of user referenced by
// data in the SAML assertion and return UserDetails object describing the user.
String userID = credential.getNameID().getValue();
LOG.info(userID + " is logged in");
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_USER");
authorities.add(authority);
// In a real scenario, this implementation has to locate user in a arbitrary
// dataStore based on information present in the SAMLCredential and
// returns such a date in a form of application specific UserDetails object.
return new User(userID, "<abc123>", true, true, true, true, authorities);
}
}

117
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/MySecureProtocolSocketFactory.java

@ -0,0 +1,117 @@
/*
* Copyright (C), 2018-2021
* Project: spring-boot-security-saml-sample
* FileName: MySecureProtocolSocketFactory
* Author: Louis
* Date: 2021/1/17 13:10
*/
package com.vdenotaris.spring.boot.security.saml.web.ssl;
import org.apache.commons.httpclient.ConnectTimeoutException;
import org.apache.commons.httpclient.HttpClientError;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.protocol.ControllerThreadSocketFactory;
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
/**
* <Function Description><br>
* <MySecureProtocolSocketFactory>
*
* @author Louis
* @since 1.0.0
*/
public class MySecureProtocolSocketFactory implements SecureProtocolSocketFactory {
//这里添加一个属性,主要目的就是来获取ssl跳过验证
private SSLContext sslContext = null;
/**
* Constructor for MySecureProtocolSocketFactory.
*/
public MySecureProtocolSocketFactory() {
}
/**
* 这个创建一个获取SSLContext的方法导入MyX509TrustManager进行初始化
*
* @return
*/
private static SSLContext createEasySSLContext() {
try {
SSLContext context = SSLContext.getInstance("SSL");
context.init(null, new TrustManager[]{new MyX509TrustManager()},
null);
return context;
} catch (Exception e) {
throw new HttpClientError(e.toString());
}
}
/**
* 判断获取SSLContext
*
* @return
*/
private SSLContext getSSLContext() {
if (this.sslContext == null) {
this.sslContext = createEasySSLContext();
}
return this.sslContext;
}
//后面的方法基本上就是带入相关参数就可以了
/*
* (non-Javadoc)
*
* @see org.apache.commons.httpclient.protocol.ProtocolSocketFactory#createSocket(java.lang.String,
* int, java.net.InetAddress, int)
*/
public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException, UnknownHostException {
return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);
}
/*
* (non-Javadoc)
*
* @see org.apache.commons.httpclient.protocol.ProtocolSocketFactory#createSocket(java.lang.String,
* int, java.net.InetAddress, int,
* org.apache.commons.httpclient.params.HttpConnectionParams)
*/
public Socket createSocket(final String host, final int port, final InetAddress localAddress, final int localPort,
final HttpConnectionParams params) throws IOException, UnknownHostException, ConnectTimeoutException {
if (params == null) {
throw new IllegalArgumentException("Parameters may not be null");
}
int timeout = params.getConnectionTimeout();
if (timeout == 0) {
return createSocket(host, port, localAddress, localPort);
} else {
return ControllerThreadSocketFactory.createSocket(this, host, port, localAddress, localPort, timeout);
}
}
/*
* (non-Javadoc)
*
* @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
*/
public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
return getSSLContext().getSocketFactory().createSocket(host, port);
}
/*
* (non-Javadoc)
*
* @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
*/
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
return getSSLContext().getSocketFactory().createSocket(socket, host, port, autoClose);
}
}

44
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/MyX509TrustManager.java

@ -0,0 +1,44 @@
/*
* Copyright (C), 2018-2021
* Project: spring-boot-security-saml-sample
* FileName: MyX509TrustManager
* Author: Louis
* Date: 2021/1/17 13:09
*/
package com.vdenotaris.spring.boot.security.saml.web.ssl;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;
/**
* <Function Description><br>
* <MyX509TrustManager>
*
* @author Louis
* @since 1.0.0
*/
public class MyX509TrustManager implements X509TrustManager {
/* (non-Javadoc)
* @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], java.lang.String)
*/
public void checkClientTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
/* (non-Javadoc)
* @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], java.lang.String)
*/
public void checkServerTrusted(X509Certificate[] arg0, String arg1)
throws CertificateException {
}
/* (non-Javadoc)
* @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
*/
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}

220
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/ssl/RSA.java

@ -0,0 +1,220 @@
/*
* Copyright (C), 2018-2020
* Project: starter
* FileName: RSA
* Author: Louis
* Date: 2020/12/28 14:42
*/
package com.vdenotaris.spring.boot.security.saml.web.ssl;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* <Function Description><br>
* <RSA>
*
* @author Louis
* @since 1.0.0
*/
public class RSA {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA"; // ALGORITHM 算法的意思
public static Map<String, String> createKeys(int keySize) {
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
// 初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
// 生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
// 得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
// map装载公钥和私钥
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
// 返回map
return keyPairMap;
}
/**
* 得到公钥
*
* @param publicKey 密钥字符串经过base64编码
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}
/**
* 得到私钥
*
* @param privateKey 密钥字符串经过base64编码
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥加密
*
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//每个Cipher初始化方法使用一个模式参数opmod,并用此模式初始化Cipher对象。此外还有其他参数,包括密钥key、包含密钥的证书certificate、算法参数params和随机源random。
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 公钥解密
*
* @param data
* @param publicKey
* @return
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
//rsa切割解码 , ENCRYPT_MODE,加密数据 ,DECRYPT_MODE,解密数据
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0; //最大块
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
//可以调用以下的doFinal()方法完成加密或解密数据:
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
public static void main(String[] args) throws Exception {
// 调用生成公钥和私钥
Map<String, String> keyMap = RSA.createKeys(1024);
// 公钥
String publicKey = keyMap.get("publicKey");
// 私钥
String privateKey = keyMap.get("privateKey");
System.out.println("公钥: \n\r" + publicKey);
System.out.println("私钥: \n\r" + privateKey);
System.out.println("公钥加密——私钥解密");
String str = "站在大明门前守卫的禁卫军,事先没有接到\n" + "有关的命令,但看到大批盛装的官员来临,也就\n" + "以为确系举行大典,因而未加询问。进大明门即\n" + "为皇城。文武百官看到端门午门之前气氛平静,\n" + "城楼上下也无朝会的迹象,既无几案,站队点名\n" + "的御史和御前侍卫“大汉将军”也不见踪影,不免\n"
+ "心中揣测,互相询问:所谓午朝是否讹传?";
System.out.println("\r明文:\r\n" + str);
System.out.println("\r明文大小:\r\n" + str.getBytes().length);
String encodedData = RSA.publicEncrypt(str, RSA.getPublicKey(publicKey)); //传入明文和公钥加密,得到密文
System.out.println("密文:\r\n" + encodedData);
String decodedData = RSA.privateDecrypt(encodedData, RSA.getPrivateKey(privateKey)); //传入密文和私钥,得到明文
System.out.println("解密后文字: \r\n" + decodedData);
}
}

28
fr-sp-saml/src/main/java/com/vdenotaris/spring/boot/security/saml/web/stereotypes/CurrentUser.java

@ -0,0 +1,28 @@
/*
* Copyright 2020 Vincenzo De Notaris
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.vdenotaris.spring.boot.security.saml.web.stereotypes;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {}

21
fr-sp-saml/src/main/resources/application.properties

@ -0,0 +1,21 @@
logging.level.org.springframework.security.saml=DEBUG
logging.level.org.opensaml=DEBUG
logging.level.com.vdenotaris.spring.boot.security.saml=DEBUG
logging.file=logs/fr-sp-saml.log
spring.main.allow-circular-references=TRUE
# Key Config
keyStorePath=classpath:/saml/samlKeystore.jks
storePass=nalle123
privatePasswordMap={"apollo":"nalle123"}
defaultPrivateKey=apollo
# Service Provider EntityId
spEntityId=https://cpds.ecouncil.ae/fr-sp-saml
# IDP meta url
metadataURL=https://login.microsoftonline.com/99cf3350-40c8-4260-bb84-1dada4a029c0/federationmetadata/2007-06/federationmetadata.xml?appid=63572eb3-c6f2-4cf2-a1ba-39d31c2c1549
# fineReport URL
frServerUrl=https://cpds.ecouncil.ae/decision
paramName=token
publicKey=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCtuesGTBnEIU3qOjsuwOM7ZtQcgzycO+0ezuEsuABzOFqqmoAIOA9yYglij/PyUvOI3JTd3NwhzLdF6Pt8uLHeJEYOiAp1bqY5TjMNLd6XpsUiXZvjCeOO1Ial1Iaron7pH2kzNOeA9ylhwGg/TuoV8W66gN2i5y4eWp2di/02RwIDAQAB
maxAuthenticationAge=5184000

BIN
fr-sp-saml/src/main/resources/saml/samlKeystore.jks

Binary file not shown.

BIN
fr-sp-saml/src/main/resources/saml/server.cer

Binary file not shown.

BIN
fr-sp-saml/src/main/resources/saml/server.keystore

Binary file not shown.

13
fr-sp-saml/src/main/resources/saml/update-certifcate.sh

@ -0,0 +1,13 @@
#!/bin/bash
IDP_HOST=dap.adidas.com
IDP_PORT=443
CERTIFICATE_FILE=idp.cert
KEYSTORE_FILE=server.keystore
KEYSTORE_PASSWORD=changeit
openssl s_client -host $IDP_HOST -port $IDP_PORT -prexit -showcerts </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > $CERTIFICATE_FILE
keytool -delete -alias ssocircle -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD
keytool -import -alias ssocircle -file $CERTIFICATE_FILE -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD -noprompt
rm $CERTIFICATE_FILE

7
fr-sp-saml/src/main/resources/static/css/bootstrap.min.css vendored

File diff suppressed because one or more lines are too long

1
fr-sp-saml/src/main/resources/static/css/bootstrap.min.css.map

File diff suppressed because one or more lines are too long

98
fr-sp-saml/src/main/resources/static/css/spring-saml-sp.css

@ -0,0 +1,98 @@
body
{
padding-bottom:25px;
padding-top:25px;
}
.text-white-50
{
color:rgba(255,255,255,.5);
}
h6.text-black
{
color:#000;
}
.spring-green
{
color:#68bd45;
}
.bg-spring-green
{
background-color:#68bd45;
}
.border-bottom
{
border-bottom:1px solid #e5e5e5;
}
.box-shadow
{
box-shadow:0 .25rem .75rem rgba(0,0,0,.05);
}
.lh-100
{
line-height:1;
}
.lh-125
{
line-height:1.25;
}
.lh-150
{
line-height:1.5;
}
.margin-top-10
{
margin-top:10px;
}
.margin-bottom-10
{
margin-top:10px;
}
.container
{
max-width:600px;
}
.btn-spring
{
background-color:#68bd45;
color:#FFF;
}
footer
{
border-top:1px solid #e5e5e5;
color:#696969;
font-size: 0.7em;
margin-top: 20px;
padding-top: 20px;
text-align:center;
}
ul.footer-note
{
list-style-type:none;
margin:0 0 10px;
padding:0;
}
#sso-btn a:hover
{
color:#FFF;
}
footer a:link,footer a:visited,footer a:hover,footer a:active
{
color:green;
}

BIN
fr-sp-saml/src/main/resources/static/img/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
fr-sp-saml/src/main/resources/static/img/nyan-cat.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

BIN
fr-sp-saml/src/main/resources/static/img/saml-flow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
fr-sp-saml/src/main/resources/static/img/spring-boot-saml.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

7
fr-sp-saml/src/main/resources/static/js/bootstrap.min.js vendored

File diff suppressed because one or more lines are too long

1
fr-sp-saml/src/main/resources/static/js/bootstrap.min.js.map

File diff suppressed because one or more lines are too long

45
fr-sp-saml/src/main/resources/templates/layout.html

@ -0,0 +1,45 @@
<!--
Source: https://github.com/vdenotaris/spring-boot-security-saml-sample
Copyright 2020 Vincenzo De Notaris
Licensed under the Apache License, Version 2.0 (the "License").
-->
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Spring Boot - SAML 2.0 Service Provider">
<meta name="author" content="Vincenzo De Notaris">
<link rel="icon" th:href="@{/img/favicon.ico}">
<title>Spring Boot &mdash; SAML 2.0 Service Provider</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/spring-saml-sp.css}" rel="stylesheet">
</head>
<body class="bg-light">
<!-- Main starts -->
<div role="main" class="container">
<!-- Header starts -->
<header role="header" class="d-flex align-items-center p-3 my-3 text-white-50 bg-spring-green rounded box-shadow">
<img class="mr-3" th:src="@{/img/spring-boot-saml.png}" alt="" height="48">
<div class="lh-100">
<h6 class="mb-0 text-white lh-100">Spring Boot &mdash; SAML 2.0 Service Provider</h6>
</div>
</header>
<!-- Header ends -->
<!-- Content starts -->
<section role="content" class="my-3 p-3 bg-white rounded box-shadow" layout:fragment="content">
<!-- Page content goes here! -->
</section>
<!-- Content ends -->
</div>
<!-- Main ends -->
<!-- Bootstrap scripts -->
<script th:src="@{/js/bootstrap.min.js}"></script>
</body>
</html>

43
fr-sp-saml/src/main/resources/templates/pages/discovery.html

@ -0,0 +1,43 @@
<!doctype html>
<html
lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
layout:decorate="~{layout}"
>
<body>
<!-- Content starts -->
<section class="my-3 p-3 bg-white rounded box-shadow" layout:fragment="content">
<div class="alert alert-warning" role="alert">
<small>
<strong>Here we go!</strong> Select an Identity provider that holds your authentication data.<br/>
<img class="img-fluid margin-top-10 margin-bottom-10" th:src="@{/img/saml-flow.png}" />
The Service Provider (SP) generates a SAML 2.0 authentication request, which is encoded and embedded into the URL for SSO
service. After being redirected, you must provide your credentials to authenticate against the selected IdP.</small>
</div>
<h6 class="border-bottom border-gray pb-2 mb-0">Select your Identity Provider:</h6>
<form th:action="${idpDiscoReturnURL}" method="get">
<fieldset class="form-group">
<div class="form-check" th:each="idp : ${idps}">
<label class="form-check-label">
<input type="radio" class="form-check-input" th:name="${idpDiscoReturnParam}" th:id="'idp_' + ${idp}" th:value="${idp}" />
<span class="badge badge-dark" th:text="${idp}">null</span>
</label>
</div>
</fieldset>
<small class="d-block text-right mt-3" id="sso-btn">
<button type="submit" class="btn btn-spring btn-sm">
<i class="fas fa-handshake"></i> Start 3rd Party Login
</button>
</small>
</form>
</section>
<!-- Content ends -->
</body>
</html>

53
fr-sp-saml/src/main/resources/templates/pages/index.html

@ -0,0 +1,53 @@
<!doctype html>
<html
lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{layout}"
>
<body>
<!-- Content starts -->
<section class="my-3 p-3 bg-white rounded box-shadow" layout:fragment="content">
<h6 class="border-bottom border-gray pb-2 mb-0">The authentication flow, step by step:</h6>
<div class="media text-muted pt-3">
<i class="fas fa-building fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
<strong class="d-block text-gray-dark">Select your Identity Provider (IdP)</strong>
Select an Identity provider that holds your authentication data. You can either enable users to explicitly select an IdP
(like in this case) or you can configure as well an automatic means of Identity Provider discovery.
</p>
</div>
<div class="media text-muted pt-3">
<i class="fas fa-user-lock fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
<strong class="d-block text-gray-dark">Authenticate against the selected IdP</strong>
The Service Provider (SP) generates a SAML 2.0 authentication request, which is encoded and embedded into the URL for SSO
service. After being redirected, you must provide your credentials to authenticate against the selected IdP.
</p>
</div>
<div class="media text-muted pt-3">
<i class="fas fa-user-tag fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
<strong class="d-block text-gray-dark">Get back and see your login data</strong>
The Identity Provider returns the encoded SAML response to the browser. You will be redirected back to the Service Provider.
If your identity is established by the IdP, you will be provided with app access and your profile data displayed.
</p>
</div>
<div class="media text-muted pt-3">
<i class="fas fa-door-closed fa-2x fa-fw mr-2 spring-green" data-fa-transform="shrink-4"></i>
<p class="media-body pb-3 mb-0 small lh-125 border-bottom border-gray">
<strong class="d-block text-gray-dark">Logout from your session</strong>
You can now logout from the app. If enabled, you can also invoke the Single Logout (SLO) that invalidates client application
sessions in addition to its own SSO session (IdP-side).
</p>
</div>
<small class="d-block text-right mt-3" id="sso-btn">
<a th:href="@{/saml/login}" class="btn btn-spring btn-sm">
<i class="fas fa-rocket"></i> Get started</a>
</small>
</section>
<!-- Content ends -->
<body>
</html>

31
fr-sp-saml/src/main/resources/templates/pages/landing.html

@ -0,0 +1,31 @@
<!doctype html>
<html
lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
layout:decorate="~{layout}"
>
<body>
<!-- Content starts -->
<section class="my-3 p-3 bg-white rounded box-shadow" layout:fragment="content">
<div class="alert alert-success" role="alert">
<small><strong>Well done!</strong> Aww yeah, you successfullying logged in.<br/>
Your SAML 2.0 authentication process works fine.</small>
</div>
<p><img class="img-fluid" th:src="@{/img/nyan-cat.png}" /></p>
<p>You are logged as <span class="badge badge-dark" th:text="${username}">null</span>.</p>
<small class="d-block text-right mt-3" id="sso-btn">
<a th:href="@{/saml/logout}" class="btn btn-spring btn-sm">
<i class="far fa-user-circle"></i> Global logout
</a>
<a th:href="@{/saml/logout?local=true}" class="btn btn-spring btn-sm">
<i class="fas fa-sign-out-alt"></i> Local logout
</a>
</small>
</section>
<!-- Content ends -->
</body>
</html>

BIN
lib/finekit-10.0.jar

Binary file not shown.

27
plugin.xml

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<id>com.fr.plugin.xxxx.saml.v10</id>
<name><![CDATA[SAML登陆集成]]></name>
<active>yes</active>
<version>1.0</version>
<env-version>10.0</env-version>
<jartime>2018-07-31</jartime>
<vendor>fr.open</vendor>
<description><![CDATA[SAML登陆集成,实现单点登陆认证功能。]]></description>
<change-notes><![CDATA[
版本1.0主要功能:<br/>
SAML单点登陆认证功能。<br/>
]]></change-notes>
<main-package>com.fr.plugin.xxxx.saml</main-package>
<prefer-packages>
<prefer-package>com.fanruan.api</prefer-package>
</prefer-packages>
<lifecycle-monitor class="com.fr.plugin.xxxx.saml.SamlMonitor"/>
<extra-core>
<LocaleFinder class="com.fr.plugin.xxxx.saml.LocaleFinder"/>
</extra-core>
<extra-decision>
<GlobalRequestFilterProvider class="com.fr.plugin.xxxx.saml.request.LoginFilter"/>
</extra-decision>
<function-recorder class="com.fr.plugin.xxxx.saml.LocaleFinder"/>
</plugin>

39
src/main/java/com/fr/plugin/xxxx/saml/LocaleFinder.java

@ -0,0 +1,39 @@
/*
* Copyright (C), 2018-2020
* Project: starter
* FileName: LocaleFinder
* Author: Louis
* Date: 2020/8/31 22:19
*/
package com.fr.plugin.xxxx.saml;
import com.fr.intelli.record.Focus;
import com.fr.intelli.record.Original;
import com.fr.record.analyzer.EnableMetrics;
import com.fr.stable.fun.Authorize;
import com.fr.stable.fun.impl.AbstractLocaleFinder;
import static com.fr.plugin.xxxx.saml.config.SamlConfig.PLUGIN_ID;
/**
* <Function Description><br>
* <LocaleFinder>
*
* @author fr.open
* @since 1.0.0
*/
@EnableMetrics
@Authorize(callSignKey = PLUGIN_ID)
public class LocaleFinder extends AbstractLocaleFinder {
@Override
@Focus(id = PLUGIN_ID, text = "Plugin-Saml", source = Original.PLUGIN)
public String find() {
return "com/fr/plugin/xxxx/saml/locale/lang";
}
@Override
public int currentAPILevel() {
return CURRENT_LEVEL;
}
}

34
src/main/java/com/fr/plugin/xxxx/saml/SamlMonitor.java

@ -0,0 +1,34 @@
/*
* Copyright (C), 2018-2021
* Project: starter
* FileName: SamlMonitor
* Author: Louis
* Date: 2021/3/30 15:10
*/
package com.fr.plugin.xxxx.saml;
import com.fr.plugin.context.PluginContext;
import com.fr.plugin.xxxx.saml.config.SamlConfig;
import com.fr.plugin.observer.inner.AbstractPluginLifecycleMonitor;
/**
* <Function Description><br>
* <SamlMonitor>
*
* @author fr.open
* @since 1.0.0
*/
public class SamlMonitor extends AbstractPluginLifecycleMonitor {
public SamlMonitor() {
}
@Override
public void afterRun(PluginContext pluginContext) {
SamlConfig.getInstance();
}
@Override
public void beforeStop(PluginContext pluginContext) {
}
}

60
src/main/java/com/fr/plugin/xxxx/saml/config/SamlConfig.java

@ -0,0 +1,60 @@
/*
* Copyright (C), 2018-2021
* Project: starter
* FileName: SamlConfig
* Author: Louis
* Date: 2021/3/30 9:38
*/
package com.fr.plugin.xxxx.saml.config;
import com.fanruan.api.util.StringKit;
import com.fr.config.*;
import com.fr.config.holder.Conf;
import com.fr.config.holder.factory.Holders;
import com.fr.intelli.record.Focus;
import com.fr.intelli.record.Original;
/**
* <Function Description><br>
* <SamlConfig>
*
* @author fr.open
* @since 1.0.0
*/
@Visualization(category = "Plugin-Saml_Group")
public class SamlConfig extends DefaultConfiguration {
public static final String PLUGIN_ID = "com.fr.plugin.mqh.saml.v10";
public static final String FR_SP_SAML = "http://localhost:8075/fr-sp-saml";
public static final String PRIVATE_KEY = "xxxx";
public static final String SP_TOKEN = "token";
public static final String URL_SP = "${spUrl}/saml/login?idp=${idpEntityId}&redirectUri=${redirectUri}";
private static volatile SamlConfig config = null;
@Identifier(value = "spUrl", name = "Plugin-Saml_Config_SpUrl", description = "Plugin-Saml_Config_SpUrl_Description", status = Status.SHOW)
private final Conf<String> spUrl = Holders.simple(FR_SP_SAML);
@Identifier(value = "idpEntityId", name = "Plugin-Saml_Config_IdpEntityId", description = "Plugin-Saml_Config_IdpEntityId_Description", status = Status.SHOW)
private final Conf<String> idpEntityId = Holders.simple(StringKit.EMPTY);
@Focus(id = PLUGIN_ID, text = "Plugin-Saml", source = Original.PLUGIN)
public static SamlConfig getInstance() {
if (config == null) {
config = ConfigContext.getConfigInstance(SamlConfig.class);
}
return config;
}
public String getSpUrl() {
return spUrl.get();
}
public void setSpUrl(String spUrl) {
this.spUrl.set(spUrl);
}
public String getIdpEntityId() {
return idpEntityId.get();
}
public void setIdpEntityId(String idpEntityId) {
this.idpEntityId.set(idpEntityId);
}
}

200
src/main/java/com/fr/plugin/xxxx/saml/request/LoginFilter.java

@ -0,0 +1,200 @@
/*
* Copyright (C), 2018-2021
* Project: starter
* FileName: LoginFilter
* Author: Louis
* Date: 2021/3/30 22:09
*/
package com.fr.plugin.xxxx.saml.request;
import com.fanruan.api.decision.login.LoginKit;
import com.fanruan.api.decision.user.UserKit;
import com.fanruan.api.i18n.I18nKit;
import com.fanruan.api.log.LogKit;
import com.fanruan.api.net.NetworkKit;
import com.fanruan.api.util.RenderKit;
import com.fanruan.api.util.StringKit;
import com.fr.decision.fun.impl.AbstractGlobalRequestFilterProvider;
import com.fr.decision.webservice.utils.DecisionServiceConstants;
import com.fr.decision.webservice.v10.login.LoginService;
import com.fr.general.ComparatorUtils;
import com.fr.plugin.context.PluginContexts;
import com.fr.plugin.xxxx.saml.config.SamlConfig;
import com.fr.plugin.xxxx.saml.util.RSA;
import com.fr.stable.CodeUtils;
import com.fr.stable.fun.Authorize;
import com.fr.third.org.apache.http.NameValuePair;
import com.fr.third.org.apache.http.client.utils.URIBuilder;
import com.fr.web.utils.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URISyntaxException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.fr.plugin.xxxx.saml.config.SamlConfig.*;
/**
* <Function Description><br>
* <LoginFilter>
*
* @author fr.open
* @since 1.0.0
*/
@Authorize(callSignKey = PLUGIN_ID)
public class LoginFilter extends AbstractGlobalRequestFilterProvider {
public static final String STATE = "state";
public static final String SAML_STATE = "saml";
public static final int EXPIRE_TIME = 3000;
private SamlConfig config;
/**
* 过滤器名称
*
* @return
*/
@Override
public String filterName() {
return "SamlFilter";
}
/**
* 过滤规则
*
* @return
*/
@Override
public String[] urlPatterns() {
return new String[]{"/decision", "/decision/view/form", "/decision/view/report", "/decision/v10/entry/access/*"};
}
/**
* 过滤器初始化
*
* @param filterConfig
*/
@Override
public void init(FilterConfig filterConfig) {
this.config = SamlConfig.getInstance();
super.init(filterConfig);
}
/**
* 过滤器处理
*
* @param request
* @param response
* @param filterChain
*/
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
try {
if (operation(request, response)) {
filterChain.doFilter(request, response);
}
} catch (Exception e) {
LogKit.error(e.getMessage(), e);
}
}
/**
* 用户验证登陆操作
*
* @param req
* @param res
* @throws Exception
*/
private boolean operation(HttpServletRequest req, HttpServletResponse res) throws Exception {
String state = NetworkKit.getHTTPRequestParameter(req, STATE);
String token = NetworkKit.getHTTPRequestParameter(req, SP_TOKEN);
// 已登录
if (LoginService.getInstance().isLogged(req)) {
return true;
}
if (StringKit.isEmpty(token)) {
redirectToLoginPage(req, res);
return false;
}
if (StringKit.isBlank(state) || !StringKit.equals(state, SAML_STATE)) {
return true;
}
String username = getUsername(token);
if (StringKit.isEmpty(username) || !UserKit.existUsername(username)) {
return true;
}
if (!PluginContexts.currentContext().isAvailable()) {
LogKit.error(I18nKit.getLocText("Plugin-Saml_Licence_Expired"));
return true;
}
String tokenFR = LoginKit.login(req, res, username);
req.setAttribute(DecisionServiceConstants.FINE_AUTH_TOKEN_NAME, tokenFR);
res.sendRedirect(removeToken(req));
return false;
}
/**
* 移除SAMl的token参数
*
* @param request
* @return
* @throws URISyntaxException
*/
private String removeToken(HttpServletRequest request) throws URISyntaxException {
URIBuilder uriBuilder = new URIBuilder(WebUtils.getOriginalURL(request).replaceAll(" ", "%2B"));
List<NameValuePair> params = uriBuilder.getQueryParams();
params.removeIf(pair -> ComparatorUtils.equals(pair.getName(), SP_TOKEN));
params.removeIf(pair -> ComparatorUtils.equals(pair.getName(), STATE));
uriBuilder.clearParameters();
if (!params.isEmpty()) {
uriBuilder.setParameters(params);
}
return uriBuilder.build().toString();
}
/**
* 跳转登陆地址
*
* @param res
* @return
* @throws Exception
*/
private void redirectToLoginPage(HttpServletRequest req, HttpServletResponse res) throws Exception {
Map<String, Object> params = new HashMap<>();
params.put("spUrl", this.config.getSpUrl());
params.put("idpEntityId", CodeUtils.encodeURIComponent(this.config.getIdpEntityId()));
params.put("redirectUri", CodeUtils.encodeURIComponent(WebUtils.getOriginalURL(req)));
String loginUrl = RenderKit.renderParameter4Tpl(URL_SP, params);
LogKit.info("saml-LoginFilter-redirectToLoginPage-loginUrl:{}", loginUrl);
res.sendRedirect(loginUrl);
}
/**
* 通过code获得用户名
* 返回内容格式时间戳#用户主体名称
*
* @param token
* @return
* @throws Exception
*/
private String getUsername(String token) throws Exception {
LogKit.info("saml-LoginFilter-operation-token:{}", token);
String decryptData = RSA.privateDecrypt(token, RSA.getPrivateKey(PRIVATE_KEY));
LogKit.info("saml-LoginFilter-operation-decryptData:{}", decryptData);
long timeStamp = Long.parseLong(decryptData.substring(0, decryptData.indexOf("#")));
String username = decryptData.substring(decryptData.indexOf("#") + 1);
if (System.currentTimeMillis() - timeStamp > EXPIRE_TIME) {
LogKit.error("saml-LoginFilter-getUsername-TimeStamp validation failed.");
return StringKit.EMPTY;
}
LogKit.info("saml-LoginFilter-getUsername-username:{}", username);
return username;
}
}

196
src/main/java/com/fr/plugin/xxxx/saml/util/RSA.java

@ -0,0 +1,196 @@
/*
* Copyright (C), 2018-2020
* Project: starter
* FileName: RSA
* Author: Louis
* Date: 2020/12/28 14:42
*/
package com.fr.plugin.xxxx.saml.util;
import com.fr.third.org.apache.commons.codec.binary.Base64;
import com.fr.third.org.apache.commons.io.IOUtils;
import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.security.*;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* <Function Description><br>
* <RSA>
*
* @author fr.open
* @since 1.0.0
*/
public class RSA {
public static final String CHARSET = "UTF-8";
public static final String RSA_ALGORITHM = "RSA"; // ALGORITHM 算法的意思
public static Map<String, String> createKeys(int keySize) {
// 为RSA算法创建一个KeyPairGenerator对象
KeyPairGenerator kpg;
try {
kpg = KeyPairGenerator.getInstance(RSA_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("No such algorithm-->[" + RSA_ALGORITHM + "]");
}
// 初始化KeyPairGenerator对象,密钥长度
kpg.initialize(keySize);
// 生成密匙对
KeyPair keyPair = kpg.generateKeyPair();
// 得到公钥
Key publicKey = keyPair.getPublic();
String publicKeyStr = Base64.encodeBase64URLSafeString(publicKey.getEncoded());
// 得到私钥
Key privateKey = keyPair.getPrivate();
String privateKeyStr = Base64.encodeBase64URLSafeString(privateKey.getEncoded());
// map装载公钥和私钥
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKey", publicKeyStr);
keyPairMap.put("privateKey", privateKeyStr);
// 返回map
return keyPairMap;
}
/**
* 得到公钥
*
* @param publicKey 密钥字符串经过base64编码
* @throws Exception
*/
public static RSAPublicKey getPublicKey(String publicKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过X509编码的Key指令获得公钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
return key;
}
/**
* 得到私钥
*
* @param privateKey 密钥字符串经过base64编码
* @throws Exception
*/
public static RSAPrivateKey getPrivateKey(String privateKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
// 通过PKCS#8编码的Key指令获得私钥对象
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
return key;
}
/**
* 公钥加密
*
* @param data
* @param publicKey
* @return
*/
public static String publicEncrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), publicKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥解密
*
* @param data
* @param privateKey
* @return
*/
public static String privateDecrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), privateKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 私钥加密
*
* @param data
* @param privateKey
* @return
*/
public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
//每个Cipher初始化方法使用一个模式参数opmod,并用此模式初始化Cipher对象。此外还有其他参数,包括密钥key、包含密钥的证书certificate、算法参数params和随机源random。
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return Base64.encodeBase64URLSafeString(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, data.getBytes(CHARSET), privateKey.getModulus().bitLength()));
} catch (Exception e) {
throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
}
}
/**
* 公钥解密
*
* @param data
* @param publicKey
* @return
*/
public static String publicDecrypt(String data, RSAPublicKey publicKey) {
try {
Cipher cipher = Cipher.getInstance(RSA_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, Base64.decodeBase64(data), publicKey.getModulus().bitLength()), CHARSET);
} catch (Exception e) {
throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
}
}
//rsa切割解码 , ENCRYPT_MODE,加密数据 ,DECRYPT_MODE,解密数据
private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
int maxBlock = 0; //最大块
if (opmode == Cipher.DECRYPT_MODE) {
maxBlock = keySize / 8;
} else {
maxBlock = keySize / 8 - 11;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] buff;
int i = 0;
try {
while (datas.length > offSet) {
if (datas.length - offSet > maxBlock) {
//可以调用以下的doFinal()方法完成加密或解密数据:
buff = cipher.doFinal(datas, offSet, maxBlock);
} else {
buff = cipher.doFinal(datas, offSet, datas.length - offSet);
}
out.write(buff, 0, buff.length);
i++;
offSet = i * maxBlock;
}
} catch (Exception e) {
throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
}
byte[] resultDatas = out.toByteArray();
IOUtils.closeQuietly(out);
return resultDatas;
}
}

7
src/main/resources/com/fr/plugin/xxxx/saml/locale/lang.properties

@ -0,0 +1,7 @@
Plugin-Saml=SAML Plugin
Plugin-Saml_Group=SAML Plugin
Plugin-Saml_Config_SpUrl=SP URL
Plugin-Saml_Config_SpUrl_Description=SP URL
Plugin-Saml_Config_IdpEntityId=IDP EntityId
Plugin-Saml_Config_IdpEntityId_Description=IDP EntityId
Plugin-Saml_Licence_Expired=SAML Plugin Licence Expired

7
src/main/resources/com/fr/plugin/xxxx/saml/locale/lang_zh_CN.properties

@ -0,0 +1,7 @@
Plugin-Saml=SAML\u767B\u9646\u96C6\u6210\u63D2\u4EF6
Plugin-Saml_Group=SAML\u767B\u9646\u96C6\u6210\u63D2\u4EF6
Plugin-Saml_Config_SpUrl=SP URL
Plugin-Saml_Config_SpUrl_Description=SP URL
Plugin-Saml_Config_IdpEntityId=IDP EntityId
Plugin-Saml_Config_IdpEntityId_Description=IDP EntityId
Plugin-Saml_Licence_Expired=SAML\u767B\u9646\u96C6\u6210\u63D2\u4EF6\u8BB8\u53EF\u8FC7\u671F

BIN
基于SAML协议与Azure AD集成实现fr单点登录.docx

Binary file not shown.
Loading…
Cancel
Save