What you won't read in books about RESTful services

Preview:

DESCRIPTION

REST is as plain as the nose on your face. However, often exploring the secrets of this pattern ends up with the positive completion of two "Hello, World" class challenges. During this lecture we will focus on common problems and ways of handling them. We will deal with the security and best practices on topics like HATEOAS or versioning.

Citation preview

The thin line between RESTful and AWfulJakub Kubrynski

jk@devskiller.com / @jkubrynski 1 / 48

jk@devskiller.com / @jkubrynski 2 / 48

"The Code is more what you'd call guidelines than actual rules. Welcomeaboard the Black Pearl, Miss Turner"

-- Cpt. Hector Barbossa to Elizabeth Swann

RT Ben Hale

jk@devskiller.com / @jkubrynski 3 / 48

Formal REST constraintsClient-Server

Stateless

Cache

Interface / Uniform Contract

Layered System

jk@devskiller.com / @jkubrynski 4 / 48

Richardson maturity model

http://martinfowler.com/articles/richardsonMaturityModel.html

jk@devskiller.com / @jkubrynski 5 / 48

POST vs PUT

jk@devskiller.com / @jkubrynski 6 / 48

POST vs PUTPOST creates new resources

jk@devskiller.com / @jkubrynski 7 / 48

POST vs PUTPOST creates new resources

PUT updates existing resources

PUT can create resource if ID is already known

jk@devskiller.com / @jkubrynski 8 / 48

Maybe PATCH?no "out of the box" support

jk@devskiller.com / @jkubrynski 9 / 48

Maybe PATCH?no "out of the box" support

partial update

@RequestMapping(value = "/{id}", method = PATCH)public void updateArticle(HttpServletRequest request, @PathVariable("id") String id) { Article currentArticle = repository.findOne(id);

Article updatedArticle = objectMapper.readerForUpdating(currentArticle) .readValue(request.getReader());

repository.save(updatedArticle);}

jk@devskiller.com / @jkubrynski 10 / 48

Cachingbe aware - especially IE caches aggressively

jk@devskiller.com / @jkubrynski 11 / 48

Cachingbe aware - especially IE caches aggressively

disable caching

@Configurationpublic class RestConfig extends WebMvcConfigurerAdapter {

@Override public void addInterceptors(InterceptorRegistry registry) { WebContentInterceptor webContentInterceptor = new WebContentInterceptor(); webContentInterceptor.setCacheSeconds(0); registry.addInterceptor(webContentInterceptor); }}

jk@devskiller.com / @jkubrynski 12 / 48

Cache headerscache-control: public, max-age=0, no-cache

public / privateno-cacheno-storemax-ages-maxage

jk@devskiller.com / @jkubrynski 13 / 48

Cache headerscache-control: public, max-age=0, no-cache

public / privateno-cacheno-storemax-ages-maxage

ETag

If-None-Match: "0d41d8cd98f00b204e9800998ecf8427e"Spring brings ShallowEtagHeaderFilter

jk@devskiller.com / @jkubrynski 14 / 48

Compressionreduces response size dramatically

in Tomcat extend Connector with

compression="on"compressionMinSize="2048"noCompressionUserAgents="gozilla, traviata"compressableMimeType="text/html,text/xml"

jk@devskiller.com / @jkubrynski 15 / 48

HATEOASself-descriptive

client understands hypermedia

{ "name": "Alice", "links": [ { "rel": "self", "href": "/customers/1213" }, { "rel": "parent", "href": "/customers/14" }, { "rel": "currentOrder", "href": "/orders/14312" } ]}

HTTP/1.1 201 CreatedLocation: http://api.mydomain.com/orders/1234

jk@devskiller.com / @jkubrynski 16 / 48

HATEOAS in Springpublic class Customer extends ResourceSupport { ... }// or wrap entity into Resource object

jk@devskiller.com / @jkubrynski 17 / 48

HATEOAS in Springpublic class Customer extends ResourceSupport { ... }// or wrap entity into Resource object

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;

