what you won't read in books about restful services
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.TRANSCRIPT
The thin line between RESTful and AWfulJakub Kubrynski
[email protected] / @jkubrynski 1 / 48
[email protected] / @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
[email protected] / @jkubrynski 3 / 48
Formal REST constraintsClient-Server
Stateless
Cache
Interface / Uniform Contract
Layered System
[email protected] / @jkubrynski 4 / 48
Richardson maturity model
http://martinfowler.com/articles/richardsonMaturityModel.html
[email protected] / @jkubrynski 5 / 48
POST vs PUT
[email protected] / @jkubrynski 6 / 48
POST vs PUTPOST creates new resources
[email protected] / @jkubrynski 7 / 48
POST vs PUTPOST creates new resources
PUT updates existing resources
PUT can create resource if ID is already known
[email protected] / @jkubrynski 8 / 48
Maybe PATCH?no "out of the box" support
[email protected] / @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);}
[email protected] / @jkubrynski 10 / 48
Cachingbe aware - especially IE caches aggressively
[email protected] / @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); }}
[email protected] / @jkubrynski 12 / 48
Cache headerscache-control: public, max-age=0, no-cache
public / privateno-cacheno-storemax-ages-maxage
[email protected] / @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
[email protected] / @jkubrynski 14 / 48
Compressionreduces response size dramatically
in Tomcat extend Connector with
compression="on"compressionMinSize="2048"noCompressionUserAgents="gozilla, traviata"compressableMimeType="text/html,text/xml"
[email protected] / @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
[email protected] / @jkubrynski 16 / 48
HATEOAS in Springpublic class Customer extends ResourceSupport { ... }// or wrap entity into Resource object
[email protected] / @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();}
[email protected] / @jkubrynski 18 / 48
@DanaDanger HTTP codes classification20x: cool
30x: ask that dude over there
40x: you fucked up
50x: we fucked up
[email protected] / @jkubrynski 19 / 48
Exceptionsinclude detailed information
{ "status": 400, "code": 40483, "message": "Incorrect body signature", "moreInfo": "http://www.mycompany.com/errors/40483"}
[email protected] / @jkubrynski 20 / 48
Exceptionsinclude detailed information
{ "status": 400, "code": 40483, "message": "Incorrect body signature", "moreInfo": "http://www.mycompany.com/errors/40483"}
hide stacktrace
[email protected] / @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() { }}
[email protected] / @jkubrynski 22 / 48
API Versioningdon't even think aboutapi.domain.com/v2/orders
URIs to the same resources should be fixed betweenversions
[email protected] / @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
[email protected] / @jkubrynski 24 / 48
Filtering and sortingGET /reviews?rating=5
GET /reviews?rating=5&sortAsc=author
[email protected] / @jkubrynski 25 / 48
Filtering and sortingGET /reviews?rating=5
GET /reviews?rating=5&sortAsc=author
Dynamic queries are easier in POST body
[email protected] / @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
[email protected] / @jkubrynski 27 / 48
[email protected] / @jkubrynski 29 / 48
Stateless or not?password hashing cost
session replication
load-balancing
[email protected] / @jkubrynski 30 / 48
Stateless or not?password hashing cost
session replication
load-balancing
...
stateless session?
[email protected] / @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(); }}
[email protected] / @jkubrynski 32 / 48
HQL InjectionList<Product> products = em.createQuery( "SELECT p FROM Product p where p.category = '" + categ + "'", Product.class) .getResultList();
[email protected] / @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
[email protected] / @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'
[email protected] / @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();
[email protected] / @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'''
[email protected] / @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>
[email protected] / @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
[email protected] / @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
[email protected] / @jkubrynski 41 / 48
XML External Entity<?xml version="1.0" encoding="utf-8"?><comment> <text>Yeah! I like it!</text></comment>
[email protected] / @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>
[email protected] / @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>
[email protected] / @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>
[email protected] / @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>
[email protected] / @jkubrynski 46 / 48
http://knowyourmeme.com/photos/531557 thx to @mihn
[email protected] / @jkubrynski 47 / 48
Thanks!
[email protected] / @jkubrynski 48 / 48