Eine populäre Methode, um REST APIs zu dokumentieren ist Swagger 2. Für Spring(-Boot)-Projekte bietet sich Springfox an. Springfox integriert sich recht nahtlos in ein Spring-Projekt und stellt für konfigurierte REST Endpoints eine Browser-basierte Swagger UI Representation zur Verfügung. Mittels Annotations im Code können umfangreiche Details und Informationen zur API-Dokumentation hinzugefügt werden, z. B. erweiterte Informationen zu http-Statuscodes oder Beschreibungen zu einzelnen Feldern von Ressource-Modellen. Allerdings gibt es keine OOTB-Verbindung zwischen Swagger UI und der Spring-Security-Konfiguration. Dieser Artikel beschreibt eine Springfox-Swagger-UI-Erweiterung, um beide miteinander zu verbinden.
Übersicht Spring Security und Spring REST Controller
Durch die Verknüpfung von normalem Spring-Code mit Springfox API Annotations zur Generierung der Swagger-Dokumentation lässt sich ein altbekanntes Problem umgehen, nämlich dass Code und Dokumentation auseinanderlaufen und damit die Dokumentation häufig nicht dem aktuellen Code entspricht.
Spring-Security-Beispiel
Allerdings lässt sich mit Springfox die verwendete Spring-Security-Konfiguration nicht dokumentieren. Spring Security sichert per (Java-)Konfiguration ein REST-API ab und stellt eine Brücke zwischen Authentifizierung und Autorisierung her. Das folgende Code-Snippet beispielsweise sichert den REST-Endpoint „/user“ für GET- und POST-Zugriffe ab:
1@Configuration
2public class SecurityConfig extends WebSecurityConfigurerAdapter {
3 @Override
4 protected void configure(HttpSecurity httpSecurity) throws Exception {
5 super.configure(httpSecurity);
6
7 httpSecurity.csrf().disable();
8 httpSecurity.httpBasic();
9
10 httpSecurity.authorizeRequests()
11 .antMatchers(HttpMethod.POST, "/user").hasRole("admin")
12 .antMatchers(HttpMethod.GET, "/user/*").hasAnyRole("admin", "user");
13
14 httpSecurity.authorizeRequests().anyRequest().fullyAuthenticated();
15 }
16}
Die obige Spring-Security-Konfiguration definiert, dass die Ressource „user“ sowohl von Usern mit der Rolle „user“ als auch mit der Rolle „admin“ abgefragt (GET) werden darf, während der schreibende Zugriff (POST) auf dieselbe Ressource nur durch Nutzer mit der Rolle „admin“ erlaubt ist.
Spring-REST-Controller-Beispiel
Eine Spring-Security-Java-Konfiguration ist relativ einfach zu lesen und zu verwalten. Ein Problem ist aber, dass diese Config von der eigentlichen Ressource, also dem REST Endpoint, losgelöst ist:
1@RestController 2public class MyRestController { 3 @RequestMapping(method = RequestMethod.GET, value = "/user/{username}", produces = APPLICATION_JSON_VALUE) 4 @ApiOperation("Get details of a user with the given username") 5 @ApiResponses(value = { 6 @ApiResponse(code = 200, message = "Details about the given user"), 7 @ApiResponse(code = 401, message = "Cannot authenticate"), 8 }) 9 public UserDTO getExampleData(@Valid 10 @ApiParam("Non-empty username") @PathVariable(name = "username", required = true) String username) { 11 /* 12 Get your user resource somehow and return 13 */ 14 } 15 @PostMapping(value = "/user", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 16 @ResponseStatus(HttpStatus.CREATED) 17 @ApiOperation("Create a new user or update an existing user (based on username)") 18 @ApiResponses(value = { 19 @ApiResponse(code = 200, message = "User was successfully updated"), 20 @ApiResponse(code = 201, message = "User was successfully created"), 21 @ApiResponse(code = 401, message = "Cannot authenticate"), 22 }) 23 public void createUser(@Valid @RequestBody UserDTO userDTO) { 24 /* 25 Save the user resource somehow 26 */ 27 } 28}
Der hier dargestellte REST-Controller wird durch Springfox API Annotations dokumentiert (Springfox ist sehr viel mächtiger als hier dargestellt, siehe Springfox Reference Documentation ). Ein Blick auf das generierte Swagger UI zeigt allerdings, dass die Spring-Security-Konfiguration aus der Klasse SecurityConfig nicht in die Dokumentation einfließt. D.h. es ist nicht ersichtlich, welche Ressource durch welche Rollen aufrufbar ist:
Wie kann also Springfox so erweitert werden, dass die Spring-Security-Konfiguration automatisch Teil der Swagger-Dokumentation wird und damit eine höhere Kohäsion zwischen Implementierung und Dokumentation entsteht?
Swagger-UI-Erweiterung für Spring Security
Im Artikel Springfox Swagger mit externem Markdown erweitern hat Markus Höfer von codecentric bereits dargestellt, wie einfach Springfox mit Custom Annotations erweitert werden kann. Darauf basierend entstand der hier beschriebene Lösungsweg, um Spring-Security-Rollen in das Swagger UI zu übernehmen.
Folgende grundlegende Schritte sind notwendig, um eine Custom Annotation für Springfox Swagger UI zu erstellen:
- Erzeugen einer Custom Annotation
- Implementierung eines Custom Springfox OperationBuilderPlugins
- Custom Annotation an alle REST-Controller Endpoints hinzufügen
Weiterhin muss die Spring-Security-Konfiguration für die Springfox-Erweiterung „auslesbar“ gemacht werden, was im Folgenden zuerst beschrieben wird.
Spring Security auslesbar machen
Wie ein REST-API mit Spring Security gesichert werden kann, wurde bereits oben kurz dargestellt .
Spring Security verwendet Ant-Matcher Pattern, um http-Methoden und URL-Pfade zu spezifizieren, auf die definierte Rollen zugreifen dürfen. Diese Konfiguration ist jedoch „write-only“, d.h. die Klasse HttpSecurity (genauer gesagt die Klasse ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry
aus dem Package org.springframework.security.config.annotation.web.configurers
) lässt nicht zu, dass die Konfiguration der Ant-Matcher wieder ausgelesen wird. Das ist aber für unseren Use Case notwendig, denn wir wollen genau diese Information in unserer Swagger-Doku haben, müssen sie also auslesen können. Die vorgeschlagene Lösung verwaltet daher die Ant-Matcher-Konfiguration in einer eigenständigen Klasse namens HttpMethodResourceAntMatchers
, die sowohl der HttpSecurity Configuration als auch der Swagger-Doku als Spring Component zur Verfügung steht:
1public class HttpMethodResourceAntMatchers { 2 Logger logger = LoggerFactory.getLogger(HttpMethodResourceAntMatchers.class); 3 // list of all defined matchers 4 List matcherList = new ArrayList<>(); 5 /** 6 * Applies all existing Matchers to the provided httpSecurity object as .authorizeRequest().antMatchers().hasAnyRole() 7 * @param httpSecurity 8 * @throws Exception 9 */ 10 public void configure(org.springframework.security.config.annotation.web.builders.HttpSecurity httpSecurity) throws Exception { 11 for (HttpMethodResourceAntMatcher matcher : this.matcherList) { 12 httpSecurity.authorizeRequests().antMatchers(matcher.getMethod(), matcher.getAntPattern()).hasAnyRole(matcher.getRoles()); 13 } 14 } 15 /** 16 * Add a new Matcher with HttpMethod and URL-Path 17 * @param method 18 * @param antPattern 19 * @return 20 */ 21 public Role antMatchers(org.springframework.http.HttpMethod method, String antPattern) { 22 // create a new matcher 23 HttpMethodResourceAntMatcher matcher = new HttpMethodResourceAntMatcher(method, antPattern); 24 // add matcher to list of matchers 25 this.matcherList.add(matcher); 26 // return a Role wrapper object, which forces the user to add the role(s) to the matcher 27 Role role = new Role(matcher, this); 28 return role; 29 } 30 /** 31 * Helper class for a builder-like creation pattern 32 */ 33 public class Role { 34 HttpMethodResourceAntMatcher matcher; 35 HttpMethodResourceAntMatchers matchers; 36 37 public Role(HttpMethodResourceAntMatcher matcher, HttpMethodResourceAntMatchers matchers) { 38 this.matcher = matcher; 39 this.matchers = matchers; 40 } 41 /** 42 * Define which role has access to the given resource identified by the Ant-Matcher 43 * @param role 44 * @return 45 */ 46 public HttpMethodResourceAntMatchers hasRole(String role) { 47 matcher.setRoles(role); 48 return matchers; 49 } 50 /** 51 * Add a list of roles which have access to the given resource identified by the Ant-Matcher 52 * @param roles 53 * @return 54 */ 55 public HttpMethodResourceAntMatchers hasAnyRole(String... roles) { 56 matcher.setRoles(roles); 57 return matchers; 58 } 59 } 60}
Die Klasse HttpMethodResourceAntMatchers
imitiert die Spring HttpSecurity antMatchers()
und hasRole()
. Die hier referenzierte Klasse HttpMethodResourceAntMatcher
ist ein einfacher Pojo mit den Membern „httpMethod“, „antPattern“ und „roles“.
Die eigentliche Konfiguration der Matcher mithilfe der Klasse HttpMethodResourceAntMatchers
findet durch die Custom Spring Component MatchersSecurityConfiguration
statt:
1@Component
2public class MatchersSecurityConfiguration {
3
4 private HttpMethodResourceAntMatchers matchers;
5
6 /**
7 * Returns all http matchers
8 * @return
9 */
10 public HttpMethodResourceAntMatchers getMatchers() {
11 if (matchers == null) {
12 matchers = new HttpMethodResourceAntMatchers();
13 matchers.antMatchers(HttpMethod.POST, "/user").hasRole("admin")
14 .antMatchers(HttpMethod.GET, "/user/*").hasAnyRole("admin", "user");
15 }
16 return matchers;
17 }
18}
MatchersSecurityConfiguration
wird sowohl in die Spring Security Config injiziert als auch in die nachfolgende Swagger-Hilfsklasse für unsere Custom Annotation.
Damit kann unsere Spring Security Config folgendermaßen angepasst werden:
1@Configuration
2public class SecurityConfig extends WebSecurityConfigurerAdapter {
3
4 @Autowired
5 private MatchersSecurityConfiguration matchersSecurityConfiguration;
6
7 @Override
8 protected void configure(HttpSecurity httpSecurity) throws Exception {
9 super.configure(httpSecurity);
10
11 httpSecurity.csrf().disable();
12 httpSecurity.httpBasic();
13 // add matchers for REST API to httpSecurity
14 this.matchersSecurityConfiguration.getMatchers().configure(httpSecurity);
15
16 httpSecurity.authorizeRequests().anyRequest().fullyAuthenticated();
17 }
18}
Nachdem wir die Spring-Security-Konfiguration angepasst haben, müssen die Swagger Annotation und die Springfox-Erweiterung erstellt werden.
Custom Annotation
Für unseren Use Case erstellen wir die Custom API Annotation ApiRoleAccessNotes
:
1import java.lang.annotation.ElementType; 2import java.lang.annotation.Retention; 3import java.lang.annotation.RetentionPolicy; 4import java.lang.annotation.Target; 5 6@Target({ElementType.METHOD}) 7@Retention(RetentionPolicy.RUNTIME) 8public @interface ApiRoleAccessNotes { 9}
Die Annotation hat keinerlei Parameter, sie ist ein Marker für unser OperationBuilderPlugin
.
Custom Springfox OperationBuilderPlugin
Damit die oben definierte Annotation verwendet werden kann, muss das OperationBuilderPlugin
Interface als Spring Component implementiert werden. Die apply-Methode wird vom Springfox DocumentationPluginsManager
für jede REST-Resource im Classpath aufgerufen:
1@Component
2@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
3public class OperationNotesResourcesReader implements springfox.documentation.spi.service.OperationBuilderPlugin {
4 private final DescriptionResolver descriptions;
5
6 @Autowired
7 private MatchersSecurityConfiguration matchersSecurityConfiguration;
8
9 final static Logger logger = LoggerFactory.getLogger(OperationNotesResourcesReader.class);
10
11 @Autowired
12 public OperationNotesResourcesReader(DescriptionResolver descriptions) {
13 this.descriptions = descriptions;
14 }
15
16 @Override
17 public void apply(OperationContext context) {
18 try {
19 Optional methodAnnotation = context.findAnnotation(ApiRoleAccessNotes.class);
20 if ( !methodAnnotation.isPresent() || this.matchersSecurityConfiguration == null) {
21 // the REST Resource does not have the @ApiRoleAccessNotes annotation --> ignore
22 return;
23 }
24 String apiRoleAccessNoteText = "Accessible by users having one of the following roles: ";
25 HttpMethodResourceAntMatchers matchers = matchersSecurityConfiguration.getMatchers();
26 // get all configured ant-matchers and try to match with the current REST resource
27 for (HttpMethodResourceAntMatcher matcher : matchers.matcherList) {
28 // get the RequestMapping annotation, which contains the http-method
29 Optional requestMappingOptional = context.findAnnotation(RequestMapping.class);
30 if (matcher.getMethod() == getHttpMethod(requestMappingOptional)) {
31 AntPathMatcher antPathMatcher = new AntPathMatcher();
32 String path = context.requestMappingPattern();
33 if (path == null) {
34 continue;
35 }
36 boolean matches = antPathMatcher.match(matcher.getAntPattern(), path);
37 if (matches) {
38 // we found a match for both http-method and URL-path, get the roles
39 // add the roles to the notes. Use Markdown notation to create a list
40 apiRoleAccessNoteText = apiRoleAccessNoteText + "\n * " + String.join("\n * ", matcher.getRoles());
41 }
42 }
43
44 }
45 // add the note text to the Swagger UI
46 context.operationBuilder().notes(descriptions.resolve(apiRoleAccessNoteText));
47 } catch (Exception e) {
48 logger.error("Error when creating swagger documentation for security roles: " + e);
49 }
50 }
51
52 private HttpMethod getHttpMethod(Optional requestMappingOptional) {
53 if (!requestMappingOptional.isPresent()) return null;
54 if (requestMappingOptional.get().method() == null || requestMappingOptional.get().method()[0] == null)
55 return null;
56 RequestMethod requestMethod = requestMappingOptional.get().method()[0];
57 switch (requestMethod) {
58 case GET:
59 return HttpMethod.GET;
60 case PUT:
61 return HttpMethod.PUT;
62 case POST:
63 return HttpMethod.POST;
64 case DELETE:
65 return HttpMethod.DELETE;
66 }
67 return null;
68 }
69
70 @Override
71 public boolean supports(DocumentationType delimiter) {
72 return SwaggerPluginSupport.pluginDoesApply(delimiter);
73 }
74}
Innerhalb der Klasse wird geprüft, ob die aktuell von Springfox verarbeitete Ressource unsere ApiRoleAccessNotes
-Annotation besitzt. Ist dies der Fall, dann holen wir uns alle Spring Security Matcher und prüfen, welche davon auf die die aktuelle Ressource passen – sowohl in Bezug auf die Http-Methode also auch auf den konfigurierten URL-Path.
Custom Annotion @ REST Controller
Abschließend muss die Custom Annotation @ApiRoleAccessNotes
zu allen REST-Ressourcen hinzugefügt werden, für die wir im Swagger UI die konfigurierten Rollen sehen wollen:
1@RestController 2public class MyRestController { 3 @RequestMapping(method = RequestMethod.GET, value = "/user/{username}", produces = APPLICATION_JSON_VALUE) 4 @ApiOperation("Get details of a user with the given username") 5 @ApiResponses(value = { 6 @ApiResponse(code = 200, message = "Details about the given user"), 7 @ApiResponse(code = 401, message = "Cannot authenticate"), 8 }) 9 @ApiRoleAccessNotes 10 public UserDTO getExampleData(@Valid 11 @ApiParam("Non-empty username") @PathVariable(name = "username", required = true) String username) { 12 /* 13 Get your user resource somehow and return 14 */ 15 } 16 17 @PostMapping(value = "/user", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) 18 @ResponseStatus(HttpStatus.CREATED) 19 @ApiOperation("Create a new user or update an existing user (based on username)") 20 @ApiResponses(value = { 21 @ApiResponse(code = 200, message = "User was successfully updated"), 22 @ApiResponse(code = 200, message = "User was successfully created"), 23 @ApiResponse(code = 401, message = "Cannot authenticate"), 24 }) 25 @ApiRoleAccessNotes 26 public void createUser(@Valid @RequestBody UserDTO userDTO) { 27 /* 28 Save the user resource somehow 29 */ 30 } 31}
Im Swagger UI sieht das Ganze dann folgendermaßen aus:
Fazit
Mit ein paar wenigen Kniffen haben wir es geschafft, dass unsere Dokumentation wieder etwas näher an den Sourcecode gerückt ist. Der Sourcecode steht unter https://github.com/HenningWaack/SpringFoxSwaggerExtensionDemo zur Verfügung. Viel Spaß damit, und ich freue mich über alle Kommentare zu diesem Post!
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Weitere Artikel in diesem Themenbereich
Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Henning Waack
IT Consultant
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.