BAEL-5325: Add Spring Security login/logout API with Springdoc (#13027)

* BAEL-5325: Add Spring Security login/logout API with Springdoc

* Updated Spring version and using annotations for Basic Auth config

* Added tests

* Removed unused dependencies

* Removed dependencies which were accidentally added.

* Removed plugins which were accidentally added.

* Removed property which was accidentally added.
This commit is contained in:
Adrian Bob 2022-12-09 20:35:09 +02:00 committed by GitHub
parent 10d561451a
commit f45b2c8659
18 changed files with 548 additions and 0 deletions

View File

@ -43,6 +43,7 @@
<module>spring-security-web-rest-basic-auth</module> <module>spring-security-web-rest-basic-auth</module>
<module>spring-security-web-rest-custom</module> <module>spring-security-web-rest-custom</module>
<module>spring-security-web-rest</module> <module>spring-security-web-rest</module>
<module>spring-security-web-springdoc</module>
<module>spring-security-web-sockets</module> <module>spring-security-web-sockets</module>
<module>spring-security-web-thymeleaf</module> <module>spring-security-web-thymeleaf</module>
<module>spring-security-web-x509</module> <module>spring-security-web-x509</module>

View File

@ -0,0 +1,15 @@
## Spring Security Web Springdoc
This module contains articles about Springdoc with Spring Security
### Relevant Articles:
- [Documenting a Spring REST API Using OpenAPI 3.0](https://www.baeldung.com/spring-rest-openapi-documentation)
- [Configure JWT Authentication for OpenAPI](https://www.baeldung.com/openapi-jwt-authentication)
### Running This Project:
To run the projects use the commands:
- `mvn spring-boot:run -Dstart-class=com.baeldung.basicauth.SpringBootSpringdocBasicAuth`
- `mvn spring-boot:run -Dstart-class=com.baeldung.formlogin.SpringBootSpringdocFormLogin`

View File

@ -0,0 +1,54 @@
<?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-security-web-springdoc</artifactId>
<version>0.1-SNAPSHOT</version>
<name>spring-security-web-springdoc</name>
<packaging>jar</packaging>
<description>Spring Security with Springdoc tutorial</description>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-boot-2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<relativePath>../../parent-boot-2</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-security</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<springdoc.version>1.6.13</springdoc.version>
</properties>
</project>

View File

@ -0,0 +1,60 @@
package com.baeldung.basicauth;
import java.io.Serializable;
public class Foo implements Serializable {
private static final long serialVersionUID = -5422285893276747592L;
private long id;
private String name;
public Foo(final String name) {
super();
this.name = name;
}
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Foo other = (Foo) obj;
if (name == null) {
return other.name == null;
} else return name.equals(other.name);
}
@Override
public String toString() {
return "Foo [name=" + name + "]";
}
}

View File

@ -0,0 +1,39 @@
package com.baeldung.basicauth;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
import com.google.common.collect.Lists;
@RestController
@OpenAPIDefinition(info = @Info(title = "Foos API", version = "v1"))
@SecurityRequirement(name = "basicAuth")
@RequestMapping(value = "foos", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE)
public class FooController {
private static final int STRING_LENGTH = 6;
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") final Long id) {
return new Foo(randomAlphabetic(STRING_LENGTH));
}
@GetMapping
public List<Foo> findAll() {
return Lists.newArrayList(new Foo(randomAlphabetic(STRING_LENGTH)), new Foo(randomAlphabetic(STRING_LENGTH)), new Foo(randomAlphabetic(STRING_LENGTH)));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Foo create(@RequestBody final Foo foo) {
return foo;
}
}

View File

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

View File

@ -0,0 +1,15 @@
package com.baeldung.basicauth.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,38 @@
package com.baeldung.basicauth.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html").permitAll()
.anyRequest().authenticated()
.and()
.httpBasic();
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, PasswordEncoder passwordEncoder) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder.encode("password"))
.roles("USER");
}
}

View File

@ -0,0 +1,12 @@
package com.baeldung.basicauth.config;
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
import io.swagger.v3.oas.annotations.security.SecurityScheme;
import org.springframework.context.annotation.Configuration;
@Configuration
@SecurityScheme(
type = SecuritySchemeType.HTTP,
name = "basicAuth",
scheme = "basic")
public class SpringdocConfig {}

View File

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

View File

@ -0,0 +1,15 @@
package com.baeldung.formlogin.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,40 @@
package com.baeldung.formlogin.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.defaultSuccessUrl("/foos");
return http.build();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth, PasswordEncoder passwordEncoder) throws Exception {
auth.inMemoryAuthentication()
.withUser("user")
.password(passwordEncoder.encode("password"))
.roles("USER");
}
}

