Spring Boot meets Swagger UI: Part II
Honestly, working from home felt good at the beginning with not getting up early, cut back all the commute time, cooking healthy, etc. However, I find weekends especially weird. Can't go out, there's nothing left to binge-watch, can't risk ordering food delivery. In short, there's nothing much to do. Bright side? I have been actively working on my side projects, listening to audiobooks and podcasts, singing and playing a lot of music. I just want to say, hang in there, this is not the end!
Let's move on to the second part of my last blog. While working on a project with Spring Security and OAuth, I encountered an issue documenting JWT authorization token in the Swagger specification and this blog will address just about it.
We have our previous Spring Boot REST API app to fetch all the employee information. Please check out my previous blog. We also used Springfox implementation of Swagger UI for the app. Now let's configure some security for the API endpoints using Oauth2 and JWT web tokens. If you are unfamiliar with Oauth2 concepts, please refer to this. In simple words, we will configure Oauth2 to protect our API endpoints using some kind of authentication and authorize all our requests. Oauth2 offers various grant types such as client_credentials, password, refresh_token and so on. For our sake, we will use client_credentials and password. Have the following code setup ready in your IDE:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo; | |
import com.fasterxml.jackson.databind.ObjectMapper; | |
import com.fasterxml.jackson.databind.SerializationFeature; | |
import org.springframework.boot.SpringApplication; | |
import org.springframework.boot.autoconfigure.SpringBootApplication; | |
import org.springframework.context.annotation.Bean; | |
@SpringBootApplication | |
public class DemoApplication { | |
public static void main(String[] args) { | |
SpringApplication.run(DemoApplication.class, args); | |
} | |
@Bean | |
public ObjectMapper objectMapper() { | |
ObjectMapper objectMapper = new ObjectMapper(); | |
objectMapper.enable(SerializationFeature.INDENT_OUTPUT); | |
return objectMapper; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Model; | |
public class Employee { | |
private long id; | |
private String name; | |
private String phone; | |
private String department; | |
private String jobTitle; | |
public Employee(long id, String name, String phone, String department, String jobTitle) { | |
this.id = id; | |
this.name = name; | |
this.phone = phone; | |
this.department = department; | |
this.jobTitle = jobTitle; | |
} | |
public long getId() { | |
return id; | |
} | |
public void setId(long id) { | |
this.id = id; | |
} | |
public String getName() { | |
return name; | |
} | |
public void setName(String name) { | |
this.name = name; | |
} | |
public String getPhone() { | |
return phone; | |
} | |
public void setPhone(String phone) { | |
this.phone = phone; | |
} | |
public String getDepartment() { | |
return department; | |
} | |
public void setDepartment(String department) { | |
this.department = department; | |
} | |
public String getJobTitle() { | |
return jobTitle; | |
} | |
public void setJobTitle(String jobTitle) { | |
this.jobTitle = jobTitle; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Controller; | |
import com.blogexampleone.demo.Model.Employee; | |
import com.blogexampleone.demo.Service.EmployeeService; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.http.HttpStatus; | |
import org.springframework.http.ResponseEntity; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RequestMethod; | |
import org.springframework.web.bind.annotation.RequestParam; | |
import org.springframework.web.bind.annotation.RestController; | |
import java.util.List; | |
@RestController | |
@RequestMapping("/employee") | |
public class EmployeeController { | |
@Autowired | |
private EmployeeService employeeService; | |
@RequestMapping(value = "", method = RequestMethod.GET) | |
public Object getAllUsers() { | |
List<Employee> employeeList = employeeService.getAllEmployees(); | |
if(employeeList.isEmpty()) | |
return new ResponseEntity<Void>(HttpStatus.NOT_FOUND); | |
return employeeList; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Service; | |
import com.blogexampleone.demo.Model.Employee; | |
import java.util.List; | |
public interface EmployeeService { | |
List<Employee> getAllEmployees(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Service; | |
import com.blogexampleone.demo.Model.Employee; | |
import org.springframework.stereotype.Component; | |
import java.util.ArrayList; | |
import java.util.List; | |
@Component | |
public class EmployeeServiceImpl implements EmployeeService { | |
@Override | |
public List<Employee> getAllEmployees() { | |
return populateMockDataForEmployees(); | |
} | |
private List<Employee> populateMockDataForEmployees(){ | |
List<Employee> employeeList = new ArrayList<>(); | |
employeeList.add(new Employee(1, "John Doe", "1234567890", "Technology", "Software Engineer")); | |
employeeList.add(new Employee(2, "Jane Smith", "9876543210", "Technology", "Sr. Software Engineer")); | |
employeeList.add(new Employee(3, "Tim Lang", "1029384756", "Business Development", "Asst. Manager")); | |
employeeList.add(new Employee(4, "Kevin Lee", "5647382910", "Marketing", "Marketing Manager")); | |
employeeList.add(new Employee(5, "Vanessa Chang", "5554446667", "Technology", "Director")); | |
return employeeList; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Config; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; | |
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; | |
@Configuration | |
@EnableResourceServer | |
public class ResourceServerConfig extends ResourceServerConfigurerAdapter { | |
private static final String[] AUTH_WHITELIST = { | |
// -- swagger ui | |
"/swagger-resources/**", | |
"/swagger-ui.html", | |
"/v2/api-docs", | |
"/webjars/**" | |
}; | |
@Override | |
public void configure(HttpSecurity http) throws Exception { | |
http | |
.headers() | |
.frameOptions() | |
.disable() | |
.and() | |
.authorizeRequests() | |
.antMatchers(AUTH_WHITELIST).permitAll() | |
.antMatchers("/oauth/token").permitAll() | |
.anyRequest().authenticated(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Config; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.security.authentication.AuthenticationManager; | |
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter; | |
@Configuration | |
@EnableWebSecurity | |
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { | |
@Bean | |
@Override | |
public AuthenticationManager authenticationManagerBean() throws Exception { | |
return super.authenticationManagerBean(); | |
} | |
@Override | |
public void configure(HttpSecurity http) throws Exception { | |
http | |
.authorizeRequests() | |
.antMatchers("/oauth/token").permitAll() | |
.anyRequest().authenticated() | |
.and() | |
.httpBasic() | |
.and() | |
.csrf().disable(); | |
} | |
@Override | |
protected void configure(AuthenticationManagerBuilder authManagerBuilder) throws Exception { | |
authManagerBuilder | |
.inMemoryAuthentication() | |
.withUser("test").password("{noop}test").roles("USER"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.blogexampleone.demo.Config; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import springfox.documentation.builders.PathSelectors; | |
import springfox.documentation.builders.RequestHandlerSelectors; | |
import springfox.documentation.spi.DocumentationType; | |
import springfox.documentation.spring.web.plugins.Docket; | |
import springfox.documentation.swagger2.annotations.EnableSwagger2; | |
@Configuration | |
@EnableSwagger2 | |
public class SwaggerConfig { | |
@Bean | |
public Docket api() { | |
return new Docket(DocumentationType.SWAGGER_2) | |
.select() | |
.apis(RequestHandlerSelectors.any()) | |
.paths(PathSelectors.any()) | |
.build(); | |
} | |
} |
Add following additional dependencies to your pom.xml.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- spring-security --> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-security</artifactId> | |
</dependency> | |
<!-- spring-security-jwt --> | |
<dependency> | |
<groupId>org.springframework.security</groupId> | |
<artifactId>spring-security-jwt</artifactId> | |
<version>1.0.11.RELEASE</version> | |
</dependency> | |
<!-- spring-security-oauth2 --> | |
<dependency> | |
<groupId>org.springframework.security.oauth</groupId> | |
<artifactId>spring-security-oauth2</artifactId> | |
<version>2.3.7.RELEASE</version> | |
</dependency> |
Getting an access token is a simple task since we already configured AuthorizationServer, ResourceServer, and WebSecurityConfigurerAdapter. Let's say we use grant_type password. Create the following POST request in Postman and it will return access token along with the scope, expiration time and token type. Note that when using grant_type password, both username-password and client_id-client_secret are needed.
We can use this access token to fetch employee information.
Now that we added security to our API, how do we document this? If you check Swagger UI, you can see a bunch of other endpoints along with employee-controller. These are automatically configured after we added Spring Security and OAuth dependencies in pom.xml.
Right now the employee-controller endpoint says no parameters but it clearly needs access token.
After Stackoverflowing and going through multiple GitHub issues, I came across a simple solution that worked for me. Global operation parameters allow global configuration of default parameters which are common for every rest operation of the API, but aren't needed in controller method signature (for example authentication information). Parameters added here will be part of every API operation in the generated Swagger specification. Meaning you don't need to update all your controller methods and add access token as a parameter. This is a great solution if you already have multiple controllers methods and want to authorize all your requests with only a few lines of code.
Update SwaggerConfig.java file and restart the app:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Bean | |
public Docket api() { | |
return new Docket(DocumentationType.SWAGGER_2) | |
.select() | |
.apis(RequestHandlerSelectors.any()) | |
.paths(PathSelectors.any()) | |
.build() | |
.globalOperationParameters(singletonList( | |
new ParameterBuilder() | |
.name("Authorization") | |
.modelRef(new ModelRef("string")) | |
.parameterType("header") | |
.required(true) | |
.hidden(true) | |
.defaultValue("Bearer ") | |
.build() | |
) | |
); | |
} |
Ta-da! We can now see updated Swagger Specification saying our API requests needs an Authorization Token. You can even enter access token and even try hitting API.
You can find all the code in part I and II here.
PS - I don't want to limit my blogs discussing only Spring Boot. The next blog will be a surprise (even for me, because I don't know the topic yet). Stay tuned!
PS - I don't want to limit my blogs discussing only Spring Boot. The next blog will be a surprise (even for me, because I don't know the topic yet). Stay tuned!
Comments
Post a Comment