BAEL-5251: Spring Boot with Swagger UI and Keycloak-Integration (#11632)

This commit is contained in:
Ralf Ueberfuhr 2022-01-05 02:58:21 +01:00 committed by GitHub
parent da600ee93a
commit 6bcc01f60a
14 changed files with 455 additions and 1 deletions

View File

@ -70,6 +70,7 @@
<module>spring-boot-springdoc</module>
<module>spring-boot-swagger</module>
<module>spring-boot-swagger-jwt</module>
<module>spring-boot-swagger-keycloak</module>
<module>spring-boot-testing</module>
<module>spring-boot-testing-2</module>
<module>spring-boot-vue</module>
@ -97,4 +98,4 @@
</dependencies>
</dependencyManagement>
</project>
</project>

View File

@ -0,0 +1,67 @@
<?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>
<artifactId>spring-boot-swagger-keycloak</artifactId>
<version>0.1.0-SNAPSHOT</version>
<name>spring-boot-swagger-keycloak</name>
<packaging>jar</packaging>
<description>Module For Spring Boot Swagger UI with Keycloak</description>
<parent>
<groupId>com.baeldung.spring-boot-modules</groupId>
<artifactId>spring-boot-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.keycloak.bom</groupId>
<artifactId>keycloak-adapter-bom</artifactId>
<version>${keycloak.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox.version}</version>
</dependency>
<!-- Authentication with with Keycloak -->
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-spring-boot-starter</artifactId>
</dependency>
<!-- Authorization with MethodSecurity (@Secured) - optional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<properties>
<spring-boot.version>2.4.5</spring-boot.version>
<springfox.version>3.0.0</springfox.version>
<keycloak.version>15.0.2</keycloak.version>
</properties>
</project>

View File

@ -0,0 +1,59 @@
package com.baeldung.swaggerkeycloak;
import org.keycloak.adapters.springsecurity.KeycloakConfiguration;
import org.keycloak.adapters.springsecurity.authentication.KeycloakAuthenticationProvider;
import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
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.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.SimpleAuthorityMapper;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
@KeycloakConfiguration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalSecurityConfig extends KeycloakWebSecurityConfigurerAdapter {
@Override
protected SessionAuthenticationStrategy sessionAuthenticationStrategy() {
return new RegisterSessionAuthenticationStrategy(new SessionRegistryImpl());
}
// otherwise, we'll get an error 'permitAll only works with HttpSecurity.authorizeRequests()'
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.csrf().disable()
.authorizeRequests()
// we can set up authorization here alternatively to @Secured methods
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers("/api/**").authenticated()
// force authentication for all requests (and use global method security)
.anyRequest().permitAll();
}
/*
* re-configure Spring Security to use
* registers the KeycloakAuthenticationProvider with the authentication manager
*/
@Autowired
void configureGlobal(AuthenticationManagerBuilder auth) {
KeycloakAuthenticationProvider provider = keycloakAuthenticationProvider();
provider.setGrantedAuthoritiesMapper(authoritiesMapper());
auth.authenticationProvider(provider);
}
GrantedAuthoritiesMapper authoritiesMapper() {
SimpleAuthorityMapper mapper = new SimpleAuthorityMapper();
mapper.setPrefix("ROLE_"); // Spring Security adds a prefix to the authority/role names (we use the default here)
mapper.setConvertToUpperCase(true); // convert names to uppercase
mapper.setDefaultAuthority("ROLE_ANONYMOUS"); // set a default authority
return mapper;
}
}

View File

@ -0,0 +1,23 @@
package com.baeldung.swaggerkeycloak;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.springboot.KeycloakSpringBootConfigResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KeycloakConfigResolverConfig {
/*
* re-configure keycloak adapter for Spring Boot environment,
* i.e. to read config from application.yml
* (otherwise, we need a keycloak.json file)
*/
@Bean
public KeycloakConfigResolver configResolver() {
return new KeycloakSpringBootConfigResolver();
}
}

View File

@ -0,0 +1,83 @@
package com.baeldung.swaggerkeycloak;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import springfox.documentation.builders.OAuth2SchemeBuilder;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Configuration
public class OpenAPISecurityConfig {
@Value("${keycloak.auth-server-url}")
String authServerUrl;
@Value("${keycloak.realm}")
String realm;
@Value("${keycloak.resource}")
private String clientId;
@Value("${keycloak.credentials.secret}")
private String clientSecret;
@Autowired
void addSecurity(Docket docket) {
docket
.securitySchemes(Collections.singletonList(authenticationScheme()))
.securityContexts(Collections.singletonList(securityContext()));
}
private SecurityScheme authenticationScheme() {
return new OAuth2SchemeBuilder("implicit")
.name("my_oAuth_security_schema")
.authorizationUrl(authServerUrl + "/realms/" + realm)
.scopes(authorizationScopes())
.build();
}
private List<AuthorizationScope> authorizationScopes() {
return Arrays.asList(
new AuthorizationScope("read_access", "read data"),
new AuthorizationScope("write_access", "modify data")
);
}
private SecurityContext securityContext() {
return SecurityContext.
builder().
securityReferences(readAccessAuth())
.operationSelector(operationContext -> HttpMethod.GET.equals(operationContext.httpMethod()))
.build();
}
private List<SecurityReference> readAccessAuth() {
AuthorizationScope[] authorizationScopes = new AuthorizationScope[] { authorizationScopes().get(0) };
return Collections.singletonList(
new SecurityReference("my_oAuth_security_schema", authorizationScopes)
);
}
@Bean
public SecurityConfiguration security() {
return SecurityConfigurationBuilder.builder()
.clientId(clientId)
.clientSecret(clientSecret)
.realm(realm)
.appName(clientId)
.scopeSeparator(",")
.additionalQueryStringParams(null)
.useBasicAuthenticationWithAccessCodeGrant(false)
.build();
}
}

