Rest With Spring
Web Service
Software System designed to support interportable machine to machine interaction over a network
3 Keys
- Designed for machine-to-machine (or application to application ) interaction
- Should be interoperable - Not platform dependent.
- Should allow communication over a network.
Service Definition
- Request/Response Format
- Request Structure
- Response Structure
- Endpoint
Web Services Groups
- Soap Based
- Rest Styled
SOAP and REST are not really comparable
SOAP
- SOAP defines a specific way of building web services
- SOAP uses XML for request excahange format.
- SOAP defines following XML Response/Request structure
SOAP (Cont.)
- Format
- SOAP XML Request
- SOAP XML Response
- Transport
- SOAP over MQ
- SOAP over HTTP
- Service Definition
- WSDL (Web Service Definition Language)
- Endpoint
- All Operations
- Request Structure
- Response Structure
- WSDL (Web Service Definition Language)
REST (Representational State Transfer )
- Makes the best use of HTTP
- HTTP Methods (GET, PUT, POST...)
- HTTP Status Codes (200, 404 ...)
- When you create a resource in rest you define operations on it using the methods provided by HTTP i.e POST, GET etc
- In REST you think in terms of resource which are in your application which you want to make available to other platforms.
- REST does not care how you represent your resource (URI) i.e XML, JSON, HTML.
- Transportation through HTTP.
- Service Definition: No standard. WADL, Swagger etc. Its is optional to provide service definition
Best Practices for REST
- Resource in the URI should be noun
- For different operations use verbs defined by HTTP i.e POST, GET, PUT and DELETE
- Response of API
- Success 2**
- Client Error 4**
- Server Error 5**
- Some common Reponse Status
- 200 Success
- 404 Resource not found
- 400 Bad Request
- 201 Created
- 401 Unauthorized
- 500 Server Error
- Use plurals e.g /users or /users/1
Sample Application
git@github.com:pulkitpushkarna/rest-with-spring.git
build.gradle
buildscript {
ext {
springBootVersion = '2.0.3.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('mysql:mysql-connector-java')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
SpringRestApplication
package com.spring.rest.springrest;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication
@EnableJpaRepositories(basePackages = "com.spring.rest.springrest.repositories")
@EntityScan(basePackages = {"com.spring.rest.springrest.entities"})
public class SpringRestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestApplication.class, args);
}
}
application.properties
spring.datasource.url=jdbc:mysql://localhost/rest_with_spring
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
Creating action to get user by Id and save User
package com.spring.rest.springrest.controllers;
import com.spring.rest.springrest.entities.User;
import com.spring.rest.springrest.services.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class UserController {
@Autowired
UserService userService;
@GetMapping("/")
String helloWorld(){
return "Hello World";
}
@GetMapping("/users")
List<User> getUsers(){
return userService.getAllUsers();
}
@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id){
return userService.getUserById(id);
}
@PostMapping("/users")
User saveUser( @RequestBody User user){
userService.saveUser(user);
return user;
}
}
package com.spring.rest.springrest.services;
import com.spring.rest.springrest.entities.User;
import com.spring.rest.springrest.repositories.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UserService {
@Autowired
UserRepository userRepository;
public List<User> getAllUsers(){
return (List<User>)userRepository.findAll();
}
public User getUserById(Long id){
Optional<User> optional=userRepository.findById(id);
return optional.isPresent() ? optional.get(): null;
}
public void saveUser(User user){
userRepository.save(user);
}
}
Add methods to retrieve user by Id and save user in user service
Enhancing post method to return the correct status code
@PostMapping("/users")
ResponseEntity<User> saveUser(@RequestBody User user){
userService.saveUser(user);
URI uri=ServletUriComponentsBuilder
.fromCurrentRequest()
.path("{id}").buildAndExpand(user.getId()).toUri();
return ResponseEntity.created(uri).build();
}
Getting user with non existing Id
@GetMapping("/users/{id}")
User getUserById(@PathVariable Long id){
User user=userService.getUserById(id);
if(user==null){
throw new UserNotFoundException("User not found!!");
}
return userService.getUserById(id);
}
package com.spring.rest.springrest.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message){
super(message);
}
}
Generic Exception Handling for all resources
package com.spring.rest.springrest.exceptions;
import java.util.Date;
public class ExceptionResponse {
private Date timestamp;
private String message;
private String details;
public ExceptionResponse(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDetails() {
return details;
}
public void setDetails(String details) {
this.details = details;
}
}
package com.spring.rest.springrest.exceptions;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.Date;
@ControllerAdvice
@RestController
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(),ex.getMessage(),request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(),ex.getMessage(),request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
}
Deleting the user
@DeleteMapping("/users/{id}")
void deleteUser(@PathVariable Long id){
User user=userService.getUserById(id);
if(user==null) {
throw new UserNotFoundException("User not found");
}
userService.deleteUser(user);
}
//UserService.java
public void deleteUser(User user){
userRepository.delete(user);
}
Validating the Bean
package com.spring.rest.springrest.entities;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.validation.constraints.Size;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(min = 2,message = "Name should have at least 2 characters")
private String name;
private Integer age;
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 Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
@PostMapping("/users")
ResponseEntity<User> saveUser(@Valid @RequestBody User user){
userService.saveUser(user);
URI uri=ServletUriComponentsBuilder
.fromCurrentRequest()
.path("{id}").buildAndExpand(user.getId()).toUri();
return ResponseEntity.created(uri).build();
}
package com.spring.rest.springrest.exceptions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
@ControllerAdvice
@RestController
public class CustomResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
@Autowired
MessageSource messageSource;
@ExceptionHandler(Exception.class)
public final ResponseEntity<Object> handleAllException(Exception ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(),ex.getMessage(),request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(UserNotFoundException.class)
public final ResponseEntity<Object> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(),ex.getMessage(),request.getDescription(false));
return new ResponseEntity(exceptionResponse, HttpStatus.NOT_FOUND);
}
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
List<String> errorMessageList=ex.getBindingResult().getAllErrors().stream().filter(e->e instanceof FieldError).map((e)->{
FieldError fieldError = (FieldError) e;
return messageSource.getMessage(fieldError, null);
}).collect(Collectors.toList());
ExceptionResponse exceptionResponse = new ExceptionResponse(new Date(),"Validation Failed....",errorMessageList.toString());
return new ResponseEntity(exceptionResponse, HttpStatus.BAD_REQUEST);
}
}
Exercise 1
- Create an Entity Student with fields:
- Name
- Standard
- age
- city
- percentage
- Generate the bootstrap data for Student
- Create Student Controller and introduce following actions in it:
- Get employee List
- Get Single Employee
- Create Employee
- Delete Employee
- Update Employee
Exercise 1 (Cont.)
- For Post Request send resource URI once the Student is created.
- Create an UnChecked Exception StudentNotFound and Throw it in case of POST, PUT and DELETE if you don't find the User.
- Create your customise response with timestamp, message and details and return your customise response on error
- Validate the Student bean and show the relevant error messages in error response
Internationalization
Create 2 files in resources directory
messages.properties
good.morning.message=Good Morning
messages_fr.properties
good.morning.message=Bonjour
@GetMapping("/")
String helloWorld(@RequestHeader(name = "Accept-Language",required = false)Locale locale){
System.out.println("hello world");
System.out.println(locale.getLanguage());
return messageSource.getMessage("good.morning.message",null,locale);
}
Accept-Language will be passed in header with language code
@Bean
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource source = new ResourceBundleMessageSource();
source.setBasenames("i18n/messages");
source.setUseCodeAsDefaultMessage(true);
return source;
}
Need to Configure this bean if you are not getting ResourceBudleMessageSource
More efficient way of introducing i18n
spring.mvc.locale=en
spring.mvc.locale-resolver=accept_header
application.properties
@GetMapping("/")
String helloWorld(){
return messageSource.getMessage("good.morning.message",null,LocaleContextHolder.getLocale());
}
Content Negotiaition
If you try to hit user API with Accept header value application/xml. You will get 406 Not Acceptable. In order to provide support for xml include the following dependency in build.gradle
compile ('com.fasterxml.jackson.dataformat:jackson-dataformat-xml')
For Post request you need to set the Accept:application/xml. Following will be the data format
<item>
<name>Sunny</name>
<age>46</age>
</item>
Static Filtering
If you want to filter some field from your object you can do it by using @JsonIgnore at the top of the field.
@JsonIgnoreProperties(value={"field1","field2"}) annotation is placed at the top of the class to ignore the fields in the reponse.
@Entity
@JsonIgnoreProperties(value = {"age"})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Size(min = 2,message = "Name should have at least 2 characters")
private String name;
//@JsonIgnore
private Integer age;
//Getters and Setters
}
Dynamic Filtering
package com.spring.rest.springrest.entities;
import com.fasterxml.jackson.annotation.JsonFilter;
@JsonFilter("myFilter")
public class Employee {
private String name;
private Integer age;
private Integer salary;
public Employee(String name, Integer age, Integer salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getSalary() {
return salary;
}
public void setSalary(Integer salary) {
this.salary = salary;
}
}
package com.spring.rest.springrest.controllers;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
import com.spring.rest.springrest.entities.Employee;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
@RestController
public class EmployeeController {
@GetMapping("/employee")
MappingJacksonValue getEmployee(){
Employee employee=new Employee("name1",23,23000);
SimpleBeanPropertyFilter simpleBeanPropertyFilter=
SimpleBeanPropertyFilter.filterOutAllExcept("name","salary");
FilterProvider filterProvider=new SimpleFilterProvider()
.addFilter("myFilter",simpleBeanPropertyFilter);
MappingJacksonValue mappingJacksonValue= new MappingJacksonValue(employee);
mappingJacksonValue.setFilters(filterProvider);
return mappingJacksonValue;
}
@GetMapping("/employee-list")
MappingJacksonValue getEmployeeList(){
List<Employee> employeeList= Arrays.asList(new Employee("name1",23,23000),
new Employee("name2",24,24000),
new Employee("name3",25,25000));
SimpleBeanPropertyFilter simpleBeanPropertyFilter=
SimpleBeanPropertyFilter.filterOutAllExcept("name","age");
FilterProvider filterProvider=new SimpleFilterProvider()
.addFilter("myFilter",simpleBeanPropertyFilter);
MappingJacksonValue mappingJacksonValue= new MappingJacksonValue(employeeList);
mappingJacksonValue.setFilters(filterProvider);
return mappingJacksonValue;
}
}
Versioning
package com.spring.rest.springrest.versioning;
public class PersonV1 {
private String name;
public PersonV1(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.spring.rest.springrest.versioning;
public class PersonV2 {
private Name name;
public PersonV2(Name name) {
this.name = name;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
}
package com.spring.rest.springrest.versioning;
public class Name {
private String firstName;
private String secondName;
public Name(String firstName, String secondName) {
this.firstName = firstName;
this.secondName = secondName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getSecondName() {
return secondName;
}
public void setSecondName(String secondName) {
this.secondName = secondName;
}
}
package com.spring.rest.springrest.versioning;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PersonController {
//URI Versioning
@GetMapping("/person/V1")
PersonV1 getPersonV1() {
return new PersonV1("Peter Parker");
}
@GetMapping("/person/V2")
PersonV2 getPersonV2() {
return new PersonV2(new Name("Peter","Parker"));
}
//Parameter Versioning
// /person/param?version=1
@GetMapping(value = "/person/param",params = "version=1")
PersonV1 getPersonParam1() {
return new PersonV1("Peter Parker");
}
@GetMapping(value = "/person/param",params = "version=2")
PersonV2 getPersonParam2() {
return new PersonV2(new Name("Peter","Parker"));
}
//Header Versioning
//API-VERSION 1 header required with request
@GetMapping(value = "/person/header",headers = "API-VERSION=1")
PersonV1 getPersonHeader1() {
return new PersonV1("Peter Parker");
}
@GetMapping(value = "/person/header",headers = "API-VERSION=2")
PersonV2 getPersonHeader2() {
return new PersonV2(new Name("Peter","Parker"));
}
//Mime Type/Content Negotiation/Accept Header Versioning
//Accept application/app-v1+json header required with request
@GetMapping(value = "/person/produces",produces = "application/app-v1+json")
PersonV1 getPersonProducer1() {
return new PersonV1("Peter Parker");
}
@GetMapping(value = "/person/produces",produces = "application/app-v2+json")
PersonV2 getPersonProducer2() {
return new PersonV2(new Name("Peter","Parker"));
}
}
Exercise 2
- Perform i18n in your application for message "Have a nice day" for 2 languages English and German.
- Provide the support for both JSON and XML in your Application.
- Perform Static and Dynamic Filtering on Student entity.
- Create 2 versions for Get Students API in one version we will return the percentage and in other we will not return the percentage. Perform following Versioning:
- URI versioning
- Parameter Versioning
- Header Versioning
- Accept Header Versioning
Consuming API with REST Template
@GetMapping("/post")
public String getPost(){
RestTemplate restTemplate = new RestTemplate();
String url="https://jsonplaceholder.typicode.com/posts/1";
ResponseEntity<String> response = restTemplate.getForEntity(url,String.class);
System.out.println(response.getStatusCode().toString());
System.out.println(response.getHeaders().toString());
return response.getBody();
}
Getting the Data in String Format
Getting and parsing the data to an Object
@GetMapping("/postObject")
public Post getPostObject(){
RestTemplate restTemplate = new RestTemplate();
String url="https://jsonplaceholder.typicode.com/posts/1";
ResponseEntity<Post> response = restTemplate.getForEntity(url,Post.class);
System.out.println(response.getStatusCode().toString());
System.out.println(response.getHeaders().toString());
return response.getBody();
}
Getting the List of Objects
@GetMapping("/postList")
public List<Post> getPostList(){
RestTemplate restTemplate = new RestTemplate();
String url="https://jsonplaceholder.typicode.com/posts";
ResponseEntity<List<Post>> response = restTemplate
.exchange(url, HttpMethod.GET, null,
new ParameterizedTypeReference<List<Post>>(){});
return response.getBody();
}
Create Post
@PostMapping("/createPost")
public ResponseEntity<Post> createPost(){
String url="https://jsonplaceholder.typicode.com/posts";
RestTemplate restTemplate= new RestTemplate();
HttpHeaders httpHeaders= new HttpHeaders();
httpHeaders.add("Content-type","application/json; charset=UTF-8");
HttpEntity<Post> request=new HttpEntity<>(new Post(10000L,10000L,"title1","description1"),httpHeaders);
Post post=restTemplate.postForObject(url,request,Post.class);
System.out.println(post);
return new ResponseEntity<Post>(post, HttpStatus.CREATED);
}
Updating the POST
@PutMapping("/updatePost")
public ResponseEntity<Post> updatePost(){
String url="https://jsonplaceholder.typicode.com/posts/1";
RestTemplate restTemplate= new RestTemplate();
HttpHeaders httpHeaders= new HttpHeaders();
httpHeaders.add("Content-type","application/json; charset=UTF-8");
HttpEntity<Post> request=new HttpEntity<>(new Post(1L,100L,"title1","description1"),httpHeaders);
return restTemplate.exchange(url,HttpMethod.PUT,request,Post.class);
}
Deleting the POST
@DeleteMapping("/deletePost")
public void deletePost(){
String url="https://jsonplaceholder.typicode.com/posts/1";
RestTemplate restTemplate= new RestTemplate();
HttpHeaders httpHeaders= new HttpHeaders();
httpHeaders.add("Content-type","application/json; charset=UTF-8");
HttpEntity<Post> request=new HttpEntity<>(httpHeaders);
restTemplate.exchange(url,HttpMethod.DELETE,request,Post.class);
}
Exercise 3
- Use RestTemplate to interact API's outside your application. Use Employee resource on http://dummy.restapiexample.com/. Try out all the API's of Employee
Rest Assured for API Testing
testCompile('io.rest-assured:rest-assured:3.1.0')
testCompile('com.jayway.restassured:rest-assured:2.3.3')
testCompile 'org.hamcrest:hamcrest-all:1.3'
Gradle Dependencies for rest assured
For Testing with Rest Assured we will use following API's:
https://jsonplaceholder.typicode.com/
package com.spring.rest.springrest;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Header;
import static org.hamcrest.Matchers.*;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRestApplicationTests {
@Test
public void canary(){
Assert.assertEquals(1,1);
}
@Test
public void testingGoogle(){
RestAssured.given().get("https://www.google.com/").then().statusCode(200);
}
}
Canary Test
String uri="https://jsonplaceholder.typicode.com/posts/{id}";
@Test
public void validPostTest() {
RestAssured.given().pathParam("id",1)
.get(uri).then()
.statusCode(200).log().all();
}
@Test
public void invalidPostTest() {
RestAssured.given().pathParam("id",1000)
.get(uri).then()
.statusCode(404).log().all();
}
Simple Test for Get
More Tests for GET
@BeforeClass
public static void init(){
RestAssured.baseURI="https://jsonplaceholder.typicode.com";
RestAssured.basePath="/posts/{id}";
}
@Test
public void postUserIdValueMatchingTest(){
RestAssured.given().pathParam("id",1)
.get().then()
.body("userId", equalTo(1))
.log().all();;
}
@Test
public void invalidPostTest() {
RestAssured.given().pathParam("id",1000)
.get().then()
.statusCode(404).log().all();
}
Matching Values
@Test
public void postUserIdValueMatchingTest(){
RestAssured.given().pathParam("id",1)
.get(uri).then()
.body("userId", equalTo(1))
.log().all();;
}
@Test
public void postUserIdAndTitleValueMatchingTest(){
RestAssured.given().pathParam("id",1)
.get(uri).then()
.body("userId", equalTo(1))
.body("title",equalTo("sunt aut facere repellat provident occaecati excepturi optio reprehenderit"))
.log().all();;
}
@Test
public void postCreationTest(){
Map<String,String> map = new HashMap<>();
map.put("userId","1");
map.put("title","title1");
map.put("body","body1");
RestAssured.given().header(new Header("Content-type","application/json; charset=UTF-8"))
.body(map)
.when().post("https://jsonplaceholder.typicode.com/posts").then()
.statusCode(201).log().all();
}
Test for Post
Test for Put
@Test
public void postUpdationTest(){
Map<String,String> map = new HashMap<>();
map.put("id","1");
map.put("userId","1");
map.put("title","title1");
map.put("body","body1");
RestAssured.given().pathParam("id",1).header(new Header("Content-type","application/json; charset=UTF-8"))
.body(map)
.when().put(uri).then()
.statusCode(200).log().all();
}
Test for Delete
@Test
public void postDeletionTest(){
RestAssured.given().pathParam("id",1).header(new Header("Content-type","application/json; charset=UTF-8"))
.when().delete(uri).then()
.statusCode(200).log().all();
}
package com.spring.rest.springrest;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.response.Header;
import static org.hamcrest.Matchers.*;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap;
import java.util.Map;
@SpringBootTest
@RunWith(SpringRunner.class)
public class SpringRestApplicationTests {
@Test
public void canary(){
Assert.assertEquals(1,1);
}
@Test
public void testingGoogle(){
RestAssured.given().get("https://www.google.com/").then().statusCode(200);
}
String uri="https://jsonplaceholder.typicode.com/posts/{id}";
@BeforeClass
public static void init(){
RestAssured.baseURI="https://jsonplaceholder.typicode.com";
RestAssured.basePath="/posts/{id}";
}
@Test
public void validPostTest() {
RestAssured.given().pathParam("id",1)
.get(uri).then()
.statusCode(200).log().all();
}
@Test
public void invalidPostTest() {
RestAssured.given().pathParam("id",1000)
.get(uri).then()
.statusCode(404).log().all();
}
@Test
public void postUserIdValueMatchingTest(){
RestAssured.given().pathParam("id",1)
.get(uri).then()
.body("userId", equalTo(1))
.log().all();;
}
@Test
public void postUserIdAndTitleValueMatchingTest(){
RestAssured.given().pathParam("id",1)
.get(uri).then()
.body("userId", equalTo(1))
.body("title",equalTo("sunt aut facere repellat provident occaecati excepturi optio reprehenderit"))
.log().all();;
}
@Test
public void postCreationTest(){
Map<String,String> map = new HashMap<>();
map.put("userId","1");
map.put("title","title1");
map.put("body","body1");
RestAssured.given().header(new Header("Content-type","application/json; charset=UTF-8"))
.body(map)
.when().post("https://jsonplaceholder.typicode.com/posts").then()
.statusCode(201).log().all();
}
@Test
public void postUpdationTest(){
Map<String,String> map = new HashMap<>();
map.put("id","1");
map.put("userId","1");
map.put("title","title1");
map.put("body","body1");
RestAssured.given().pathParam("id",1).header(new Header("Content-type","application/json; charset=UTF-8"))
.body(map)
.when().put(uri).then()
.statusCode(200).log().all();
}
@Test
public void postDeletionTest(){
RestAssured.given().pathParam("id",1).header(new Header("Content-type","application/json; charset=UTF-8"))
.when().delete(uri).then()
.statusCode(200).log().all();
}
}
Following file will reside under test java
Exercise 4
- Use Rest Assured to test all API's on http://dummy.restapiexample.com/
OAuth2 with Spring Security
- OAuth2 is a standard for authorization.
- A high level goal of OAuth is allowing a Resource Owner to give access to a third party in a limited way, without giving away the password.
Roles and Actors in OAuth
- The Resource Owner (the user) is capable of granting access to a Resource.
- The Resource Server (the API) is the host of the protected Resources.
- The Authorization Server is capable of issuing Access Tokens to the Client.
- The Client (the front end app) is capable of making requests on behalf of the Resource Owner.
Getting Start with OAuth config in Spring Security
build.gradle
apply plugin: 'java'
apply plugin: org.springframework.boot.gradle.plugin.SpringBootPlugin
buildscript {
ext {
springBootVersion = "1.4.0.RELEASE"
propdepsPluginVersion = "0.0.7"
}
repositories {
jcenter()
maven { url 'http://repo.spring.io/plugins-release' }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion")
classpath("org.springframework.build.gradle:propdeps-plugin:$propdepsPluginVersion")
}
}
repositories {
mavenCentral()
}
dependencies {
compile("org.springframework.boot:spring-boot-starter-web")
compile("org.springframework.boot:spring-boot-starter-security")
compile("org.springframework.security.oauth:spring-security-oauth2")
compile("org.springframework.security:spring-security-jwt")
}
Authorization Server
package com.spring_security.oauth.config;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
AuthenticationManager authenticationManager;
public AuthorizationServerConfiguration() {
super();
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("live-test")
.secret("abcde")
.authorizedGrantTypes("password")
.scopes("app")
.accessTokenValiditySeconds(30);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer authorizationServerSecurityConfigurer) throws Exception {
authorizationServerSecurityConfigurer.allowFormAuthenticationForClients();
}
}
Resource Server
package com.spring_security.oauth.config;
@Configuration
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
public ResourceServerConfiguration() {
super();
}
@Bean
public AuthenticationProvider authenticationProvider() {
final DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
return authenticationProvider;
}
@Autowired
public void configureGlobal(final AuthenticationManagerBuilder authenticationManagerBuilder) {
authenticationManagerBuilder.authenticationProvider(authenticationProvider());
}
@Override
public void configure(final HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable();
}
}
GrantAuthorityImpl
package com.spring_security.oauth.model;
import org.springframework.security.core.GrantedAuthority;
public class GrantAuthorityImpl implements GrantedAuthority {
String authority;
@Override
public String getAuthority() {
return authority;
}
}
User Entity
import java.util.List;
public class User implements UserDetails {
private String username;
private String password;
List<GrantAuthorityImpl> grantAuthorities;
public User(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public List<GrantAuthorityImpl> getAuthorities() {
return grantAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", grantAuthorities=" + grantAuthorities +
'}';
}
}
UserDetailService
package com.spring_security.oauth.service;
import com.spring_security.oauth.model.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class AppUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails userDetails = new User(username, "pass");
return userDetails;
}
}
Introduce Controller action
package com.spring_security.oauth;
import javax.annotation.security.RolesAllowed;
@Controller
@SpringBootApplication
@RolesAllowed("ROLE_ANONYMOUS")
public class OauthApplication {
@RequestMapping("/")
@ResponseBody
String home() {
return "Hello World!";
}
public static void main(String[] args) {
SpringApplication.run(OauthApplication.class, args);
}
}
Post Request for Authentication
Request:
http://localhost:8080/oauth/token <Post Request>
grant_type : password
client_id : live-test
username : user
password : pass
client_secret : abcde
Response:
{
"access_token": "fc132af8-e5ad-4318-8f29-b3cf95b51161",
"token_type": "bearer",
"expires_in": 29,
"scope": "app"
}
Now lets hit the controller action
Request:
localhost:8080/
<Header > Authorization bearer <access-token>
JWT token
A simple high level structure of JWT Token is:
headers.payloads.signature
Configuration changes for JWT Token in Authorization Server
@Bean
JwtAccessTokenConverter accessTokenConverter(){
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey("1234");
return jwtAccessTokenConverter;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.authenticationManager(authenticationManager)
.accessTokenConverter(accessTokenConverter());
}
Configuration changes in Authorization Server for Refresh Token
@Autowired
UserDetailsService userDetailsService;
@Bean
@Primary
DefaultTokenServices tokenServices(){
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Override
public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("live-test")
.secret("abcde")
.authorizedGrantTypes("password","refresh_token")
.refreshTokenValiditySeconds(30 * 24 * 3600)
.scopes("app")
.accessTokenValiditySeconds(30);
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.tokenStore(tokenStore())
.userDetailsService(userDetailsService)
.authenticationManager(authenticationManager);
}
Refresh Token
Request:
http://localhost:8080/oauth/token
grant_type : refresh_token
refresh_token: <refresh_token>
client_id:<client_id>
client_secret:<client_secret>
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjQyMzk4MzUsInVzZXJfbmFtZSI6InVzZXIiLCJqdGkiOiI4MWJkNjQ3Ny02NmY3LTRjNWUtODdkOS1lZmE2NjdlMzhmZDAiLCJjbGllbnRfaWQiOiJsaXZlLXRlc3QiLCJzY29wZSI6WyJhcHAiXX0.lNu44yo3HgJ2EkTcRHKj4mu9Rq6Nhi9IMXT_SM1PIxk",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MjQyNjk1MDQsInVzZXJfbmFtZSI6InVzZXIiLCJqdGkiOiI2NTk5NTdiMC04NzM4LTRjNjktYTk5My01Y2Q5YzQ1NTg3NzMiLCJjbGllbnRfaWQiOiJsaXZlLXRlc3QiLCJzY29wZSI6WyJhcHAiXSwiYXRpIjoiODFiZDY0NzctNjZmNy00YzVlLTg3ZDktZWZhNjY3ZTM4ZmQwIn0.Vx2dKyTympi6VTBnGid4uW5yGavcsRvwD5ezy1b3M8I",
"expires_in": 29,
"scope": "app",
"jti": "81bd6477-66f7-4c5e-87d9-efa667e38fd0"
}
Gradle project with oauth dependencies
git@github.com:pulkitpushkarna/spring-oauth-boiler-plate-code.git
Exercise 5
Create a Spring Boot Application and Implement OAuth in it.
Rest With Spring
By Pulkit Pushkarna
Rest With Spring
- 1,283