ほしいメモ
2021 1/4追記 TypeScriptで実装してとりあえずブラウザで動くようにした github.com
ほしいメモ
2021 1/4追記 TypeScriptで実装してとりあえずブラウザで動くようにした github.com
多重継承できない言語でなるべく誰でもわかるように共通化のために継承を使うべきではない理由をまとめた。 継承を使うべきでない理由は他にもあるがとりあえず一番簡単に納得できそうな理由について記載した。 多重継承できたとしても、共通化のために継承を使うのは個人的にあまり良くないと思っているが、それについては記載しない。
ある程度の規模感があるJavaのコードは責務ごとにクラスを分割するとコードが読みやすくなる(≒全部mainメソッドに書いたコードは読みづらい)。
そもそもコンストラクタでバリデーションしろよ、とかは無視してもとりあえず言いたいことは伝わる気がするので諦めた。
多重継承できる言語だとこの論理では継承よりもコンポジションを主張できない。 リスコフの置換原則の話とかはしない。
Javaで継承による共通化を行うと、多重継承ができないため1つの親クラスに全ての共通処理を入れる必要がある。 つまり処理ごとにクラスを分割することができないし、クラスに適切な名前をつけることもできない(なので多くの場合Baseなんとかクラスになってしまう)。
これを避けるために無理やりクラスを分割すると、全く関連の無いクラス同士を親子関係にする必要が出てくる。 こうなると、あるクラスに手を入れる際に関係ないクラスを破壊していないか意識する必要が出てきて辛い。 関係ないクラス同士に親子関係があると、あとから見たときに混乱するので辛い。
以下のようなコードがあったとする。
public class UserCreator { public User createUser(User user) { if (user.id != null) { throw new IllegalArgumentException("ユーザーIDは必須です。"); } if (user.age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります。"); } // 永続化 return db.create(user); } } public class UserUpdater { public User updateUser(User user) { if (user.id != null) { throw new IllegalArgumentException("ユーザーIDは必須です。"); } if (user.age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります。"); } // 永続化 return db.update(user); } }
ユーザーを永続化する際にバリデーションを行っているが、このバリデーションはCreaterとUpdaterで重複している。 これを継承で共通化すると以下のようになる。
public abstract class UserPersistentBase { public void validateUser(User user) { if (user.id != null) { throw new IllegalArgumentException("ユーザーIDは必須です。"); } if (user.age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります。"); } } } public class UserCreator extends UserPersistentBase { public User createUser(User user) { validateUser(user); return repository.create(user); } } public class UserUpdater extends UserPersistentBase { public User updateUser(User user) { validateUser(user); return repository.update(user); } }
validateUserがUserPersistentBaseに共通化され、UserCreator、UserUpdaterがすっきりした。 次にユーザーが作成されたり更新した際に、WebhookでHTTPリクエストが飛ぶ、という要件が追加されたとする。
public abstract class UserPersistentBase { public void validateUser(User user) { if (user.id != null) { throw new IllegalArgumentException("ユーザーIDは必須です。"); } if (user.age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります。"); } } } public class UserCreator extends UserPersistentBase { public User createUser(User user) { validateUser(user); try { HttpClient client = HttpClientFactory.create(); UserChangeEvent event = new UserChangeEvent(user, WebHookType.CREATE_USER); client.post(System.getProperty("WEBHOOK_URL"), event.toString()); } catch (HttpRequestFailureException e) { // webhookのリクエストエラーの場合は通知のみで処理をすすめる log.error("failed webhook request", event); } return repository.create(user); } } public class UserUpdater extends UserPersistentBase { public User updateUser(User user) { validateUser(user); try { HttpClient client = HttpClientFactory.create(); UserChangeEvent event = new UserChangeEvent(user, WebHookType.UPDATE_USER); client.post(System.getProperty("WEBHOOK_URL"), event.toString()); } catch (HttpRequestFailureException e) { // webhookのリクエストエラーの場合は通知のみで処理をすすめる log.error("failed webhook request", event); } return repository.update(user); } }
Webhookのリクエストを行うコードが重複しているので共通化する。
public abstract class UserPersistentBase { public void validateUser(User user) { if (user.id != null) { throw new IllegalArgumentException("ユーザーIDは必須です。"); } if (user.age < 0) { throw new IllegalArgumentException("年齢は0以上である必要があります。"); } } public void requestWebhook(String message) { try { HttpClient client = HttpClientFactory.create(); client.post(System.getProperty("WEBHOOK_URL"), message); } catch (HttpRequestFailureException e) { // webhookのリクエストエラーの場合は通知のみで処理をすすめる log.error("failed webhook request", event); } } } public class UserCreator extends UserPersistentBase { public User createUser(User user) { validateUser(user); requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString()); return repository.create(user); } } public class UserUpdater extends UserPersistentBase { public User updateUser(User user) { validateUser(user); requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString()); return repository.update(user); } }
UserPersistentBaseにユーザー永続化時のバリデーションとWebhookの送信処理が追加された。 UserだけではなくItemの作成クラスについても考えてみる。この場合も同様にWebhookの送信が必要である。
public class ItemCreator { public Item createItem(Item item) { try { HttpClient client = HttpClientFactory.create(); client.post(System.getProperty("WEBHOOK_URL"), new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); } catch (HttpRequestFailureException e) { // webhookのリクエストエラーの場合は通知のみで処理をすすめる log.error("failed webhook request", event); } return repository.create(item); } }
Webhookの送信処理はユーザーの場合と同様であり、共通化できそうだがそのまま共通化するとこうなる。
public class ItemCreator extends UserPersistentBase { public Item createItem(Item item) { requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } }
UserPersistentBaseを継承したItemCreatorが出来てしまった。 さらにItemについても更新処理とバリデーションを追加する。
public class ItemCreator extends UserPersistentBase { public Item createItem(Item item) { validateItem(item); requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } } public class ItemUpdater extends UserPersistentBase { public Item createItem(Item item) { validateItem(item); requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } }
UserPersistentBaseには現在以下の処理がある。
これはもはやUserPersistentBaseという名前にマッチしない。 PersistentBaseに変更しても良いが今後永続化するリソースが増えたり共通化したい処理が増えるたびにPersistentBaseに処理が追加され続けると、PersistentBaseが肥大化する。 この状況はもはや責務ごとに処理が分割されているとは言いがたい。
責務ごとにクラスを分けつつ継承で共通化した場合、以下のようになる。
public abstract class UserValidator extends WebhookRequester { public void validateUser(User user) { } } public abstract class ItemValidator extends WebhookRequester { public void validateUser(User user) { } } public abstract class WebhookRequester { public void requestWebhook(String message) { } } public class UserCreator extends UserValidator { public User createUser(User user) { validateUser(user); requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString()); return repository.create(user); } } public class UserUpdater extends UserValidator { public User updateUser(User user) { validateUser(user); requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString()); return repository.update(user); } } public class ItemCreator extends ItemValidator { public Item createItem(Item item) { validateItem(item); requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } } public class ItemUpdater extends ItemValidator { public Item createItem(Item item) { validateItem(item); requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } }
UserPersistentBaseがUserValidator、ItemValidator、WebhookRequesterに分割された。 コードは確かに分割されたが、UserValidatorにはWebhookRequesterから継承されたrequestWebhookが生えている。
UserValidatorはバリデーションを行うためのクラスだが、バリデーション以外のことをしている。名前と責務が一致していない。
しかも、UserValidatorはWebhookRequesterに依存したり、overrideによってWebhookRequesterの処理に影響を与える事ができる。 例えば、WebhookRequesterの修正によってバリデーションを壊したり、UserValidatorの修正によってWebhookの処理を壊したりする可能性がでてくる。 つまりそれぞれの処理に手を入れる際、親クラスや子クラスを気にしながら手を入れる必要がある。
さらにこの構造では、共通処理が増えるたびに継承の階層が深くなり、何が何を継承しているのかを判断するのが難しくなってくる。 この階層はJavaの制約を満たすためだけに存在する階層で本質的には不要であり、複雑さを増している。
継承をやめてコンポジションで共通化を行うと以下のようになる。
public class UserValidator { public void validateUser(User user) { } } public class ItemValidator { public void validateItem(Item item) { } } public class WebhookRequester { public void requestWebhook(String message) { } } public class UserCreator { UserValidator validator = new UserValidator(); WebhookRequester webhookRequester = new WebhookRequester(); public User createUser(User user) { validator.validateUser(user); webhookRequester.requestWebhook(new UserChangeEvent(user, WebHookType.CREATE_USER).toString()); return repository.create(user); } } public class UserUpdater { UserValidator validator = new UserValidator(); WebhookRequester webhookRequester = new WebhookRequester(); public User updateUser(User user) { validator.validateUser(user); webhookRequester.requestWebhook(new UserChangeEvent(user, WebHookType.UPDATE_USER).toString()); return repository.update(user); } } public class ItemCreator { ItemValidator validator = new ItemValidator(); WebhookRequester webhookRequester = new WebhookRequester(); public Item createItem(Item item) { validator.validateItem(item); webhookRequester.requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } } public class ItemUpdater { ItemValidator validator = new ItemValidator(); WebhookRequester webhookRequester = new WebhookRequester(); public Item createItem(Item item) { validator.validateItem(item); webhookRequester.requestWebhook(new ItemChangeEvent(item, WebHookType.CREATE_ITEM).toString()); return repository.create(item); } }
UserValidator、ItemValidator、WebhookRequesterは完全に独立していて各クラスの名前と処理の剥離も無い。 完全に独立しているため、WebhookRequesterに手を入れる時はWebhookRequesterのことだけを意識すればよい。
上記のような状態になってしまうことを避けたいという点だけでも継承よりコンポジションを選べる状況なのであれば、コンポジションを選ぶべきだと思う。
共通化ではなく拡張のために使うものだと思っている。思っています。
Javaで継承による共通化を行うと、多重継承ができないため1つの親クラスに全ての共通処理を入れる必要がある。 つまり処理ごとにクラスを分割することができないし、クラスに適切な名前をつけることもできない(なので多くの場合Baseなんとかクラスになってしまう)。
これを避けるために無理やりクラスを分割すると、全く関連の無いクラス同士を親子関係にする必要が出てくる。 こうなると、あるクラスに手を入れる際に関係ないクラスを破壊していないか意識する必要が出てきて辛い。 関係ないクラス同士に親子関係があると、あとから見たときに混乱するので辛い。
わかんない
このまとめに書いてあるこれからやること以外のことばかりしている
PLとかBSとかなんもわからんという話をしたら会社の人に教えてもらった
これは大変によかった
この辺なんもわからんのをなんとかしようと思って最初簿記の本読んでたんだけど、なんとなく財務諸表の作り方はわかるけどなんでこういう構造になってるのかさっぱりわからん状態になったがそのあたりの疑問がかなり解消された
個人的にこの本は複式簿記の勉強を始めるよりも前に読むとその後の学習がかなり効率よくなる気がする 表をいったりきたりしながら読むのがちょっとつらいので前ページの表と比べなくても差分がわかる構成になっているともっと読みやすい気がした 電書だとそのへんが特に辛いかも でもこれはよかった
GCなんもわからんやばいという話をしたら会社の人に教えてもらった
JPAあたりからなんとなく興味がどっかいってしまったがGCの下りはわかりやすくてよかった
わーっと読んだだけで全然触ってないので触る必要がある
クソアプリアドベントカレンダー用にクソアプリを書いたりしていた
資格関連ばっかやってた あとは社内で全員 OAuth 2.0 ちゃんと理解しよう勉強会をやってた
とった inabajunmr.hatenablog.com 1ヶ月でいけるだろと思って予約したら割とギリギリなかんじでこればっかりになってしまった
150ページくらいまでよんだ 読み始めてから資格のほうが以外とやばいことに気づいてそっちにシフトしたので停滞していた GCとかJVMの話面白いのでもうちょっと掘り下げたい気持ちがでてきた とりあえず最後まで読む
9月はシャドーイングしたり単語力の衰えを感じたので
をやったりしていた 10月は停滞していた
社内で参加者 OAuth 2.0 全員理解するぞ勉強会をやった
参加者に質問してもらったやつでなんでだっけ?ってなったのがあったので整理した
あとはなんかポエムかいてた
息の長いプロダクトのコードを見ていると、「なぜか本番にリリースされていない機能」とか「誰もしらない機能」のコードを見つけることがある。 開発途中で機能が不要になったり、一度リリースした機能を潰したり、といった事情は色々あるがとにかくこのようなコードはたまに見つかる。 さらに退職などの理由でこのようなコードについての背景が誰にもわからなくなっていることもある。
そして、その機能がやはり欲しくなったり、コードを部分的に流用したい、といった場合に、このコードをそのまま使いたくなることがある。
この時、このコードを「既にあるから」という理由で信用したくなる時がある。 これはググったら出てきたコードをよくわからないままコピペしたくなる感覚と似ている気がする。 しかし、コピペはNG、という価値観を持っていても既にあるコードはOK、と判断してしまうことがある気がしている。
「既にあるから」という理由でそのコードを信じてはいけない。 コードの背景がわからないということは、そのコードの品質が担保されているのかわからない、ということである。
品質はテストや本番での動作実績に担保されるが、「既にあること」は品質を担保しない。 プロダクトの品質を担保する基準があるのであれば、「既にあるコード」を利用する場合もそのコードが基準を満たしているか確認する必要がある。 満たしていないのであればテストを追加するなどして、基準を満たす必要がある。 ただし、基準を満たしていて工数の削減につながるのであれば、コードの流用はどんどんするべきだと思う。
ついで
コードを書く人的なロールでAWSは触っている が、日常的には触るのはSNS、SQS、DynamoDBくらい とはいえメジャーな?サービスはどういうものなのか?くらいは理解している
後はあそびでEC2立てたりECSタスクかいてアプリをデプロイしたりはする
2017年に以下を取得しているが、ろくにAWS触ってない状態で知識だけ詰め込んだので今受けたらアソシエイトも多分おちる
セキュリティ系のサービスはIAMはなんとなくわかるけどほかは全然わからん、くらいの感じ
40時間くらい
いろんな合格記で紹介されてるやつとだいたい同じような感じ 流れ的には「資格本1冊読む」→「公式の模試」→「公式のトレーニング(Exam Readiness)」→「BlackBelt読みながらちょくちょくガイドみる」→「公式の模試」→「受験」
ホワイトペーパーは一切読んでない 読んでないので良し悪しがわからん
主にBlackBeltとガイド ガイドは網羅的によむというよりもBlackBelt見てて気になったところを見ていった
実際に動かしてどうこうもしてない(すべき)が、Trusted Advisorのチェック項目はマネコンで眺めたりしていた
何したらいいのかわからんのでとりあえず買って読んだ セキュリティ系のサービスそもそもなんなのみたいのがわかる これだけで受かるかというと多分受からない
ADの説明がわかりやすいのと回答付き模擬試験がまあまあなボリュームであるのは嬉しい あとは何の勉強したらいいのリストを作るのに使える が、今考えると別に読まなくてもよかった感はある 回答付きの模擬試験を解きたい人は買おう
なぜ本を読まなくてもいいかというとこのトレーニングでよさそうだったから 何の勉強したらいいのリストを作るのに使える それ以外はまあなんかいいかなという感じのやつ 回答のコツ、みたいのも紹介されてた気がする
Exam Readinessで紹介されてたサービスのBlackBeltを片っ端から読むのが勉強のメイン 実際に読んだやつの一覧は以下 なんか漏れてるかもしれんけどだいたいこれで全部だと思う
特にCloudWatchとSecretManagerはスルーしないほうがよさそう スルーした
公式の模試を2回やった 初っ端いきなりやって50パーくらいで次に試験前日にやったら75パーだった かなりギリギリ感 本番のスコアは見方がよくわからないので何パーだかわからん
いきなりBlackBelt、はそこまで間違ってた感じではないけど動画だと時間かかるので、公式のガイド見てピンとこないやつだけBlackBelt、のが効率いい気がした
ガイドのがわかりやすかったり網羅的だったりおもしろかったりするので ガイド読まなすぎてかなりギリギリだった感ある というかそもそも明らかに試験にでそうなサービスを見切れてないのがまずかった
あとおもしろはんぶんでプラクティショナーも同日にうけた こっちは準備してなくても受かったけどぜんぜん知らん範囲の問題が多かった
AWSのセキュリティにくわしくなったかもしれない
OIDC で redirect_uri の登録と完全一致が必須だという前提で物事を考えていたため、どういう攻撃が成立するのかわからず混乱した。 微妙に確信が持てないので間違ってたらおしえてください。
認可要求に認可コードグラントタイプを用いるとき, クライアントは redirect_uri パラメーターによりリダイレクトURIを指定できる. 攻撃者がリダイレクトURIの値を操作可能であるとき, 認可サーバーによってリソースオーナーのユーザーエージェントを攻撃者の管理下にあるURIに認可コードを含んだ状態でリダイレクトさせることができる.
攻撃者は, 正しいクライアントにおいてアカウントを作成し, 認可フローを開始することができる. 攻撃者のユーザーエージェントがアクセス許可のために認可サーバーに送られたとき, 攻撃者は正当なクライアントにより提供された認可URIを取得し, クライアントのリダイレクトURIを攻撃者の管理下にあるURIに交換する. 攻撃者はその後, 正当なクライアントに向けた操作された認可アクセスリンクをたどるよう被害者を騙す.
認可サーバーにおいて, 被害者は正当で信頼できるクライアントによる正常で有効なリクエストを促され, そのリクエストを認可する. 被害者はその後, 認可コードとともに攻撃者の管理下にあるエンドポイントにリダイレクトされる. 攻撃者は, クライアントにより提供されたオリジナルのリダイレクトURIを用いて, 認可コードを送ることにより認可フローを完了する. クライアントは認可コードとアクセストークンを交換し, それを攻撃者のクライアントのアカウントと紐づけることで, 被害者により (クライアント経由で) 認可された保護されたリソースへのアクセス権を獲得できる.
このような攻撃を防ぐため, 認可サーバーは認可コードの取得に用いたリダイレクトURIと, 認可コードとアクセストークンの交換時に提供されたリダイレクトURIが同一であることを確認しなければならない (MUST). 認可サーバーはパブリッククライアントに対してリダイレクトURIの登録を要求しなければならず (MUST), コンフィデンシャルクライアントに対してもリダイレクトURIの登録を要求すべきである (SHOULD). リダイレクトURIがリクエストにより提供されたとき, 認可サーバーは登録された値を用いてそれを検証しければならない (MUST).
認可リクエストの redirect_uri に攻撃者が管理しているサイトの URI を指定されたときに、認可コードが攻撃者のサイトに飛ぶ。 その認可コードを正規のクライアントにコールバックとして送信すると他人のアクセストークンを保持したクライアントを操作できる。
認可リクエストに以下のようなパラメーターを指定して、被害者に踏ませる。
client_id=攻撃対象のクライアント&redirect_uri=https://攻撃者のドメイン.example.com
認可コードが https://攻撃者のドメイン.example.com
に送られるので、攻撃者は被害者が認可を行った認可コードを手に入れる。
攻撃者はこの認可コードをクライアントの本来の redirect_uri に送信する。 このときトークンエンドポイントが redirect_uri の検証を行っていない場合、クライアントが認可コードとトークンの引き換えに成功する。 認可を行ったのは攻撃者ではなく被害者なので、トークンは被害者に紐付いている。
この状態で攻撃者がリソースサーバーに対して自らのプロフィールを取得する API を利用するような操作を行うと、プロフィールは被害者のものとなり、攻撃者は被害者の個人情報を取得することが出来る。
redirect_uri の検証を行った場合、認可レスポンスを受け取った攻撃対象のクライアントは、トークンエンドポイントに https://攻撃者のドメイン.example.com
ではなく https://攻撃対象のドメイン.example.com
を送信する。(そもそも攻撃対象のクライアントは攻撃者のredirect_uri を知らない)
こうなると認可リクエストで送信した redirect_uri とトークンエンドポイントに送った redirect_uri が別の値になるので、攻撃が失敗する。 ようするに認可リクエストを行ったときに指定した redirect_uri にちゃんとコールバックで認可コードが渡って、その redirect_uri のクライアント自身がトークンリクエストをしているよね?というところが担保できる。
認可リクエストの client_id は正規のクライアントとなる。
認可コードをアクセストークンに引き換えには
が必要だが、認可リクエストの client_id が正規のクライアントのものなのでどちらの条件も満たしている。
トークンエンドポイントで redirect_uri の検証をしている場合、認可コードを送信した redirect_uri とトークンリクエストを送ってきたクライアントが想定している redirect_uri が異なるので、攻撃を防ぐことが出来る。
3.1.2.2. 登録要件より引用。
認可サーバーは, 以下のようなクライアントに対してリダイレクトエンドポイントの事前登録を要求すること (MUST): パブリッククライアント. インプリシットグラントタイプを利用するコンフィデンシャルクライアント. 認可サーバーは, すべてのクライアントに対して, 認可エンドポイントアクセス前にリダイレクトURIの事前登録を要求すべきである (SHOULD).
この攻撃は認可コードグラントのみを使うコンフィデンシャルクライアントに対しても有効だが、その場合の redirect_uri の事前登録は MUST ではない。 また、マッチングの条件については以下の記述がある。
認可サーバーは, クライアントに (一部分ではなく) 完全なリダイレクトURIの登録を要求するべきである (SHOULD). (クライアントはリクエスト毎のカスタマイズするために state リクエストパラメーターを使用できる (MAY)) 完全なリダイレクトURIの登録を要求することができない場合, 認可サーバーはURIスキーム, オーソリティー, パス (認可要求の時, リダイレクトURIのクエリーコンポーネントのみ動的に変更を許可する) の登録を要求するべきである (SHOULD).
この SHOULD を守っていない場合、パスに指定した値によってオープンリダイレクタが可能になっていると、redirect_uri を事前登録していてもこの攻撃は成立する気がする。
シーケンス書いてから見直したらまるっきり同じ話だった。