What you won't read in books about RESTful services
-
Upload
jakub-kubrynski -
Category
Internet
-
view
696 -
download
1
description
Transcript of What you won't read in books about RESTful services
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