package edu.remad.tutoring2.security.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity(debug = true)
@EnableMethodSecurity
public class SpringSecurityConfig {
@Value("${spring.websecurity.debug:true}")
boolean webSecurityDebug;
@Bean
WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.debug(webSecurityDebug);
}
@Bean
PasswordEncoder passwordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
}
SecurityFilterConfig.java:
package edu.remad.tutoring2.security.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.logout.HeaderWriterLogoutHandler;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter;
import org.springframework.security.web.header.writers.ClearSiteDataHeaderWriter.Directive;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import edu.remad.tutoring2.jwt.Tutoring2CustomJwtAuthenticationConverter;
import edu.remad.tutoring2.security.ContentSecurityPolicySettings;
import edu.remad.tutoring2.security.filters.DebugLoggingFilter;
import edu.remad.tutoring2.security.filters.HttpHeadersFilter;
import edu.remad.tutoring2.security.filters.TenantFilter;
@Configuration
public class SecurityFilterChainsConfig {
private static final ClearSiteDataHeaderWriter.Directive[] COOKIES = Directive.values();
@Autowired
private ContentSecurityPolicySettings contentSecurityPolicies;
@Autowired
private Tutoring2CustomJwtAuthenticationConverter jwtAuthConverter;
/**
* Does form login filter chain and has also http security.
*
* @param http similar to spring security xml config for filtering request
* @return created security filter chain, {@link SecurityFilterChain}
* @throws Exception
*/
@Bean
@Order(1)
SecurityFilterChain formloginSecurityFilterChain(HttpSecurity http) throws Exception {
http.cors().and().headers(headers -> headers.xssProtection().and()
.contentSecurityPolicy(contentSecurityPolicies.getContentSecurityPolicies()));
http.addFilterAfter(new TenantFilter(), BasicAuthenticationFilter.class)
.addFilterAfter(new HttpHeadersFilter(), HeaderWriterFilter.class)
.addFilterAfter(new DebugLoggingFilter(), HttpHeadersFilter.class)
.securityContext((securityContext) -> securityContext.requireExplicitSave(true))
.sessionManagement(
session -> session.maximumSessions(1).maxSessionsPreventsLogin(true).expiredUrl("/login"))
.authorizeRequests(requests -> requests.antMatchers("/", "/helloWorld", "/logoutSuccess", "/signup", "/api/v1/csrf")
.permitAll().antMatchers("/hello", "/bye", "/login", "/logout", "/templates/**").authenticated())
.formLogin(login -> login.loginPage("/myCustomLogin").loginProcessingUrl("/process-login")
.defaultSuccessUrl("/hello", true)).csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/logoutSuccess")
.addLogoutHandler(new HeaderWriterLogoutHandler(new ClearSiteDataHeaderWriter(COOKIES))));
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain oauth2rescourceserverSecurityFilterChain(HttpSecurity http) throws Exception {
return http.securityMatcher(AntPathRequestMatcher.antMatcher("/v2/**"))
.authorizeHttpRequests(requests -> requests.anyRequest().authenticated()).csrf(csrf -> csrf.disable())
.oauth2ResourceServer(server -> server.jwt().jwtAuthenticationConverter(jwtAuthConverter))
.sessionManagement(management -> management.sessionCreationPolicy(SessionCreationPolicy.STATELESS)).build();
}
}
oauth2ResourcServerConfig.java:
package edu.remad.tutoring2.security.config;
import java.net.MalformedURLException;
import java.net.URL;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import com.nimbusds.jose.KeySourceException;
import com.nimbusds.jose.proc.JWSAlgorithmFamilyJWSKeySelector;
import com.nimbusds.jose.proc.JWSKeySelector;
import com.nimbusds.jose.proc.SecurityContext;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
@Configuration
public class Oauth2ResourcServerConfig {
private String keySetUri = "http://192.168.120.59:8080/realms/ConnectTrial/protocol/openid-connect/certs";
@Bean
JwtDecoder jwtDecoder() throws KeySourceException, MalformedURLException {
JWSKeySelector<SecurityContext> jwsKeySelector =
JWSAlgorithmFamilyJWSKeySelector.fromJWKSetURL(new URL(keySetUri));
DefaultJWTProcessor<SecurityContext> jwtProcessor =
new DefaultJWTProcessor<>();
jwtProcessor.setJWSKeySelector(jwsKeySelector);
return new NimbusJwtDecoder(jwtProcessor);
}
}
Tutoring2customjwtauthenticationconverter.java:
package edu.remad.tutoring2.jwt;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CLAIM_RESSOURCE_ACCESS;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_CONVERTER_RESOURCE_ID;
import static edu.remad.tutoring2.appconstants.JwtAppConstants.JWT_ROLES_KEY;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.convert.converter.Converter;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.stereotype.Component;
/**
* Converts roles from Keycloak to Spring Security roles. It reads JWT and fetches all claims and roles as roles.
*/
@Component
public class Tutoring2CustomJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
private final JwtGrantedAuthoritiesConverter jwtGrantedAuthoritiesConverter;
/**
* Default Constructor
*/
public Tutoring2CustomJwtAuthenticationConverter() {
jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
}
@Override
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
Collection<GrantedAuthority> authorities = Stream
.concat(jwtGrantedAuthoritiesConverter.convert(jwt).stream(), extractJwtResourceRoles(jwt).stream())
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities, getPrincipalClaimName(jwt));
}
private Collection<? extends GrantedAuthority> extractJwtResourceRoles(Jwt jwt) {
if (jwt.getClaimAsMap(JWT_CLAIM_RESSOURCE_ACCESS) == null) {
return Set.of();
}
Map<String, Object> resourceAccess = jwt.getClaim("resource_access");
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
if (resourceAccess.get(JWT_CONVERTER_RESOURCE_ID) == null) {
return Set.of();
}
Map<String, Object> resource = (Map<String, Object>) resourceAccess.get(JWT_CONVERTER_RESOURCE_ID);
Collection<String> resourceRoles = (Collection<String>) resource.get(JWT_ROLES_KEY);
return resourceRoles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.collect(Collectors.toSet());
}
private String getPrincipalClaimName(Jwt jwt) {
String claimName = JwtClaimNames.SUB;
if (JWT_CONVERTER_PRINCIPAL_ATTRIBUTE != null) {
claimName = JWT_CONVERTER_PRINCIPAL_ATTRIBUTE;
}
return jwt.getClaim(claimName);
}
}
pom.xml:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- ###################### -->
<!-- start of project setup -->
<!-- ###################### -->
<modelVersion>4.0.0</modelVersion>
<groupId>edu.remad</groupId>
<artifactId>tutoring2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>Tutoring 2 Maven Webapp</name>
<url>http://maven.apache.org</url>
<!-- #################### -->
<!-- end of project setup -->
<!-- #################### -->
<!-- ################### -->
<!-- start of properties -->
<!-- ################### -->
<properties>
<maven.compiler.target>11</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<encoding>UTF-8</encoding>
<spring.version>5.3.39</spring.version>
<spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.14</spring.boot.version>
<junit5.version>5.10.0</junit5.version>
<lombok.version>1.18.30</lombok.version>
<spring.oauth2.resourceserver.version>2.7.14</spring.oauth2.resourceserver.version>
<version>3.3.2</version>
</properties>
<!-- ################# -->
<!-- end of properties -->
<!-- ################# -->
<!-- ##################### -->
<!-- start of dependencies -->
<!-- ##################### -->
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-beans -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- spring security needed deoendencies -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
<version>5.8.16</version>
</dependency>
<!-- Spring framework -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>5.6.9.Final</version>
</dependency>
<!-- Hibernate Validator -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.4.3.Final</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- CGLib for @Configuration -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
<scope>runtime</scope>
</dependency>
<!-- Servlet Spec -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
<!--<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>5.0.0</version>
</dependency>-->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<!-- Maven Plugins section -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
</dependency>
<!-- JAXB, for version 3 use Jakarta -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.8</version>
</dependency>
<dependency>
<groupId>org.webjars</groupId>
<artifactId>bootstrap</artifactId>
<version>5.3.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.16.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.0</version>
</dependency>
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- resource-server -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.oauth2.resourceserver.version}</version>
</dependency>
<!-- start of own implementations -->
<dependency>
<groupId>edu.remad</groupId>
<artifactId>ical4j-builder</artifactId>
<version>1.0.6-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>edu.remad</groupId>
<artifactId>pdf-toolboxing</artifactId>
<version>0.30.1-SNAPSHOT</version>
</dependency>
<!-- end of own implementation -->
</dependencies>
<!-- ################### -->
<!-- end of dependencies -->
<!-- ################### -->
<!-- ################# -->
<!-- start of profiles -->
<!-- ################# -->
<profiles>
<profile>
<id>development</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withoutTests</id>
<properties>
<maven.test.skip>true</maven.test.skip>
</properties>
</profile>
<profile>
<id>withIntegrationTests</id>
<properties>
</properties>
</profile>
</profiles>
<!-- ############### -->
<!-- end of profiles -->
<!-- ############### -->
<!-- ###################### -->
<!-- start of build section -->
<!-- ###################### -->
<build>
<finalName>tutoring2</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.target}</source>
<target>${maven.compiler.source}</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.1</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit5.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<!-- #################### -->
<!-- end of build section -->
<!-- #################### -->
</project>
开关
@Order
@Order
中尝试了安全过滤器链(在2之前尝试了1个)。使用第一个匹配的请求,而其他请求被丢弃。如果没有
securityMatcher
,则滤清器链匹配所有请求,这是最后一个过滤器链的好选择(默认情况下),但对于第一个请求(如在您的conf中)。
在旁注时,您可以找到我有用的ime starter。它将为您节省维护当局转换器和
oauth2ResourceServer
过滤链的负担。但是请注意,由我的入门者自动配置的资源服务器过滤器链没有securityMatcher
且最低的优先级。因此,除了删除您的oauth2rescourceserverSecurityFilterChain
Tutoring2CustomJwtAuthenticationConverter
外,您还必须为您的securityMatcher
定义一个(具有类似于您必须formloginSecurityFilterChain
的东西)