public HttpEntity<Customer> get(@PathVariable("id") String customerId) { Customer customer = repository.findOne(customerId); String pId = customer.getBoss(); String oId = customer.currentOrderId();

customer.add(linkTo(methodOn(CustomerController.class).get(customerId)).withSelfRel()); customer.add(linkTo(methodOn(CustomerController.class).get(pId)).withRel("parent")); customer.add(linkTo(methodOn(OrderController.class).get(oId)).withRel("currentOrder"));

return new ResponseEntity<Customer>(customer, HttpStatus.OK);}

public ResponseEntity create(@RequestBody Customer customer) { String id = repository.save(customer); return ResponseEntity.created(linkTo(CustomerController.class).slash(id).toUri()) .build();}

jk@devskiller.com / @jkubrynski 18 / 48

@DanaDanger HTTP codes classification20x: cool

30x: ask that dude over there

40x: you fucked up

50x: we fucked up

jk@devskiller.com / @jkubrynski 19 / 48

Exceptionsinclude detailed information

{ "status": 400, "code": 40483, "message": "Incorrect body signature", "moreInfo": "http://www.mycompany.com/errors/40483"}

jk@devskiller.com / @jkubrynski 20 / 48

Exceptionsinclude detailed information

{ "status": 400, "code": 40483, "message": "Incorrect body signature", "moreInfo": "http://www.mycompany.com/errors/40483"}

hide stacktrace

jk@devskiller.com / @jkubrynski 21 / 48

Handling Spring MVC exceptions@ControllerAdvicepublic class MyExceptionHandler extends ResponseEntityExceptionHandler {

/* Handling framework exceptions */ @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { LOG.error("Spring MVC exception occurred", ex); return super.handleExceptionInternal(ex, body, headers, status, request); }

/* Handling application exceptions */ @ResponseStatus(value = HttpStatus.NOT_FOUND) @ExceptionHandler(ResourceNotFoundException.class) public void handleResourceNotFound() { }}

jk@devskiller.com / @jkubrynski 22 / 48

API Versioningdon't even think aboutapi.domain.com/v2/orders

URIs to the same resources should be fixed betweenversions

jk@devskiller.com / @jkubrynski 23 / 48

API Versioningdon't even think aboutapi.domain.com/v2/orders

URIs to the same resources should be fixed betweenversions

use Content-Type

1 version: application/vnd.domain+json

2 version: application/vnd.domain.v2+json

jk@devskiller.com / @jkubrynski 24 / 48

Filtering and sortingGET /reviews?rating=5

GET /reviews?rating=5&sortAsc=author

jk@devskiller.com / @jkubrynski 25 / 48

Filtering and sortingGET /reviews?rating=5

GET /reviews?rating=5&sortAsc=author

Dynamic queries are easier in POST body

jk@devskiller.com / @jkubrynski 26 / 48

Filtering and sortingGET /reviews?rating=5

GET /reviews?rating=5&sortAsc=author

Dynamic queries are easier in POST body

POST /reviews/searches

GET /reviews/searches/23?page=2

jk@devskiller.com / @jkubrynski 27 / 48

Documentationrunnable with examples

Swagger

jk@devskiller.com / @jkubrynski 28 / 48

jk@devskiller.com / @jkubrynski 29 / 48

Stateless or not?password hashing cost

session replication

load-balancing

jk@devskiller.com / @jkubrynski 30 / 48

Stateless or not?password hashing cost

session replication

load-balancing

...

stateless session?

jk@devskiller.com / @jkubrynski 31 / 48

