Documenting Spring's login/logout API in Swagger

倾然丶 夕夏残阳落幕 提交于 2019-12-03 02:13:30

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"
        } ]
      }
    }

You can add a fake login and logout method in your API just to generate the Swagger documentation, it'll be automatically overriden by Spring Security filters.

@ApiOperation("Login.")
@PostMapping("/login")
public void fakeLogin(@ApiParam("User") @RequestParam String email, @ApiParam("Password") @RequestParam String password) {
    throw new IllegalStateException("This method shouldn't be called. It's implemented by Spring Security filters.");
}

@ApiOperation("Logout.")
@PostMapping("/logout")
public void fakeLogout() {
    throw new IllegalStateException("This method shouldn't be called. It's implemented by Spring Security filters.");
}

Just adding a little correction. If you want to make real POST-request (through HTML page of swagger-ui for example), you need to make little changes to Morten's answer.

Morten's code makes POST request to /login like this:

http://<hostname>/api/login?username=<user>&password=<password>

But if you want to make a POST request you need to pass a body with it, not just query parameters. To make that happen, you need to add parameter with name body and parameter type body like this:

@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("body")
            .required(true)
            .description("The body of request")
            .parameterType("body")            
            .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;
}

Now we can pass a body with our POST request. A body could be JSON, for example:

{"username":"admin","password":"admin"}

spike

You can use an interface describing the authentication API. The acutal implementation is provided by Spring Security. (This a variation of Italo's answer, where an interface is used instead of a fake implementation.)

/**
 * Authentication API specification for Swagger documentation and Code Generation.
 * Implemented by Spring Security.
 */
@Api("Authentication")
@RequestMapping(value = "/", produces = MediaType.APPLICATION_JSON_VALUE)
public interface AuthApi {
    /**
     * Implemented by Spring Security
     */
    @ApiOperation(value = "Login", notes = "Login with the given credentials.")
    @ApiResponses({@ApiResponse(code = 200, message = "", response = Authentication.class)})
    @RequestMapping(value = "/login", method = RequestMethod.POST)
    default void login(
        @RequestParam("username") String username,
        @RequestParam("password") String password
    ) {
        throw new IllegalStateException("Add Spring Security to handle authentication");
    }

    /**
     * Implemented by Spring Security
     */
    @ApiOperation(value = "Logout", notes = "Logout the current user.")
    @ApiResponses({@ApiResponse(code = 200, message = "")})
    @RequestMapping(value = "/logout", method = RequestMethod.POST)
    default void logout() {
        throw new IllegalStateException("Add Spring Security to handle authentication");
    }
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!