springmvc-springsecurity-redhat keycloak SAML2 xml实现
环境准备:
jdk17
redhat keycloak 24
spring security 6
参照文档:
红帽KeyCloak:Red Hat build of Keycloak | Red Hat Product Documentation
入门指南:入门指南 | Red Hat Product Documentation
服务器管理指南:服务器管理指南 | Red Hat Product Documentation
Redhat Keycloak:
本地启动:
\rhbk-24.0.7\bin\kc.bat start-dev --http-port 8180
管理控制台的URL:http://localhost:8180/admin
账户控制台的URL:http://localhost:8180/realms/{myrealm}/account
Spring MVC:
<mvc:redirect-view-controller path="/aml01/saml2/sso_login"
redirect-url="/saml2/authenticate/saml-app" />
saml-app:同security中的registration-id
Spring security:
POM引入包:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
</dependency>
<http auto-config="true">
<intercept-url pattern="/**" access="authenticated"/>
<saml2-login
authentication-success-handler-ref="samlAuthenticationSuccessHandler"
/>
<saml2-logout />
</http>
<relying-party-registrations>
<relying-party-registration registration-id="saml-app"
entity-id="saml-app"
assertion-consumer-service-location="http://localhost:8080/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="POST"
single-logout-service-location="http://localhost:8080/logout/saml2/slo"
single-logout-service-response-location="http://localhost:8080/logout/saml2/slo"
asserting-party-id="saml-xml">
<signing-credential certificate-location="classpath:credentials/rp-certificate.crt"
private-key-location="classpath:credentials/rp-private.key"/>
</relying-party-registration>
<asserting-party asserting-party-id="saml-xml"
entity-id="http://localhost:8180/realms/demo"
single-sign-on-service-location="http://localhost:8180/realms/demo/protocol/saml"
single-sign-on-service-binding="POST"
signing-algorithms="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
single-logout-service-location="http://localhost:8180/realms/demo/protocol/saml"
single-logout-service-binding="POST"
single-logout-service-response-location="http://localhost:8180/realms/demo/protocol/saml"
want-authn-requests-signed="true"
>
<verification-credential private-key-location="classpath:credentials/rp-private.key"
certificate-location="classpath:credentials/idp-certificate.crt"/>
<encryption-credential private-key-location="classpath:credentials/rp-private.key"
certificate-location="classpath:credentials/idp-certificate.crt"/>
</asserting-party>
</relying-party-registrations>
这个URL「assertion-consumer-service-location="{baseUrl}/login/saml2/sso/{registrationId}"」和keycloak的client的Valid redirect URIs相同
Valid redirect URIs:localhost:8080/login/saml2/sso/saml-app
证明书和key做成:
openssl req -newkey rsa:2048 -nodes -keyout rp-private.key -x509 -days 365 -out rp-certificate.crt
rp-certificate.crt导入keycloak的Clients -> client details ->keys ->import key
代码:
包结构:
└─pom.xml
│
└─src
├─main
│ ├─java
│ │ └─example
│ │ IndexController.java
│ │ KeyLoader.java
│ │ WebConfiguration.java
│ │
│ ├─resources
│ │ │ logback.xml
│ │ │
│ │ └─credentials
│ │ idp-certificate.crt
│ │ rp-certificate.crt
│ │ rp-private.key
│ │
│ └─webapp
│ ├─META-INF
│ │ MANIFEST.MF
│ │
│ ├─resources
│ │ ├─css
│ │ │ bootstrap-responsive.css
│ │ │ bootstrap.css
│ │ │
│ │ └─img
│ │ favicon.ico
│ │ logo.png
│ │
│ └─WEB-INF
│ │ jboss-web.xml
│ │ spring-servlet.xml
│ │ web.xml
│ │
│ ├─spring
│ │ security.xml
│ │
│ └─templates
│ index.html
│
└─test
└─java
pom:
<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>org.springframework.security</groupId>
<artifactId>keycloak-integration-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<spring.version>6.2.1</spring.version> <!-- Adjust to your Spring BOM version -->
<junit.version>5.10.3</junit.version>
</properties>
<dependencies>
<!-- OpenSAML Dependencies -->
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-api</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>org.opensaml</groupId>
<artifactId>opensaml-saml-impl</artifactId>
<version>4.1.1</version>
</dependency>
<!-- Spring Dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.1.3</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-saml2-service-provider</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Thymeleaf Dependencies -->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
<version>3.1.2.RELEASE</version>
</dependency>
<!-- Servlet API -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- Testing Dependencies -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.1.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.26.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>aml-web</finalName>
<plugins>
<!-- Maven War Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
<!-- Maven Surefire Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<configuration>
<includes>
<include>**/*Tests.java</include>
</includes>
</configuration>
</plugin>
<!-- Other plugins like Gretty and Integrtest would need to be replaced with their Maven equivalents or configured differently -->
</plugins>
</build>
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
<repository>
<id>spring-snapshots</id>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<url>https://repo.spring.io/milestone</url>
</repository>
<repository>
<id>shibboleth-releases</id>
<url>https://build.shibboleth.net/nexus/content/repositories/releases</url>
</repository>
</repositories>
</project>
security.xml:
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">
<http auto-config="true">
<intercept-url pattern="/**" access="authenticated"/>
<saml2-login
authentication-success-handler-ref="samlAuthenticationSuccessHandler"
/>
<saml2-logout />
</http>
<!-- 認証成功した場合画面遷移Handler -->
<b:bean id="samlAuthenticationSuccessHandler"
class="org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler">
<b:property name="targetUrlParameter" value="redirectTo" />
<b:property name="alwaysUseDefaultTargetUrl" value="true" />
<b:property name="defaultTargetUrl" value="/test" />
</b:bean>
<user-service>
<user name="user" password="{noop}password" authorities="ROLE_USER" />
</user-service>
<relying-party-registrations>
<relying-party-registration registration-id="saml-app"
entity-id="saml-app"
assertion-consumer-service-location="http://localhost:8080/login/saml2/sso/{registrationId}"
assertion-consumer-service-binding="POST"
single-logout-service-location="http://localhost:8080/logout/saml2/slo"
single-logout-service-response-location="http://localhost:8080/logout/saml2/slo"
asserting-party-id="saml-xml">
<signing-credential certificate-location="classpath:credentials/rp-certificate.crt"
private-key-location="classpath:credentials/rp-private.key"/>
</relying-party-registration>
<asserting-party asserting-party-id="saml-xml"
entity-id="http://localhost:8180/realms/demo"
single-sign-on-service-location="http://localhost:8180/realms/demo/protocol/saml"
single-sign-on-service-binding="POST"
signing-algorithms="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
single-logout-service-location="http://localhost:8180/realms/demo/protocol/saml"
single-logout-service-binding="POST"
single-logout-service-response-location="http://localhost:8180/realms/demo/protocol/saml"
want-authn-requests-signed="true"
>
<verification-credential private-key-location="classpath:credentials/rp-private.key"
certificate-location="classpath:credentials/idp-certificate.crt"/>
<encryption-credential private-key-location="classpath:credentials/rp-private.key"
certificate-location="classpath:credentials/idp-certificate.crt"/>
</asserting-party>
</relying-party-registrations>
</b:beans>
spring-servlet.xml:
<!--
~ Copyright 2022 the original author or authors.
~
~ 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
~
~ https://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.
-->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="example"/>
</beans>
web.xml:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<!--
- Location of the XML file that defines the root application context
- Applied by ContextLoaderListener.
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*.xml
</param-value>
</context-param>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>spring</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>spring</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
index.html:
<!--
~ Copyright 2022 the original author or authors.
~
~ 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
~
~ https://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.
-->
<!doctype html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
<head>
<title>Spring Security - SAML 2.0 Login & Logout</title>
<meta charset="utf-8" />
<style>
span, dt {
font-weight: bold;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
<div class="container">
<ul class="nav">
<li class="nav-item">
<form th:action="@{/logout}" method="post">
<button class="btn btn-primary" id="rp_logout_button" type="submit">
RP-initiated Logout
</button>
</form>
</li>
</ul>
</div>
<main role="main" class="container">
<h1 class="mt-5">SAML 2.0 Login & Single Logout with Spring Security</h1>
<p class="lead">You are successfully logged in as <span sec:authentication="name"></span></p>
<p class="lead">You're email address is <span th:text="${emailAddress}"></span></p>
<h2 class="mt-2">All Your Attributes</h2>
<dl th:each="userAttribute : ${userAttributes}">
<dt th:text="${userAttribute.key}"></dt>
<dd th:text="${userAttribute.value}"></dd>
</dl>
<h6>Visit the <a href="https://docs.spring.io/spring-security/site/docs/current/reference/html5/#servlet-saml2" target="_blank">SAML 2.0 Login & Logout</a> documentation for more details.</h6>
</main>
</div>
</body>
</html>
logback.xml:
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.springframework.security" level="TRACE"/>
<root level="TRACE">
<appender-ref ref="STDOUT" />
</root>
</configuration>
credentials:
idp-certificate.crt keycloak的idp RSA256 cetificate
rp-certificate.crt 上面生成
rp-private.key 上面生成
WebConfiguration.java:
/*
* Copyright 2022 the original author or authors.
*
* 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
*
* https://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 example;
import java.util.List;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer, ApplicationContextAware {
private ApplicationContext context;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
AuthenticationPrincipalArgumentResolver principalArgumentResolver = new AuthenticationPrincipalArgumentResolver();
principalArgumentResolver
.setBeanResolver(new BeanFactoryResolver(this.context.getAutowireCapableBeanFactory()));
resolvers.add(principalArgumentResolver);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.context);
templateResolver.setPrefix("/WEB-INF/templates/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(false);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
templateEngine.setEnableSpringELCompiler(true);
templateEngine.addDialect(new SpringSecurityDialect());
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
}
IndexController.java:
/*
* Copyright 2022 the original author or authors.
*
* 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
*
* https://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 example;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal;
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class IndexController {
private final RelyingPartyRegistrationRepository repository;
public IndexController(RelyingPartyRegistrationRepository repository) {
this.repository = repository;
}
@GetMapping("/test")
public String index(Model model, @AuthenticationPrincipal Saml2AuthenticatedPrincipal principal) {
String emailAddress = principal.getFirstAttribute("email");
model.addAttribute("emailAddress", emailAddress);
model.addAttribute("userAttributes", principal.getAttributes());
return "index";
}
}
访问地址:
localhost:8080/test