rest with spring boot #jqfk
Post on 14-Jul-2015
3.332 Views
Preview:
TRANSCRIPT
REST with Spring Boot
槙 俊明(@making) JavaQne 2015 #jqfk
2015-01-24
今日のお話
• Spring Boot概要 • RESTについて色々
• Richardson Maturity Model • Spring HATEOAS / Spring Data REST
• JSON Patch • Spring Sync
• Securing REST Serivces • Spring Security OAuth / Spring Session
Spring Bootの概要
Spring Boot概要• Springを使って簡単にモダンなアプリケーションを開発するための仕組み
• AutoConfigure + 組み込みサーバーが特徴
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.2.1.RELEASE</version></parent><dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency></dependencies><build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins></build><properties> <java.version>1.8</java.version></properties>
この設定を追加するだけ
package com.example;!import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.EnableAutoConfiguration;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;!@RestController@EnableAutoConfigurationpublic class App {! @RequestMapping("/") String home() { return "Hello World!"; }! public static void main(String[] args) { SpringApplication.run(App.class, args); }}
魔法のアノテーション
mainメソッドでアプリ実行
ログ
組込Tomcatが起動した
ログ
組込Tomcatが起動した
実行可能jarを作成
$ mvn package
jarを実行
$ java -jar target/jggug-helloworld-1.0.0-SNAPSHOT.jar
プロパティを変更して実行
$ java -jar target/jggug-helloworld-1.0.0-SNAPSHOT.jar --server.port=8888
--(プロパティ名)=(プロパティ値)
@SpringBootApplication@RestControllerpublic class App {! @RequestMapping("/") String home() { return "Hello World!"; }! public static void main(String[] args) { SpringApplication.run(App.class, args); }}
Spring Boot 1.2より
@SpringBootApplication@RestControllerpublic class App {! @RequestMapping("/") String home() { return "Hello World!"; }! public static void main(String[] args) { SpringApplication.run(App.class, args); }}
Spring Boot 1.2より
@EnableAutoConfiguration+ @Configuration+ @ComponentScan
RESTについて
REST?
•クライアントとサーバ間でデータをやりとりするためのソフトウェアアーキテクチャスタイルの一つ
•サーバーサイドで管理している情報の中からクライアントに提供すべき情報を「リソース」として抽出し、リソースをHTTPで操作
RESTに関するいろいろな話題
• Richardson Maturity Model
• JSON Patch
• Security
Richardson Maturity Model
Richardson Maturity Model
http://martinfowler.com/articles/richardsonMaturityModel.html
RESTの成熟モデル
Richardson Maturity Model
http://martinfowler.com/articles/richardsonMaturityModel.html
RESTの成熟モデル
あなたの
Level 0: Swamp of POX
• POX (Plain Old XML) • SOAP、XML-RPC
転送プロトコルとして HTTPを使っているだけ。 通常POSTオンリー
Level 0: Swamp of POX
Level 1: Resources
• /customers、/usersなど
•なんちゃってREST
URLに名詞を使う。
Level 1: Resources
Level 2: HTTP Vebs
• GET/POST/PUT/DELETEなど
•一般的にみんなが言っている”REST”
HTTPメソッドを動詞に使う。 ヘッダやステータスを活用
Level 2: HTTP Vebs
ここまでは対応している Webフレームワークは多い
Spring Boot + Spring MVC@SpringBootApplication@RestController @RequestMapping("user")public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } @RequestMapping(method = RequestMethod.GET) User get() { return new User("demo", "password"); } @RequestMapping(method = RequestMethod.POST) ResponseEntity<User> post(@RequestBody User user) { // create return ResponseEntity .created(location).body(created); }}
Level 3: Hypermedia Controls
• HATEOAS (Hypermedia As The Engine Of Application State)
Hypermediaリンクを 使用してナビゲーション。 ユーザーにサービス全体の 知識を強いない。
Level 3: Hypermedia Controls
Level 3: Hypermedia Controls
{ "name": "Alice", "links": [ { "rel": "self", "href": "http://.../customer/1" } ]}
Level 3: Hypermedia Controls
{ "name": "Alice", "links": [ { "rel": "self", "href": "http://.../customer/1" }, { "rel": "user", "href": "http://.../customer/1/user" } ]}
Level 3: Hypermedia Controls
{ "name": "Alice", "links": [ { "rel": "self", "href": "http://.../customer/1" }, { "rel": "user", "href": "http://.../customer/1/user" } ]}
関連するリソースのリンクが含まれる
Spring HATEOAS
Spring MVCにHATEOASの概念を追加
•リソースのモデルにLink追加
• HAL等のデータフォーマットに対応
具体例で説明
扱うモデル
Bookmarkエンティティ@Entitypublic class Bookmark { @ManyToOne @JsonIgnore Account account; @Id @GeneratedValue Long id; String uri; String description; // omitted}
Accountエンティティ@Entitypublic class Account { @OneToMany(mappedBy = "account") Set<Bookmark> bookmarks; @Id @GeneratedValue Long id; @JsonIgnore String password; String username; // omitted}
BookmarkRepository
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {!
Collection<Bookmark> findByAccountUsername(String username);!
}
BookmarkRepository
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {!
Collection<Bookmark> findByAccountUsername(String username);!
}
Spring Data JPAを使用。 CRUDを簡単に使える。
BookmarkRepository
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {!
Collection<Bookmark> findByAccountUsername(String username);!
}
Spring Data JPAを使用。 CRUDを簡単に使える。
命名規約に対応したクエリが 実行されるメソッド(実装不要)
BookmarkRepository
public interface BookmarkRepository extends JpaRepository<Bookmark, Long> {!
Collection<Bookmark> findByAccountUsername(String username);!
}
Spring Data JPAを使用。 CRUDを簡単に使える。
命名規約に対応したクエリが 実行されるメソッド(実装不要)
SELECT b FROM Bookmark b WHERE b.account.username= :username
AccountRepository
public interface AccountRepository extends JpaRepository<Account, Long> {!
Optional<Account> findByUsername(String username);!
}
AccountRepository
public interface AccountRepository extends JpaRepository<Account, Long> {!
Optional<Account> findByUsername(String username);!
} Java SE 8のOptionalに対応。 1件取得結果の存在有無をOptionalで表現
Level 2
普通のSpring MVCプログラミング
Controller@RestController @RequestMapping("/{userId}/bookmarks")class BookmarkRestController { @Autowired BookmarkRepository bookmarkRepository; @Autowired AccountRepository accountRepository; @RequestMapping(value = "/{bookmarkId}", method = RequestMethod.GET) Bookmark readBookmark( @PathVariable String userId, @PathVariable Long bookmarkId) { this.validateUser(userId); return this.bookmarkRepository.findOne(bookmarkId); } // …}
Controller@RequestMapping(method = RequestMethod.POST)ResponseEntity<?> add(@PathVariable String userId, @RequestBody Bookmark in) { return this.accountRepository .findByUsername(userId) .map(account -> { Bookmark result = bookmarkRepository.save( new Bookmark(account, in.uri, in.description)); URI location = …; return ResponseEntity .created(location).body(result); }) .orElseThrow(() -> new UserNotFoundException(userId));}
起動@SpringBootApplication public class Application { @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) { return (evt) -> Stream.of("kis", "skrb", "making") .forEach(a -> { Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description"));}); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
起動@SpringBootApplication public class Application { @Bean CommandLineRunner init(AccountRepository accountRepository, BookmarkRepository bookmarkRepository) { return (evt) -> Stream.of("kis", "skrb", "making") .forEach(a -> { Account account = accountRepository.save(new Account(a, "password")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/1/" + a, "A description")); bookmarkRepository.save(new Bookmark(account, "http://bookmark.com/2/" + a, "A description"));}); } public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
起動時に実行されるクラス
Example: GET
$ curl -X GET localhost:8080/making/bookmarks[{ "description": "A description", "uri": "http://bookmark.com/1/making", "id": 5 }, { "description": "A description", "uri": "http://bookmark.com/2/making", "id": 6 }]
Example: GET
$ curl -X GET localhost:8080/making/bookmarks/5{ "description": "A description", "uri": "http://bookmark.com/1/making", "id": 5}
Example: POST$ curl -v -X POST localhost:8080/making/bookmarks -H 'Content-Type: application/json' -d '{"url":"http://bit.ly/hajiboot", "description":"はじめてのSpring Boot"}'(略)< HTTP/1.1 201 Created< Location: http://localhost:8080/making/bookmarks/7(略){"id":7,"uri":null,"description":"はじめてのSpring Boot"}
Error Handling
@ResponseStatus(HttpStatus.NOT_FOUND)class UserNotFoundException extends RuntimeException { public UserNotFoundException(String userId) { super("could not find user '" + userId + "'."); }}
Error Handling$ curl -v -X GET localhost:8080/maki/bookmarks/6(略)< HTTP/1.1 404 Not Found(略){ "path": "/maki/bookmarks/6", "message": "could not find user 'maki'.", "exception": "bookmarks.UserNotFoundException", "error": "Not Found", "status": 404, "timestamp": 1421044115740}
Level 3
<dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId></dependency>
Spring HATEOASを使用
Level 3
<dependency> <groupId>org.springframework.hateoas</groupId> <artifactId>spring-hateoas</artifactId></dependency>
Spring HATEOASを使用
Spring Bootを使うと依存関係を定義するだけでHATEOASを使える
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
HypermediaLinkを表現するための基本的な
情報を持つ
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
HypermediaLinkを表現するための基本的な
情報を持つ
ControllerLinkBuilder
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
“bookmark-uri"というrelで 対象のブックマークへのlinkを追加
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
"bookmarks"というrelで ブックマークコレクションの リソースへのlinkを追加
class BookmarkResource extends ResourceSupport { private final Bookmark bookmark; public BookmarkResource(Bookmark bookmark) { String username = bookmark.getAccount().getUsername(); this.bookmark = bookmark; this.add(new Link(bookmark.getUri(), "bookmark-uri")); this.add(linkTo(BookmarkRestController.class, username) .withRel("bookmarks")); this.add(linkTo(methodOn(BookmarkRestController.class, username) .readBookmark(username, bookmark.getId())) .withSelfRel()); } public Bookmark getBookmark() {/**/} }
"self"というrelで 自身へのlinkを追加
@RequestMapping(value = “/{bookmarkId}", method = RequestMethod.GET)BookmarkResource readBookmark( @PathVariable String userId, @PathVariable Long bookmarkId) { this.validateUser(userId); return new BookmarkResource( this.bookmarkRepository .findOne(bookmarkId));}
@RequestMapping(method = RequestMethod.POST) ResponseEntity<?> add(@PathVariable String userId, @RequestBody Bookmark in) { return accountRepository.findByUsername(userId) .map(account -> { Bookmark bookmark = bookmarkRepository .save(new Bookmark(account, in.uri, in.description)); Link selfLink = new BookmarkResource(bookmark) .getLink("self"); URI location = URI.create(selfLink.getHref()); return ResponseEntity .created(location).body(bookmark); }) .orElseThrow(() -> new UserNotFoundException(userId)); }
サンプル: GET$ curl -X GET localhost:8080/making/bookmarks/5{ "_links": { "self": { "href": "http://localhost:8080/making/bookmarks/5" }, "bookmarks": { "href": "http://localhost:8080/making/bookmarks" }, "bookmark-uri": { "href": "http://bookmark.com/1/making" } }, "bookmark": { "description": "A description", "uri": "http://bookmark.com/1/making", "id": 5 }}
Example: GET$ curl -v -X GET localhost:8080/making/bookmarks/6> GET /making/bookmarks/6 HTTP/1.1> User-Agent: curl/7.30.0> Host: localhost:8080> Accept: */*>< HTTP/1.1 200 OK< Server: Apache-Coyote/1.1< Content-Type: application/hal+json;charset=UTF-8< Transfer-Encoding: chunked< Date: Mon, 12 Jan 2015 05:45:40 GMT<(略)
HALという規格のフォーマットを使用
している
HAL
http://stateless.co/hal_specification.html
Hypertext Application Language
Hypermediaを表現する フォーマット仕様の1つ
@ControllerAdviceclass BookmarkControllerAdvice {! @ResponseBody @ExceptionHandler(UserNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) VndErrors userNotFoundExceptionHandler( UserNotFoundException ex) { return new VndErrors("error", ex.getMessage()); }}
Error Handling
$ curl -X GET localhost:8080/maki/bookmarks/5[ { "message": "could not find user 'maki'.", "logref": "error" }] Vnd.Errror規格の
エラーフォーマット
Error Handling
https://github.com/blongden/vnd.error
普通の人はLevel 2で十分。 こだわりたい人はLevel 3へ。
Spring Data REST
Spring Dataのリポジトリを そのままREST APIとしてExport
Spring Data
Spring Data
JPASpring D
ata M
ongoDB
Spring Data
Xxx
JPASpring Data REST
RDB
JSON
MongoDB
Xxx
Spring Bootから使う場合
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-rest</artifactId></dependency>
Spring Bootを使うと依存関係を定義するだけでSpring Data RESTを使える
Event Handler@RepositoryEventHandler(Bookmark.class)public class BookmarkEventHandler { @HandleBeforeSave public void beforeSave(Bookmark p) { /* … */ } @HandleAfterDelete public void afterDelete(Bookmark p) { /* … */ }}
超短期間でRESTサービスをつくる 必要がある場合に強力
JSON Patch
コレクションの変更
[{"value":"a"},{"value":"b"},{"value":"c"}]
[{"value":"a"},{"value":"c"},{"value":"d"}]
Original
Modified
[+] {"value":"d"} を4番目に追加 [-] 2番目の要素を削除 …
コレクションの変更
[{"value":"a"},{"value":"b"},{"value":"c"}]
[{"value":"a"},{"value":"c"},{"value":"d"}]
Original
Modified
[+] {"value":"d"} を4番目に追加 [-] 2番目の要素を削除 …もっと効率的なデータ転送を!
動機
より効率的なデータ転送
複数のクライアント間での データ同期
オフライン作業の反映
http://www.slideshare.net/briancavalier/differential-sync-and-json-patch-s2-gx-2014/13
http://www.slideshare.net/briancavalier/differential-sync-and-json-patch-s2-gx-2014/13
Diff & Patch!
JSON PatchRFC 6902
パッチをJSONで表現
PATCHメソッドで送信
JSON Pointer (RFC 6901)で指定したJSONパスへの操作を表現
application/json-patch+json
JSON PatchRFC 6902
パッチをJSONで表現
PATCHメソッドで送信
JSON Pointer (RFC 6901)で指定したJSONパスへの操作を表現
application/json-patch+jsonpatch(diff(a, b), a) === b
を満たすこと
JSON Patch
[{"value":"a"},{"value":"b"},{"value":"c"}]
[ {"op":"add","path":"/3","value":{"value":"d"}}, {"op":"remove","path":"/1"}]
[{"value":"a"},{"value":"c"},{"value":"d"}]
Original
Modified
Patch
典型的なREST
POST /todos {"title":"fizzbuz","done":false }
PUT /todos/1 {"title":"fizzbuz","done":true }
PATCH /todos/2 {"done":true }
DELETE /todos/3
典型的なREST
POST /todos {"title":"fizzbuz","done":false }
PUT /todos/1 {"title":"fizzbuz","done":true }
PATCH /todos/2 {"done":true }
DELETE /todos/3HTTP通信回数=操作回数 リソースが増えるとさらに増える
PATCH /todos [ {"op":"add","path":"-","value": {"title":"fizzbuzz","done":false}}, {"op":"replace","path":"/1","value": {"title":"fizzbuzz","done":true}}, {"op":"replace","path":"/2/done", "value":true}, {"op":"remove","path":"/3"}]
JSON PatchがあるREST
PATCH /todos [ {"op":"add","path":"-","value": {"title":"fizzbuzz","done":false}}, {"op":"replace","path":"/1","value": {"title":"fizzbuzz","done":true}}, {"op":"replace","path":"/2/done", "value":true}, {"op":"remove","path":"/3"}]
JSON PatchがあるREST
HTTP通信回数が1回 リソースが増えても1回 操作もアトミック
Spring Sync
* https://github.com/spring-projects/spring-sync * https://github.com/spring-projects/spring-sync-samples * https://spring.io/blog/2014/10/22/introducing-spring-sync
@Configuration@EnableDifferentialSynchronizationpublic class DiffSyncConfig extends DiffSyncConfigurerAdapter { @Autowired private PagingAndSortingRepository<Todo, Long> repo; @Override public void addPersistenceCallbacks(PersistenceCallbackRegistry registry) { registry.addPersistenceCallback( new JpaPersistenceCallback<Todo>(repo, Todo.class)); }}
Spring (MVC)でJSON Patchを扱うためのプロジェクト
まだ1.0.0.RC1 乞うご期待
Securing REST Services
どっちが好き?
• HttpSessionを使わない
• HttpSessionを使う
どっちが好き?
• HttpSessionを使わない
• HttpSessionを使う
KVSにデータを保存
OAuth 2.0を利用
OAuth 2.0
•アクセストークンを使って認可する標準的な仕組み
•多くのAPIプロバイダがOAuthによるリソースアクセスを提供
OAuth2.0の基本
Resource Owner Client
Resource Server
Authorization Server
OAuth2.0の基本
Resource Owner Client
Resource Server
Authorization Server
Github APIの例
OAuth2.0の基本
Resource Owner Client
Resource Server
Authorization Server
Githubの アカウント管理
Github API
Github API を使ったサービス プロバイダ(アプリ)
エンドユーザー (Githubユーザー)
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
サービスへ リクエスト
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何かサービスへ リクエスト
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何か
アクセストークン
サービスへ リクエスト
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何か
アクセストークン
アクセストークン
サービスへ リクエスト
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何か
アクセストークン
アクセストークン
リソース
サービスへ リクエスト
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何か
アクセストークン
アクセストークン
リソース
サービスへ リクエスト
サービスからの レスポンス
OAuth2.0の流れ
Resource Owner Client
Resource Server
Authorization Server
何か
アクセストークン
アクセストークン
リソース
サービスへ リクエスト
サービスからの レスポンス
ここの方式 (どうやってアクセストークンを交換するか)
=GrantType
Grant Types
• Authorization Code • Resource Owner Password Credentials • Client Credentials • Refresh Token • Implicit • JWT Bearer
Grant Types
• Authorization Code • Resource Owner Password Credentials • Client Credentials • Refresh Token • Implicit • JWT Bearer
Authorization Code (grant_type=authorization_code)•認可コードとアクセストークンを交換
•一般的にOAuthと思われているやつ
画像: http://www.binarytides.com/php-add-login-with-github-to-your-website/
Resource Owner Client
Resource Server
Authorization Server
Resource Owner Client
Resource Server
Authorization Server
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Pageログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Pageログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
認可コード
ログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
認可コード
ログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
認可コード
認可コード
ログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
認可コード
認可コード
認可コード
ログイン
サービスへ リクエスト
Resource Owner Client
Resource Server
Authorization Server
Login Page
認可コード
認可コード
アクセストークン
認可コード
ログイン
サービスへ リクエスト
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
GET /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
302 FoundLocation: https://client.example.com/cb?code=0fcfa4625502c209702e6d12fc67f4c298e44373&state=xyz
認可コード取得
認可コード取得POST /tokenAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW grant_type=authorization_code&code=0fcfa4625502c209702e6d12fc67f4c298e44373&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
client_id:client_secretをBase64エンコード
認可コード取得POST /tokenAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW grant_type=authorization_code&code=0fcfa4625502c209702e6d12fc67f4c298e44373&redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb
client_id:client_secretをBase64エンコード
200 OK!
{"access_token":"e651bdf91e704c0f3d060ffd4ff0403eb087f519","expires_in":3600,"token_type":"bearer"}
アクセストークン取得
アクセストークン取得
GET /api/friendsAuthorization: Bear e651bdf91e704c0f3d060ffd4ff0403eb087f519
リソース取得
Resource Owner Password Credentials (grant_type=password)
•ユーザー名・パスワードとアクセストークンを交換
• Clientが直接ユーザー名・パスワードを知ることになるので、通常公式アプリで使用される。
Resource Owner Client
Resource Server
Authorization Server
Authorization Serverと提供元が同じ
Resource Owner Client
Resource Server
Authorization Server
ユーザー名・ パスワード
Authorization Serverと提供元が同じ
Resource Owner Client
Resource Server
Authorization Server
ユーザー名・ パスワード
アクセストークン
Authorization Serverと提供元が同じ
POST /tokenAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW!grant_type=password&username=demouser&password=testpass
client_id:client_secretをBase64エンコード
POST /tokenAuthorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW!grant_type=password&username=demouser&password=testpass
client_id:client_secretをBase64エンコード
200 OK!
{"access_token":"e651bdf91e704c0f3d060ffd4ff0403eb087f519","expires_in":3600,"token_type":"bearer"}
アクセストークン取得
アクセストークン取得
GET /api/friendsAuthorization: Bear e651bdf91e704c0f3d060ffd4ff0403eb087f519
リソース取得
Spring Security OAuth• Spring Securityの拡張でOAuthに対応
•認証認可に加え、トークン管理、クライアント管理等
• OAuth認可サーバー、クライアントの実装が簡単
•標準のGrantTypeは用意済み。カスタムGrantTypeも実装可能
Resource Owner Client
Resource Server
Authorization Server
Spring Security OAuthサーバーの場合
Resource Owner Client
Resource Server
Authorization Server
OAuth2RestTem
plate
Spring Security OAuth
クライアントの場合
Resource Owner
Client (curl) Resource
Server (Bookmark)
Authorization Server
ユーザー名・ パスワード
アクセストークン
Bookmark APIの例
<dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.0.5.RELEASE</version></dependency>
まだSpring Boot用の AutoConfigure/Starterはない
Spring Securityの認証設定@Configurationclass WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter { @Autowired AccountRepository accountRepository; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } @Bean UserDetailsService userDetailsService() { return (username) -> accountRepository .findByUsername(username) .map(a -> new User(a.username, a.password , true, true, true, true, AuthorityUtils .createAuthorityList("USER", "write"))) .orElseThrow( () -> new UsernameNotFoundException(…)); }}
Spring Securityの認証設定@Configurationclass WebSecurityConfiguration extends GlobalAuthenticationConfigurerAdapter { @Autowired AccountRepository accountRepository; @Override public void init(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()); } @Bean UserDetailsService userDetailsService() { return (username) -> accountRepository .findByUsername(username) .map(a -> new User(a.username, a.password , true, true, true, true, AuthorityUtils .createAuthorityList("USER", "write"))) .orElseThrow( () -> new UsernameNotFoundException(…)); }}
ユーザー名から認証ユーザーを取得するインタフェース
@Configuration @EnableResourceServerclass OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer r) { r.resourceId("bookmarks"); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .anyRequest().authenticated(); }}
ResourceServerの設定
@Configuration @EnableResourceServerclass OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer r) { r.resourceId("bookmarks"); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .anyRequest().authenticated(); }}
ResourceServerの設定
リソースID
@Configuration @EnableResourceServerclass OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer r) { r.resourceId("bookmarks"); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .anyRequest().authenticated(); }}
ResourceServerの設定
リソースID
HTTPセッションを使わない!!
@Configuration @EnableResourceServerclass OAuth2ResourceConfiguration extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer r) { r.resourceId("bookmarks"); } @Override public void configure(HttpSecurity http) throws Exception { http.sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.authorizeRequests() .anyRequest().authenticated(); }}
ResourceServerの設定
リソースID
HTTPセッションを使わない!!
認可設定
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
APIにアクセスするクライアントを登録 (今回はデモ用にインメモリ実装)
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
client_idとclient_secret を設定
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
対象のclientに許可するgrant_typeを指定
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
対象のclientに許可するロールとスコープを指定
AuthorizationServerの設定@Configuration @EnableAuthorizationServer class OAuth2AuthorizationConfiguration extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Override public void configure(AuthorizationServerEndpointsConfigurer ep) throws Exception { ep.authenticationManager(authenticationManager); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("demoapp").secret("123456") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .authorities("ROLE_USER") .scopes("write") .resourceIds("bookmarks"); }}
clientに対応する リソースIDを指定
リソースアクセス$ curl -v http://localhost:8080/bookmarks(略)< HTTP/1.1 401 Unauthorized(略)< WWW-Authenticate: Bearer realm="bookmarks", error="unauthorized", error_description="An Authentication object was not found in the SecurityContext"(略){"error_description": "An Authentication object was not found in the SecurityContext","error": "unauthorized"}
トークン発行$ curl-X POST -u demoapp:123456 http://localhost:8080/oauth/token -d "password=password&username=making&grant_type=password&scope=write"!
{"access_token":"5f4b1353-ddd0-431b-a4b6-365267305d73","token_type":"bearer","refresh_token":"a50e4f67-373c-4f62-bdfb-560cf005d1e7","expires_in":4292,"scope":"write"}
リソースアクセス$ curl -H 'Authorization: Bearer 5f4b1353-ddd0-431b-a4b6-365267305d73' http://localhost:8080/bookmarks!
{ "content": [ { "links": […], "book": {…} } ], …}
HTTPS対応
$ keytool -genkeypair -alias mytestkey -keyalg RSA -dname "CN=Web Server,OU=Unit,O=Organization,L=City,S=State,C=US" -keypass changeme -keystore server.jks -storepass letmein
•設定ファイル(application.yml)に設定を書くだけで簡単SSL対応
server: port: 8443 ssl: key-store: server.jks key-store-password: letmein key-password: changeme
いつも通り起動$ mvn spring-boot:run… (略)2014-12-13 12:07:47.833 INFO --- [mple.App.main()] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8443/https2014-12-13 12:07:47.836 INFO --- [mple.App.main()] com.example.App : Started App in 5.322 seconds (JVM running for 10.02)
いつも通り起動$ mvn spring-boot:run… (略)2014-12-13 12:07:47.833 INFO --- [mple.App.main()] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8443/https2014-12-13 12:07:47.836 INFO --- [mple.App.main()] com.example.App : Started App in 5.322 seconds (JVM running for 10.02)
Spring Security OAuthで ステートレスにRESTを
セキュア化!!
Spring Security OAuthで ステートレスにRESTを
セキュア化!!
スケールブル!!
Spring Security OAuthで ステートレスにRESTを
セキュア化!!
スケールブル!!
って思うやん?
https://spring.io/blog/2015/01/12/the-login-page-angular-js-and-spring-security-part-ii#help-how-is-my-application-going-to-scale
https://spring.io/blog/2015/01/12/the-login-page-angular-js-and-spring-security-part-ii#help-how-is-my-application-going-to-scale
いつまで"ステートレス"で消耗してんの?
(意訳 違訳)
https://spring.io/blog/2015/01/12/the-login-page-angular-js-and-spring-security-part-ii#help-how-is-my-application-going-to-scale
いつまで"ステートレス"で消耗してんの?
(意訳 違訳)
https://spring.io/blog/2015/01/12/the-login-page-angular-js-and-spring-security-part-ii#help-how-is-my-application-going-to-scale
いつまで"ステートレス"で消耗してんの?
(意訳 違訳)セキュリティ対策は何やかんやでステートフル
(CSRFトークンとか。アクセストークンだって広い意味でステート)
これまでのHttpSession を使う場合
HttpSessionのデータを APサーバーのメモリに保存
ロードバランサのSticky Sessionで 同じSessionを同一JVMにバインド
Spring Session•セッションデータをJVM間で共有する新しい選択肢
•新しいセッションAPI
• HttpSessionと統合して、以下を提供
• Clustered Session • Multiple Browser Sessions • RESTful APIs
• WebSocketにも対応
Spring Session•セッションデータをJVM間で共有する新しい選択肢
•新しいセッションAPI
• HttpSessionと統合して、以下を提供
• Clustered Session • Multiple Browser Sessions • RESTful APIs
• WebSocketにも対応
ServletRequest/Response、HttpSessionをラップする
Servlet Filterを提供
Clustered Session
• KVSをつかったセッション
• Redis実装が提供されている
•アプリを跨いだセッション共有も可能
@Import(EmbeddedRedisConfiguration.class)@EnableRedisHttpSession public class SessionConfig { @Bean JedisConnectionFactory connectionFactory() { return new JedisConnectionFactory(); }}
public class SessionInitializer extends AbstractHttpSessionApplicationInitializer { !
public SessionInitializer() { super(SessionConfig.class); }}
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session</artifactId> <version>1.0.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> <version>1.0.0.RELEASE</version> </dependency>
Multiple Browser Sessions
割愛
HttpSession & RESTful APIs
Cookie(JSESSIONID)の代わりに HTTPヘッダ(X-AUTH-TOKEN)に セッション情報を載せる
@Import(EmbeddedRedisConfiguration.class)@EnableRedisHttpSession public class SessionConfig { @Bean JedisConnectionFactory connectionFactory() { return new JedisConnectionFactory(); } @Bean HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); }}
class MvcInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[] { SessionConfig.class, …}; // …}
$ curl -v http://localhost:8080/ -u user:password!
HTTP/1.1 200 OK(略)x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3!
{"username":"user"}
$ curl -v http://localhost:8080/ -H "x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3"
Spring Bootを使うと
@EnableRedisHttpSession class HttpSessionConfig { @Bean HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); } }
Spring Bootを使うと
@EnableRedisHttpSession class HttpSessionConfig { @Bean HttpSessionStrategy httpSessionStrategy() { return new HeaderHttpSessionStrategy(); } }
これだけ! (Redisの設定も不要)
認可設定@Configuration @EnableWebSecurity class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() .and().httpBasic(); } }
サンプルコントローラー
@RestController class AuthController { @RequestMapping String check(Principal principal) { return principal.getName(); } }
$ curl -v http://localhost:8080/ -u making:password!
HTTP/1.1 200 OK(略)x-auth-token: fe1b6d11-9867-4df2-b5bf-a33eb004ac65!
making
$ curl -v http://localhost:8080/ -u making:password!
HTTP/1.1 200 OK(略)x-auth-token: fe1b6d11-9867-4df2-b5bf-a33eb004ac65!
making
$ curl -v -H 'x-auth-token: fe1b6d11-9867-4df2-b5bf-a33eb004ac65' http://localhost:8080/bookmarks!
{"_embedded": {"bookmarkResourceList": [{"_links": {…,"bookmark-uri": { "href": "http://bookmark.com/1/making"}},…}]
APIを3rdパーティに提供したい場合以外、 Spring Session使えばいいんじゃないか?
まとめ
• Spring Boot概要
• RESTについていろいろ
• Richardson Maturity Model / HATEOAS • JSON-Patch • Security (OAuth/Spring Session)
Q&A?
• はじめてのSpring Boot
• http://bit.ly/hajiboot • 今日話した内容のチュートリアル
• http://spring.io/guides/tutorials/bookmarks • 今日のソースコード
• https://github.com/making/tut-bookmarks
top related