View File

@ -0,0 +1,36 @@
package com.baeldung.swaggerkeycloak;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import static springfox.documentation.builders.RequestHandlerSelectors.basePackage;
@EnableOpenApi
@Configuration
class SwaggerUIConfig {
@Bean
Docket api() {
return new Docket(DocumentationType.OAS_30)
.useDefaultResponseMessages(false)
.select()
.apis(basePackage(TodosApplication.class.getPackage().getName()))
.paths(PathSelectors.any())
.build()
.apiInfo(apiInfo());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder().title("Todos Management Service")
.description("A service providing todos.")
.version("1.0")
.build();
}
}

View File

@ -0,0 +1,44 @@
package com.baeldung.swaggerkeycloak;
import java.time.LocalDate;
public class Todo {
private Long id;
private String title;
private LocalDate dueDate;
public Todo() {
}
public Todo(Long id, String title, LocalDate dueDate) {
this.id = id;
this.title = title;
this.dueDate = dueDate;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public LocalDate getDueDate() {
return dueDate;
}
public void setDueDate(LocalDate dueDate) {
this.dueDate = dueDate;
}
}

View File

@ -0,0 +1,13 @@
package com.baeldung.swaggerkeycloak;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class TodosApplication {
public static void main(String[] args) {
SpringApplication.run(TodosApplication.class, args);
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.swaggerkeycloak;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.PostConstruct;
import java.time.LocalDate;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/todos")
public class TodosController {
private final Map<Long, Todo> todos = new HashMap<>();
@PostConstruct
public void initData() {
todos.put(1L, new Todo(1L, "Install Keycloak", LocalDate.now().plusDays(14)));
todos.put(2L, new Todo(2L, "Configure realm", LocalDate.now().plusDays(21)));
}
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation("Read all todos")
@ApiResponses({
@ApiResponse(code = 200, message = "The todos were found and returned.")
})
@PreAuthorize("hasAuthority('SCOPE_read_access')")
public Collection<Todo> readAll() {
return todos.values();
}
}

View File

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en-US">
<head>
<title>Swagger UI: OAuth2 Redirect</title>
</head>
<body>
<script>
'use strict';
function run () {
var oauth2 = window.opener.swaggerUIRedirectOauth2;
var sentState = oauth2.state;
var redirectUrl = oauth2.redirectUrl;
var isValid, qp, arr;
if (/code|token|error/.test(window.location.hash)) {
qp = window.location.hash.substring(1);
} else {
qp = location.search.substring(1);
}
arr = qp.split("&");
arr.forEach(function (v,i,_arr) { _arr[i] = '"' + v.replace('=', '":"') + '"';});
qp = qp ? JSON.parse('{' + arr.join() + '}',
function (key, value) {
return key === "" ? value : decodeURIComponent(value);
}
) : {};
isValid = qp.state === sentState;
if ((
oauth2.auth.schema.get("flow") === "accessCode" ||
oauth2.auth.schema.get("flow") === "authorizationCode" ||
oauth2.auth.schema.get("flow") === "authorization_code"
) && !oauth2.auth.code) {
if (!isValid) {
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "warning",
message: "Authorization may be unsafe, passed state was changed in server Passed state wasn't returned from auth server"
});
}
if (qp.code) {
delete oauth2.state;
oauth2.auth.code = qp.code;
oauth2.callback({auth: oauth2.auth, redirectUrl: redirectUrl});
} else {
let oauthErrorMsg;
if (qp.error) {
oauthErrorMsg = "["+qp.error+"]: " +
(qp.error_description ? qp.error_description+ ". " : "no accessCode received from the server. ") +
(qp.error_uri ? "More info: "+qp.error_uri : "");
}
oauth2.errCb({
authId: oauth2.auth.name,
source: "auth",
level: "error",
message: oauthErrorMsg || "[Authorization failed]: no accessCode received from the server"
});
}
} else {
oauth2.callback({auth: oauth2.auth, token: qp, isValid: isValid, redirectUrl: redirectUrl});
}
window.close();
}
window.addEventListener('DOMContentLoaded', function () {
run();
});
</script>
</body>
</html>

View File

@ -0,0 +1,9 @@
keycloak:
auth-server-url: https://api.example.com/auth # Keycloak server url
realm: todos-service-realm # Keycloak Realm
resource: todos-service-clients # Keycloak Client
public-client: true
principal-attribute: preferred_username
ssl-required: external
credentials:
secret: 00000000-0000-0000-0000-000000000000