LAPTOP-SB56SG4Q\86185
3 years ago
45 changed files with 2373 additions and 1 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +1,6 @@
|
||||
# open-JSD-9373 |
||||
|
||||
JSD-9373 SAML+Azure AD单点登录 |
||||
JSD-9373 SAML+Azure AD单点登录\ |
||||
免责说明:该源码为第三方爱好者提供,不保证源码和方案的可靠性,也不提供任何形式的源码教学指导和协助!\ |
||||
仅作为开发者学习参考使用!禁止用于任何商业用途!\ |
||||
为保护开发者隐私,开发者信息已隐去!若原开发者希望公开自己的信息,可联系hugh处理。 |
@ -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/> <!– lookup parent from repository –>--> |
||||
</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> |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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(); |
||||
} |
||||
|
||||
} |
@ -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)); |
||||
} |
||||
} |
@ -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"; |
||||
} |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -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); |
||||
} |
||||
|
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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); |
||||
|
||||
} |
||||
} |
@ -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 {} |
@ -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 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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; |
||||
} |
After Width: | Height: | Size: 148 KiB |
After Width: | Height: | Size: 89 KiB |
After Width: | Height: | Size: 106 KiB |
After Width: | Height: | Size: 14 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -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 — 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 — 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> |
@ -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> |
@ -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> |
@ -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> |
Binary file not shown.
@ -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> |
@ -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; |
||||
} |
||||
} |
@ -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) { |
||||
|
||||
} |
||||
} |
@ -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); |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
} |
@ -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; |
||||
} |
||||
|
||||
} |
@ -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 |
@ -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 |
Binary file not shown.
Loading…
Reference in new issue