BAEL-5251: Spring Boot with Swagger UI and Keycloak-Integration (#11632)
This commit is contained in:
@@ -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>
|
||||
|
||||
67
spring-boot-modules/spring-boot-swagger-keycloak/pom.xml
Normal file
67
spring-boot-modules/spring-boot-swagger-keycloak/pom.xml
Normal 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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 665 B |
Binary file not shown.
|
After Width: | Height: | Size: 628 B |
@@ -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>
|
||||
File diff suppressed because one or more lines are too long
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,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
|
||||
Reference in New Issue
Block a user