View File

@ -0,0 +1,37 @@
package com.baeldung.formlogin.controller;
import com.baeldung.formlogin.model.Foo;
import com.google.common.collect.Lists;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic;
@RestController
@RequestMapping(value = "foos", produces = MediaType.APPLICATION_JSON_VALUE)
@OpenAPIDefinition(info = @Info(title = "Foos API", version = "v1"))
public class FooController {
private static final int STRING_LENGTH = 6;
@GetMapping(value = "/{id}")
public Foo findById(@PathVariable("id") final Long id) {
return new Foo(randomAlphabetic(STRING_LENGTH));
}
@GetMapping
public List<Foo> findAll() {
return Lists.newArrayList(new Foo(randomAlphabetic(STRING_LENGTH)), new Foo(randomAlphabetic(STRING_LENGTH)), new Foo(randomAlphabetic(STRING_LENGTH)));
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Foo create(@RequestBody final Foo foo) {
return foo;
}
}

View File

@ -0,0 +1,17 @@
package com.baeldung.formlogin.controller;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.web.bind.annotation.*;
@RestController
@OpenAPIDefinition(info = @Info(title = "logout-endpoint"))
public class LogoutController {
@PostMapping("logout")
@Operation(description = "End authenticated user session")
public void logout() {
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,62 @@
package com.baeldung.formlogin.model;
import java.io.Serializable;
public class Foo implements Serializable {
private static final long serialVersionUID = -5422285893276747592L;
private long id;
private String name;
public Foo(final String name) {
this.name = name;
}
public Foo() {
}
public long getId() {
return id;
}
public void setId(final long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final Foo other = (Foo) obj;
if (name == null) {
return other.name == null;
} else return name.equals(other.name);
}
@Override
public String toString() {
return "Foo [name=" + name + "]";
}
}

View File

@ -0,0 +1 @@
springdoc.show-login-endpoint=true

View File

@ -0,0 +1,64 @@
package com.baeldung.basicauth;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OpenAPIIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenInvokeSwagger_thenRenderIndexPage() {
String response = this.restTemplate.getForObject("http://localhost:" + port + "/swagger-ui/index.html", String.class);
assertNotNull(response);
assertTrue(response.contains("Swagger UI"));
assertTrue(response.contains("<div id=\"swagger-ui\"></div>"));
}
@Test
void whenInvokeOpenApi_thenCheckHeaders() {
ResponseEntity<String> response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getHeaders().get("Content-Type"));
assertEquals(1, response.getHeaders().get("Content-Type").size());
assertEquals("application/json", response.getHeaders().get("Content-Type").get(0));
}
@Test
void whenInvokeOpenApi_thenVerifyOpenApiDoc() {
ResponseEntity<String> response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class);
assertNotNull(response);
assertNotNull(response.getBody());
assertTrue(response.getBody().contains("\"openapi\":"));
assertTrue(response.getBody().contains("Foos API"));
assertTrue(response.getBody().contains("\"post\""));
}
@Test
void whenInvokeOpenApi_thenCheckSecurityConfig() {
ResponseEntity<String> response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class);
assertNotNull(response);
assertNotNull(response.getBody());
assertTrue(response.getBody().contains("\"securitySchemes\""));
assertTrue(response.getBody().contains("\"type\":\"http\""));
assertTrue(response.getBody().contains("\"scheme\":\"basic\""));
}
}

View File

@ -0,0 +1,52 @@
package com.baeldung.formlogin;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OpenAPIIntegrationTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void whenInvokeSwagger_thenRenderIndexPage() {
String response = this.restTemplate.getForObject("http://localhost:" + port + "/swagger-ui/index.html", String.class);
assertNotNull(response);
assertTrue(response.contains("Swagger UI"));
assertTrue(response.contains("<div id=\"swagger-ui\"></div>"));
}
@Test
void whenInvokeOpenApi_thenCheckHeaders() {
ResponseEntity<String> response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class);
assertNotNull(response);
assertEquals(HttpStatus.OK, response.getStatusCode());
assertNotNull(response.getHeaders().get("Content-Type"));
assertEquals(1, response.getHeaders().get("Content-Type").size());
assertEquals("application/json", response.getHeaders().get("Content-Type").get(0));
}
@Test
void whenInvokeOpenApi_thenVerifyOpenApiDoc() {
ResponseEntity<String> response = this.restTemplate.getForEntity("http://localhost:" + port + "/v3/api-docs", String.class);
assertNotNull(response);
assertNotNull(response.getBody());
assertTrue(response.getBody().contains("\"openapi\":"));
assertTrue(response.getBody().contains("Foos API"));
assertTrue(response.getBody().contains("\"post\""));
}
}