Avoiding session creation in Spring@EnableWebSecuritypublic class SpringSecurity extends WebSecurityConfigurerAdapter {

@Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/secure/**").fullyAuthenticated() .and()

.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER)

.and() .httpBasic(); }}

jk@devskiller.com / @jkubrynski 32 / 48

SecuritySQL Injection

XSS

CSRF

XXE

jk@devskiller.com / @jkubrynski 33 / 48

HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();

jk@devskiller.com / @jkubrynski 34 / 48

HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();

categ = ' OR '1'='1

jk@devskiller.com / @jkubrynski 35 / 48

HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();

categ = ' OR '1'='1

SELECT __fields__ FROM products WHERE category = '' OR '1'='1'

jk@devskiller.com / @jkubrynski 36 / 48

HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();

categ = ' OR '1'='1

SELECT __fields__ FROM products WHERE category = '' OR '1'='1'

List<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = :categ", Product.class) .setParameter("categ", categ) .getResultList();

jk@devskiller.com / @jkubrynski 37 / 48

HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();

categ = ' OR '1'='1

SELECT __fields__ FROM products WHERE category = '' OR '1'='1'

List<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = :categ", Product.class) .setParameter("categ", categ) .getResultList();

SELECT __fields__ FROM products WHERE category = ' OR ''1''=''1'''

jk@devskiller.com / @jkubrynski 38 / 48

CSRF - Cross-site request forgery<img src="https://api.mybank.com/transfers/from/1233/to/1234/amount/5000">

<form action="https://api.mybank.com/transfers" method="POST"> <input type="hidden" name="from" value="1233"/> <input type="hidden" name="to" value="1234"/> <input type="hidden" name=amount" value="5000"/> <input type="submit" value="Celebrity Nude Photos!"/></form>

jk@devskiller.com / @jkubrynski 39 / 48

CSRF - Cross-site request forgery<img src="https://api.mybank.com/transfers/from/1233/to/1234/amount/5000">

<form action="https://api.mybank.com/transfers" method="POST"> <input type="hidden" name="from" value="1233"/> <input type="hidden" name="to" value="1234"/> <input type="hidden" name=amount" value="5000"/> <input type="submit" value="Celebrity Nude Photos!"/></form>

One time request tokens

Correct CORS headers

jk@devskiller.com / @jkubrynski 40 / 48

CORS - Cross Origin Requests SharingPreflight request

OPTIONS /cors HTTP/1.1Origin: http://www.domain.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: X-Custom-HeaderHost: api.mydomain.orgAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...

Preflight response

Access-Control-Allow-Origin: http://www.domain.comAccess-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: X-Custom-HeaderContent-Type: text/html; charset=utf-8

jk@devskiller.com / @jkubrynski 41 / 48

XML External Entity<?xml version="1.0" encoding="utf-8"?><comment> <text>Yeah! I like it!</text></comment>

jk@devskiller.com / @jkubrynski 42 / 48

XML External Entity<?xml version="1.0" encoding="utf-8"?><comment> <text>Yeah! I like it!</text></comment>

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE myentity [ <!ENTITY a "Yeah! I like it!"> ]><comment> <text>&a;</text></comment>

jk@devskiller.com / @jkubrynski 43 / 48

XML External Entity<?xml version="1.0" encoding="utf-8"?><!DOCTYPE myentity [ <!ENTITY a SYSTEM "/etc/passwd"> ]><comment> <text>&a;</text></comment>

jk@devskiller.com / @jkubrynski 44 / 48

XML External Entity<?xml version="1.0" encoding="utf-8"?><!DOCTYPE myentity [ <!ENTITY a SYSTEM "/etc/passwd"> ]><comment> <text>&a;</text></comment>

<?xml version="1.0" encoding="utf-8"?><comment> <text>root:x:0:0:root:/root:/bin/bash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown halt:x:7:0:halt:/sbin:/sbin/halt ..... </text></comment>

jk@devskiller.com / @jkubrynski 45 / 48

XML External Entity<?xml version="1.0" encoding="utf-8"?><!DOCTYPE myentity [<!ENTITY a "abcdefghij1234567890" > <!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a" > <!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;" > <!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;" > ...<!ENTITY h "&g;&g;&g;&g;&g;&g;&g;&g;&g;&g;" >]><comment> <text>&h;</text></comment>

jk@devskiller.com / @jkubrynski 46 / 48

http://knowyourmeme.com/photos/531557 thx to @mihn

jk@devskiller.com / @jkubrynski 47 / 48

Thanks!

jk@devskiller.com / @jkubrynski 48 / 48

Recommended