I am developing demo REST service using Spring Boot
where user has to login in order to to perform certain subset of operations. After adding Swagger UI
(using springfox
library) with that simple configuration:
@Bean
public Docket docApi() {
return new Docket(DocumentationType.SWAGGER_2)
.select()
.apis(any())
.paths(PathSelectors.ant("/api/**"))
.build()
.pathMapping("/")
.apiInfo(apiInfo())
.directModelSubstitute(LocalDate.class, String.class)
.useDefaultResponseMessages(true)
.enableUrlTemplating(true);
}
I end up with all apis with all operations listed on Swagger UI
page. Unfortunately I don't have login/logout endpoints listed among them.
The problem is that part of that operations cannot be performed via Swagger UI
built-in form (I find it really nice feature and would like make it work), because user is not logged in. Is there any solution to that problem? Can I define manually some endpoints in Swagger
?
If there was a form to submit credentials (i.e. login/logout endpoints) I could perform authorization before using that secured endpoints. Then, Swagger
user could extract token/sessionid
from response and paste it to custom query parameter defined via @ApiImplicitParams
.
Below you can find my security configuration:
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginProcessingUrl("/api/login")
.usernameParameter("username")
.passwordParameter("password")
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(new CustomLogoutSuccessHandler())
.deleteCookies("JSESSIONID")
.permitAll()
.and()
.csrf()
.disable()
.exceptionHandling()
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.and()
.authorizeRequests()
.and()
.headers()
.frameOptions()
.disable();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
A bit late for the party, but since SpringFox relies on Spring beans for building the documentation, we can easily manipulate it. Hope this can help someone!
Register it as a bean
@Primary
@Bean
public ApiListingScanner addExtraOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager)
{
return new FormLoginOperations(apiDescriptionReader, apiModelReader, pluginsManager);
}
The class used to add any operation manually:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.collect.Multimap;
import springfox.documentation.builders.ApiListingBuilder;
import springfox.documentation.builders.OperationBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiDescription;
import springfox.documentation.service.ApiListing;
import springfox.documentation.service.Operation;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;
import springfox.documentation.spring.web.readers.operation.CachingOperationNameGenerator;
import springfox.documentation.spring.web.scanners.ApiDescriptionReader;
import springfox.documentation.spring.web.scanners.ApiListingScanner;
import springfox.documentation.spring.web.scanners.ApiListingScanningContext;
import springfox.documentation.spring.web.scanners.ApiModelReader;
public class FormLoginOperations extends ApiListingScanner
{
@Autowired
private TypeResolver typeResolver;
@Autowired
public FormLoginOperations(ApiDescriptionReader apiDescriptionReader, ApiModelReader apiModelReader, DocumentationPluginsManager pluginsManager)
{
super(apiDescriptionReader, apiModelReader, pluginsManager);
}
@Override
public Multimap<String, ApiListing> scan(ApiListingScanningContext context)
{
final Multimap<String, ApiListing> def = super.scan(context);
final List<ApiDescription> apis = new LinkedList<>();
final List<Operation> operations = new ArrayList<>();
operations.add(new OperationBuilder(new CachingOperationNameGenerator())
.method(HttpMethod.POST)
.uniqueId("login")
.parameters(Arrays.asList(new ParameterBuilder()
.name("username")
.description("The username")
.parameterType("query")
.type(typeResolver.resolve(String.class))
.modelRef(new ModelRef("string"))
.build(),
new ParameterBuilder()
.name("password")
.description("The password")
.parameterType("query")
.type(typeResolver.resolve(String.class))
.modelRef(new ModelRef("string"))
.build()))
.summary("Log in") //
.notes("Here you can log in")
.build());
apis.add(new ApiDescription("/api/login/", "Authentication documentation", operations, false));
def.put("authentication", new ApiListingBuilder(context.getDocumentationContext().getApiDescriptionOrdering())
.apis(apis)
.description("Custom authentication")
.build());
return def;
}
}
Rendering Swagger json:
"/api/login/" : {
"post" : {
"summary" : "Log in",
"description" : "Here you can log in",
"operationId" : "loginUsingPOST",
"parameters" : [ {
"name" : "username",
"in" : "query",
"description" : "The username",
"required" : false,
"type" : "string"
}, {
"name" : "password",
"in" : "query",
"description" : "The password",
"required" : false,
"type" : "string"
} ]
}
}