杖をまた購入
どうも杖エンジニアのあるかなむです。自分自身ではロマンスグレーな髪を颯爽としながら杖をコツコツと歩き渋い感じだと思っていますが、はたから見ればただのジジイなのでしょう。また50前なのに。
どうも今使っている杖が壊れ始めて危ないのでまた購入しました。前回と同じKINGGEARの折りたたみ杖です。自分の場合、お客さんにはあんまりこういうの見せたくないので折りたたんでカバンにいれておけるのがいいなと使っています。
KINGGEARブランド 4本折りたたみ ステッキ 杖 5段階長さ調整 母の日 父の日 シニア向けプレゼントに最適 ! 日本正規代理店 (ブリリアントブルー)
- 出版社/メーカー: 平成電子株式会社
- メディア: ヘルスケア&ケア用品
- この商品を含むブログを見る
ヘルニア関係でもう足が動かないため杖を購入したのが去年、2018年の4月末に購入してGWに届いてから使っていたのでかれこれ1年ちょっとでした。意外と壊れやすいのか、それとも自分の畳んで伸ばしての頻度が高すぎたのかはわかりません。
さて、届いてAmazonなのでブツは例によって例のごとく巨大な箱に入ってくるのですが、こんな風に梱包されてきます。
開けるとこんな感じですね。杖本体が折りたたまれた状態になって、あと収納袋、説明書が入ってきます。
あれ?おまけでついてくる収納袋がちょっと前回と違う・・・これは今回のもの。裏面も同じ生地です。
これは前回ついてきた収納袋
裏地はこんな感じでメッシュになっていて入れると中身がちょい見える。まぁ自分はいらないかなぁって思っていたけど、まさかのグレードダウン・・・?
あと握る部分とシャフトを連結している削り出し部分がちょっと変わってました。上は今回購入、下は前回購入。ちょっと安っぽくなったような気がします。以前のは曇ったヘアライン風にピカピカな丸が所々ついていてかっこよかったのですが。
見えづらいですがシャフトの連結部分の形状も変わってますね。左が今回購入、右が前回購入。前回のは直線的でゴツい感じだったのですが新しいのは連結部がテーパーになっています。
これの違いは連結後にちょっと変わってきますね。こんな感じで上が今回購入したものですが、連結部がテーバーに削り出しているためか、きっちりおさまる感じになっています。
これは下のゴムの部分。ついてきたゴムは大きくてゴツくてダサいのですぐ付け替えますが。
畳んだときに束ねる輪っかです。プラスチックじゃない何かでできてます。上は今回購入したのに付いてきたもの。下は古い方です。結構これが便利ですね。古いのは壊れたのですがガムテで付けて使ってました。
さて、杖を使うからといって年から年中杖を使わないと歩けないわけではありません。普通に杖なしでも歩きますが、ヨタヨタする割には前から人が来て対面するときには自分もよけなくてはならないので非常に大変なんです。
そりゃ人から見れば前からくる危ないフラフラしたオッさん酒でも飲んでるのか?だし他人が歩くのも大変だなんてわかりませんし、オッさんお前がよけろよ!がデフォでしょうから。
そこで杖ですが街中を歩くときにさすがに杖はアイコンになるのですね。前から来る人たちはよけてくれます。老若男女、DQNも工事現場のおにいさんも。さすがに前から来た行動ののろい三本足クリーチャーが見えると大変だなって感じてよけてくれるのですね。いや、キモっ!!魂が近寄っちゃいけないって叫んでる!かもしれませんが。ありがたいことです。トトロのこのシーンですね。
さつき「木がよけてる!」
ジジババになると道でよけないのなんでだよ?お互い様だろ?って思っていましたが、結構体が言うこときかないと言う事はこう言う事なんだろうなと最近思うわけです。よけないのではなくて、よけられない、なのだろうと。
自分の場合は基本的に前から人が来た場合、よけるのが辛いので一旦止まる、止まってやり過ごすが行動として身についてしまいました。
でも街中でよけてくれない人がいます。
ゾンビです
スマホ見ながら歩くアレですね、まぁギリギリではよけてくれたりするんですが正直、杖使いはギリギリやられると辛いのですよね。あぁ、この方このまま自分にぶつかるんだろうか・・・心臓に悪いです。
意外と杖で歩くときのパーソナルスペースって横方向に1.5倍ほど広くなるので、それが分からずにスマホに夢中なゾンビは直前で杖が足元に見えて「あっww」とか言いながら蹴っ飛ばして行く輩もいます。若い女性に多いのですが「チッ!」っとムカついて去っていく方もいます。いや、前見ないで歩いて危ないの貴方なのですが・・・
ゾンビは死んでますし考える力なんて無いでしょうから前から来る人がどんな状況か?なんて関係ないのでしょう。たまに池に捕獲された映像も見ますしね。千鳥足でアッチへフラフラ、こっちへフラフラ、唯一の餌であるスマホにかじりつきながら歩きます。死んでるんだからもう成仏しろよ!って思いますが。
あと、電車ですね。まぁキモくて汚いオッさんは社会的なランクとしては一番最下層にいる上に杖なんてついてる激弱君はそのまた下層にいる人種。気にする必要はない存在です。いない存在。多分皆の目には僕の体は透過画像の透過部分のように市松模様に見えてるかもしれません。蚊みたいなもんでしょ。まぁ、、席を譲られると言うことはまずありません。そんなもんです。むしろ蚊みたいに弱いアピールするんじゃねぇ、邪魔だなぁ俺の気持ちを逆なですんな!なんて思われているかもしれませんね。
ガッツリ壁際によりかかって杖で支えます。日本一高い仙台地下鉄は必ず進行方向の左側のドアは開かないようになっているため、自分みたいなのは助かります。ドアと椅子の間にある角の吹き溜まりが僕の居場所です。ダンゴムシみたいな気持ちです。
ただ、どうしても杖を持つ手は下になるため、正直満員電車になると女性が乗ってくるのが怖いのですが。
最近は腰の痛みがまた出て来たらしく、あぁ、根本的に直さないとだめだよなぁと思いつつ、毎日、椅子から立ち上がって歩き出すと野球部の兄からよく聞いたケツバットの痛みが走ります。オールウェイズケツバーーット!!
体は大事にしましょう。
ずん子さん、革のコースター製作
先日書いたこちらですが・・・
実はこのコースターを作る際に他にもコースターを作っていたのでご紹介。
革の表面に何かしらずん子さん的なものの彫刻を施して、あと中央に印象的な何かを埋め込んだら良いものができるのではないかと思い、製作。コースターを見る為と使う為の自分なりの工夫です。
コースターは通常カップを置くので中央に何かしらあっても見えませんがこの方法なら置いている間もずん子さんがわかるため良いのではないかと。
皮を切ってイメージはこんな感じになるのでしょうか・・・
皮2枚でずん子さんのプレートを挟むように作ります。こんな風に接着剤でがっちりと接着しちゃいます。
これに絵を綺麗に見せるために周りに縁取りの金の輪っかをつけてレジンで表面を固めます。
実は2枚とも接着剤で接着しちゃえば糸で縫う必要などないのですが見せるために縫ってみます。この作業は初めてです。こんな風に糸を通すための線を作ります。向こうに見えるコースターは別件で作ったのですが正直見せられる代物にはなりませんでした。
1辺だけ縫ってみました。これをあと3回すると思うと気が重いですね・・・
さて、全て縫いました。もっと綺麗に縫えれば最高なのですが、自分の実力はこんなもんです。難しい!
皮を使ったものは初めてでした。糸がロウで固められていて、縫うたびに指の温度で溶け出して、手がベタベタになり、で縫うのが苦労しました。革で何かを作る人ってこう言った苦労があるのかしら?
うーん、、正直また作れと言われたら・・・無理です。ごめんなさい。
ちゃんちゃん。。。
人は3度死ぬ(ポエム)
最近、邦画見るようになって、面白いなとおもったのが、「四月は君の嘘」や、「君の脾臓を食べたい」などの主人公の身近な人が若くして死ぬという作品。
多分だけどこの出来事によって、主人公たちはその瞬間瞬間では深い悲しみや色々な感情、影響を受けるのだろうけど、長い年月でみるとすっかり忘れるって事なんだよねと。
そして長いあいだ忘れて突然思い出すんだ。でもおっさんになって、そんな悲しい別れはなかったけど、昔のふとした記憶が蘇ってくるって事。突然思い出してあぁそういえばそんな事があったなぁと。そしてその人を思い出す。そして自分が死ねばその人の記憶は本当になくなる。
人は2度死ぬというが、人は死ぬと他人に強烈な影響を与えるが、忘れ去れる、2度目の死。そして潜水艦が浮上するように思い出させ、また生き返る。そしてその人が死ぬと自分がいたという記憶は全て失われ、そして3度目の死が訪れる。
とくにオチはありません
東北3姉妹、ずん子さん、きりたん、イタコ姉のコースターセットを作る、その2
これはこちらの続きです。
前回は切るところまでは行いました。今回はデータ作成と実際のUVプリンタでの印刷です。前回作ったコースターデータだけでは実は30分以内には終わってしまうため、施設のUVプリンタを使う時間が余ってしまいます。そのため、今回はコルクコースターへも印刷してみました。
データとしてはこんな感じです。こちらはヒノキのデータ
こちらはコルク用のデータ。今回は凝ったことはしませんでした。
これを当日印刷します。
印刷してみる
データをバーサワークスに転送します。毎回何が緊張するかって、このソフトで印刷直前まで持っていく事ですね。データは大丈夫か、セッティングは簡単になっているけど、どんな風に印刷されるだろう・・・って・・・
はじめにガイド線を印刷して・・・
そこに先日切ったヒノキの部品を置き、印刷を開始します。これを2回ほど繰り返します。ヒノキの板の置く方向を間違えてしまい焦ったのは秘密です。
ヒノキは反りやすいと聞いていましたが、今回レーザーで切ってから2週間ほど放っておいたのですが、微妙に反ってました。というか、購入して商品到着時からホント微妙に反っていたのですが。これは木の性質上仕方がないことです。反らないのが欲しいなら合板などを検討すべきなんですね。でも一枚板のヒノキはとてもいいですね。
UVプリンタは設定でヘッドが印刷媒体に当たらない程度に目視で近づける必要があるため、反ってると非常に難しいのです。反ったせいもありヘッドを微妙に上に持っていく必要があり、印刷があまくならないか心配でした。
印刷中・・・だいたい7分ぐらい。
2枚ほど印刷を終えると30分程度でまだ時間があまりました。予定通り。そこへ今回おまけとしてコースターに印刷します。これもセッティング含めて15分程度。なので今回は15分ほど時間があまりました。勿体無い。
作品を見てみる
一つは失敗作。コースター本体への印刷自身は特に問題ないです。コースターを立てる部品が切りが失敗してしまったものです。あと、印刷時もずん子の木の目の方向を間違えてしまいました。キットとしてみるとずん子だけ違和感があります。ただ、単体で見る分には問題ないです。
ずん子さん、木の目を間違えて印刷してしまいました。木枠から外せば関係ないといえば関係ないのですが。
もう一つは成功したものです。
姉妹の写真
ずん子さん。塗料がちょっとテラテラしてますね。 ねじり梅模様に背景にちょっとだけ黄色をつけてみました。結構いいですね。今回は下地のホワイトを入れませんでしたがヒノキが白いためか不要ですね。いい具合に木の目が見えます。
別の角度からみたものです。テラテラはいい具合に見えなくなりました。木の質感と、ねじり梅がとてもいいと思います。オレスゲー
イタコ姉。こちらも塗料がテラテラしてます。菊模様で下からグラデーションをかけてみました。うん、いいです。浴衣の青と赤の対比がとても良いです。
別の角度からみたもの。テラテラが見えなくなります。
きりたん。亀甲模様です。模様の色合いにグラデーションをかけたくてこんな感じにしてます。なかなかいいと思います。亀甲模様と水風船がちょっと競合しちゃったかな? そこがちょっと勿体無い。
別の角度から見たもの。テラテラは見えなくなります。
こんな感じで台に収納できます。
前面にある小さい部品は取れるようにしておけば、寝付けとして外に持ち出せます。そのための穴も開けてみました。
裏側はよくみると茶色いのが付いています。これはレーザーで切ったときに出来る反射跡ですね。出来るとは言われていたのですが、次に作る際は何か手立てを行う必要があると思います。
コルクコースター
今回は下地のホワイトを入れませんでした。みるとわかりますがちょっと見えづらいところがありますね。
ずん子さんです。
イタコ姉です
きりたんです。きりたんだけちょっと違う感じにしました。
3人並んでます。
先日誕生日だった中国うさぎです。このキャラなんかいいですね。
大江戸ちゃんこ です。めんこいです。公式さん、もっと描いてください。(切実)
さて、上記について、テストとして作成したものなのでBOOTHで安く販売しております。ご興味のある方は以下をどうぞ。買っていただくと次の作品が作れます。どうぞよろしくお願いいたします。
3姉妹コースターセット
ずん子さん東北3姉妹コルクコースターセット(試作版) - arcanum_jp - BOOTH
※1枚づつ商品登録すると複数枚数購入時に複数の送料がかかってしまうため、抱き合わせにしてます。
東北ずん子3姉妹、中国うさぎ、大江戸ちゃんこ
ずん子さん東北3姉妹、大江戸ちゃんこ、中国うさぎ、コルクコースターセット(試作品) - arcanum_jp - BOOTH
※1枚づつ商品登録すると複数枚数購入時に複数の送料がかかってしまうため、抱き合わせにしてます。
東北ずん子さん3姉妹、ヒノキコースターセット(失敗版)
ずん子さん東北3姉妹ヒノキ製コースターキット(試作版・失敗版) - arcanum_jp - BOOTH
東北ずん子さん3姉妹、ヒノキコースターセット(成功版)
ずん子さん東北3姉妹ヒノキコースターセット(試作、成功版) - arcanum_jp - BOOTH
SpringBootで認証を学ぶ、PASETO
こちらの続きです。
arcanum.hatenablog.com
クライアントーサーバ間の認証をするためのトークンって、JWTってのが今の流行りなんだ!フフン、俺最先端!とばかりにググってると結構、JWTを使うべきか?なんてJWTの危険性についての記事が目につき、なんでだろ〜と調べていると今はPASETOなんてものもあるらしい。なんですとーー!(の割には2018年からでPASETOの記事が引っかからないのであんまり普及していないのかなとか思ったりしますが・・・)
PASETOは以下のサイトが公式みたい。検索しているとPASETOではなくPASTと表記する人もいるみたいなので検索するときは注意ですね。にしてもgithubは2018年からあんまり動いていませんね・・・気になります。一通り今の仕様での開発が終わったって事でしょうか?各言語の実装を見るにチマチマとイシューは発行されているようで、動いてはいるようですが。
paseto.io
PASETOはPlatform-Agnostic Security Tokensの略で、プラットフォームに依存しないセキュリティトークンらしいです。すみません、グーグル翻訳に突っ込んだだけですがいい具合に感じられるので書いてみました。PASETO自体はJWTの危険性を回避しつつ、ステートレスなトークンを実現するための仕様にとどまっており実装は各言語に任せられているようです。また、PASETOのトークン形式は以下になっています
version + "." + purpose + "." + Payload + "." + Footer(ある場合)
これらがPayload、Footer部分がBASE64エンコードされます。
例としてこんな感じ。これはPASETOのgithubから引っ張ってきたのですがPayloadしかない例ですね。
v2.local.QAxIpVe-ECVNI1z4xQbm_qQYomyT3h8FtV8bxkz8pBJWkT8f7HtlOpbroPDEZUKop_vaglyp76CzYy375cHmKCW8e1CCkV0Lflu4GTDyXMqQdpZMM1E6OaoQW27gaRSvWBrR3IgbFIa0AkuUFw.UGFyYWdvbiBJbml0aWF0aXZlIEVudGVycHJpc2Vz
versionは2019年5月現在、V1とV2があり、V1はそれまでの互換性のためにあるため、利用はV2が推奨されています。公式サイトの各言語の実装状況を見ると、V1が実装されていない言語がありますが、V2以降に実装されたのだと思います。
purposeはlocalとpublicの2種類あり、localは共有キーによる暗号化で、Payload自身が暗号化されます。publicは公開鍵、秘密鍵による暗号化でPayloadは暗号化されません。単にサーバーとクライアント間をトークンで認証したい(ただし内容にIDなどの情報を含む)で使いたい場合はlocal、クライアント、サーバーともトークンの内容を知る必要がある場合はpublicでしょうか。よくわかりません。JWTと同様に改ざんに強いようですね。改ざんされたトークンはトークンの検証時に弾かれる仕組みは同じです。
利用シーケンスですが、以下のページのWhat Should We Use PASETO For?の節によると、自サイトに閉じる、APIを公開するようなサービスでトークンとして使う分にはlocalで十分なようですね。
github.com
色々な言語向けに実装を作っている方がいて、Javaは2つあるみたいですが今回はMavenでの使い方があるので、atholbro/pasetoを使います。では先日作ったこのリポジトリからPASETOを使うよう変更していきたいと思います。githubはこちら。この内容を元にJava側の使い方とRFCの行ったり来たりを繰り返していました。
pom.xmlの変更部分
<!-- JWT関連をコメントアウト。 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency> --> <dependency> <groupId>net.aholbrook.paseto</groupId> <artifactId>meta</artifactId> <version>0.3.0</version> </dependency> <!-- Javaのバージョンなどにより以下も必要な場合があります。 --> <dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency>
あと、pom.xmlにこれも追加しておきます
<repositories> <repository> <id>lazysodium</id> <url>https://dl.bintray.com/terl/lazysodium-maven</url> </repository> </repositories>
JWTを使っている部分でエラーが出ます。以下のファイルがコンパ入りエラーになります。ID/PASS認証とトークン認証の部分なので狙い通りです。
JWTAuthenticationFilter
JWTAuthorizationFilter
クラス名もPASETOに合うようにちょいと変更しながら修正していきます。
JWTAuthenticationFilter | --> | PASETOAuthenticationFilter |
JWTAuthorizationFilter | --> | PASETOAuthorizationFilter |
v2.local , Footerを使わないケース
atholbro/pasetoにある説明の通り直していきます。
前回使ったソースを変更していくため、変更部分はコメントにしています。
JWTAuthenticationFilter.java
/* String token = Jwts.builder() .setSubject(user.getUsername()) .claim("role", user.getAuthorities()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET.getBytes()) .compact(); */ byte[] key = Hex.decode("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"); TokenService<Token> tokenService = PasetoBuilders.V2.localService(() -> key, Token.class).build(); Token pasetoToken = new Token(); pasetoToken.setIssuer("arcanum.jp"); pasetoToken.setSubject(user.getUsername()); pasetoToken.setTokenId("uniqu id at JWT"); pasetoToken.setExpiration(OffsetDateTime.now().plusHours(8)); // 8 hours String token = tokenService.encode(pasetoToken);
こちらも前回のソースに変更を加える形で行なっています。ロールはとりあえずROLE_USERを入れています。
JWTAuthorizationFilter.java
/* JwtParser parser = Jwts.parser().setSigningKey(SECRET.getBytes()); Claims claims = parser.parseClaimsJws(token).getBody(); String user = claims.getSubject(); List grants = (List) claims.get("role"); String[] arrayRole = new String[grants.size()]; for (int i = 0 ; i < grants.size(); i++) { LinkedHashMap grant = (LinkedHashMap) grants.get(i); String rolestr = (String) grant.get("authority"); arrayRole[i] = rolestr; } List<GrantedAuthority> roles = AuthorityUtils.createAuthorityList(arrayRole); */ byte[] key = Hex.decode("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"); TokenService<Token> tokenService = PasetoBuilders.V2.localService(() -> key, Token.class).build(); Token pToken = tokenService.decode(token); String user = pToken.getSubject(); List<GrantedAuthority> auth = AuthorityUtils.createAuthorityList("ROLE_USER"); // とりあえず
暗号化キーをバイト化してビルダーに突っ込むとトークンサービスが取得できるようです。取得できたトークンサービスに任意の値を入れたトークンを突っ込むと文字列化されたトークンが取得できます。非常にシンプルですね。トークン(この場合クレーム)にはPASETOのサイトで公開してるRFCによると以下の情報を格納できます。
iss | Issuer | 発行者 |
sub | Subject | ユーザーを識別する情報、ユーザーIDなど |
aud | Audience | クライアント側の情報*1 |
exp | Expiration | 有効期限 |
nbf | Not Before | 有効開始日時 |
iat | Issured At | 発行日 |
jti | Token ID | JWTの一意な識別子*2 |
kid | Key-ID | クレームのオプショナルな情報 *3 |
*1 この情報を元に受け取ったクライアントは自分向けのトークンかどうか検証を行う
*2 トークンで一意な値。例えばユーザーが複数回ログインした場合は1:多の関係になります。
*3 実際にはクレーム部ではなくフッターに入る(下記参照)
ビルダーはversion, purposeに合わせて4種類あるので目的にあったものでサービスを取得します。
PasetoBuilders.V1.localService( )
PasetoBuilders.V1.publicService( )
PasetoBuilders.V2.localService( )
PasetoBuilders.V2.publicService( )
curl -v -X POST -d "{ \"loginId\" : \"nyasba\", \"pass\" : \"password\"}" -H "accept: application/json" -H "Content-Type: application/json" "http://localhost:8080/user/login" | jq .
v2.local.M9kI2YfLSnbSF1ir3HvLxYTtHOvM9pjZf_XG3MaCJmYK6jOVwwZM0cLLKNaQA97dkm23vTrOQFtfvFi2jOYvXONxgBwLMUbi8mOsb2vItH6Zf18vd0go5SwqF7OEpuDu1Bi8xYJwJjNALtwXyChGeGRniht0x9PyL2i_1OXYIHgzyqF3mzqL5KDeA6YXuqvDPFWMdmJiJD1OW38XRnUQfuUvXT9P-xQ
このトークンのクレーム(local.より後)についてBASE64デコードしてみます。デコードにはこちらのサイトを使いました。
tool-taro.com
もちろんデコードしても見えません。暗号化されているのですから。JWTと異なりクレームの内容は暗号化されているので安心ですね。
このトークンを元に今度はアクセスしてみます
curl -X GET -H "Authorization: Bearer v2.local.M9kI2YfLSnbSF1ir3HvLxYTtHOvM9pjZf_XG3MaCJmYK6jOVwwZM0cLLKNaQA97dkm23vTrOQFtfvFi2jOYvXONxgBwLMUbi8mOsb2vItH6Zf18vd0go5SwqF7OEpuDu1Bi8xYJwJjNALtwXyChGeGRniht0x9PyL2i_1OXYIHgzyqF3mzqL5KDeA6YXuqvDPFWMdmJiJD1OW38XRnUQfuUvXT9P-xQ" "http://localhost:8080/private"
コンソールに以下が出たのでトークンの検証が無事行われたと言う事でしょう。先ほどの修正にあるようにROLE_USERは後から入れたのでnyasubaが表示された事でクレームにある情報からユーザーのIDが取得できたという事になります。
this is private for nyasba auth: [ROLE_USER]
TokenService#decode( )でクライアントから送られたトークンの検証を行います。ここで例外など起こらずにTokenオブジェクトが取得できた場合はトークン自体の検証は行われたと言う事になります。便利ですね。また、トークンが期限切れを起こしている場合はTokenService#decode( )の時点でExpiredTokenExceptionが送出されます。
v2.local , Footerを使うケース
フッターを使うケースです。フッターにはシステムで任意の値がJsonで入れられる模様ですね。ただし、フッターは暗号化されないため、出来上がったトークンのフッター部分をBASE64デコードすると丸見えになるので注意が必要です。ライブラリではKeyIdクラスが用意されていて、RFCにあるkidのみサポートしています。なのでフッターを使う場合はTokenService#encode( )を以下のように変更すれば良いです。
KeyId kid = new KeyId(); kid.setKeyId("my key id"); //String token = tokenService.encode(pasetoToken); String token = tokenService.encode(pasetoToken, kid);
これで以下のトークンが取得できます。
v2.local.iHsfvYhVJhDL8CKBB_X1kNa_4_iNLRMGgdjT5w85rTqj_ElGy-V5TAJHRg09baCQdtNSv1NA1IaeNnbGdLbIo7-ZV8sRdwAOiYFBPuEHLVAP7ty1wlwo8gxn0AAsGwm-klEr2-lNReHQWYm9nOZH-kzi5dIzvw_IIA_-tBjNz5_sCcIy_LCxIuEzjxx86qgNZZxvg9kHFVkjVsuf_RcWRqY-GAxZDMs.eyJraWQiOiJteSBrZXkgaWQifQ
トークンの最後に”eyJraWQiOiJteSBrZXkgaWQifQ”が追加されましたね。フッター部です。これをBASE64デコードすると以下になります。たしかにフッター部は暗号化されていませんん。また追加した情報がBASE64デコードする事で見る事ができました。
Java上ではフッターがある場合はtokenService.decode( )ではなく、tokenService.decodeWithFooter( )を使います。tokenService.decode( )と同様、このメソッドでクレームとフッターの検証が行われ、完了した場合のみTokenWithFooterオブジェクトが取得できるようです。
TokenWithFooter<Token, KeyId> tokenWithFooter = tokenService.decodeWithFooter(token, KeyId.class);
Token pToken = tokenWithFooter.getToken();
KeyId footer = tokenWithFooter.getFooter();
トークンの検証ができていない状態でもフッターの内容は取得できます。以下のようにするようです。ただし、ここでの注意点はtokenService.decode( )/ tokenService.decodeWithFooter( )をする前に tokenService.getFooter( )をしてもフッター情報は取得できますが、その情報はトークンの検証が済んでない状態という事に注意してください。
KeyId kid = tokenService.getFooter(token, KeyId.class);
フッター部は何に必要なのでしょうか?どうせなら全部暗号化されているクレームに含めた方がよいと感じるのですが。トークンの検証が行われない限り、トークンのクレームにあるユーザーに対する内容は取得できません、しかしUIの都合上、認証エラー時に例えばユーザー名を付加したメッセージを出したい場合などがあります。その場合にフッター部は暗号化されていないため、この情報を使えると言う事です。
ここで疑問が起きます。フッターは自由な情報を設定できるそうですがKeyID意外の情報はどのようにして格納するのでしょうか?格納したい情報のクラスを作成し、エンコードする際に指定します。この場合、クラスのインスタンス変数はpublicにするか、Bean仕様にします。
オリジナルなフッター情報(CustomFooter.java)
Stringはもとより、int, List
public class CustomFooter { private String[] auth; private String name; private int age; // getter/ setter }
こんな風に入れます
//KeyId kid = new KeyId(); //kid.setKeyId("my key id"); //String token = tokenService.encode(pasetoToken, kid); CustomFooter footer = new CustomFooter(); footer.setAuth(new String[] {"ROLE_USER", "ROLE_ADMIN"}); footer.setAge(17); footer.setName("zunko"); String token = tokenService.encode(pasetoToken, footer);
出来上がったトークンを見てみましょう
v2.local.7-s3cd5GLbXvEUmng4k7DCpG37K4A-c6w3Jrs-jyqJlAO6NDjFNNZHAAjqb9ZwHvkP8G63WdRun7rnj8-Vgzhh1AEk87cwHek3_up03QWgKJM6BlvQ46RpRnGA3Bue9w8MKfiv47eOYHItK8dSBu4gt9kP6sxESeIfSevjbIsd31Il8OjX6D0obVYib6APrD0tJlTyc3wwm_90MdNqXj6jQcFSSpmKI.eyJhdXRoIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwibmFtZSI6Inp1bmtvIiwiYWdlIjoxN30
このうちのフッターは以下のようにデコードできました
カスタムフッターの時も同様にカスタムクラスのClassを指定すれば良いです。
TokenWithFooter<CustomToken, CustomFooter> tokenWithFooter = tokenService.decodeWithFooter(token, CustomFooter.class);
Token pToken = tokenWithFooter.getToken();
CustomFooter footer = tokenWithFooter.getFooter();
このトークンを元に/privateにアクセスするとコンソール上に以下が表示されましたのでカスタムなフッターが利用できた模様です。
this is private for nyasba auth: [ROLE_USER, ROLE_ADMIN]
v2.public , Footerを使わないケース
さて次にv2.publicの使い方です。TokenService
SecurityConstants.java に以下を追加
// get from RFCTestVectors public static byte[] RFC_TEST_SK = Hex.decode("b4cbfb43df4ce210727d953e4a713307fa19bb7d9f85041438d9e11b942a37741eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2"); // 非公開鍵 public static byte[] RFC_TEST_PK = Hex.decode("1eb9dbbbbc047c03fd70604e0071f0987e16b28b757225c11f00415d0e20b1a2"); // 公開鍵 public static KeyProvider PROVIDER = new KeyProvider() { @Override public byte[] getSecretKey() { return RFC_TEST_SK; } @Override public byte[] getPublicKey() { return RFC_TEST_PK; } };
TokenService
//byte[] key = Hex.decode("707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f"); //TokenService<Token> tokenService = PasetoBuilders.V2.localService(() -> key, Token.class) // .withDefaultValidityPeriod(Duration.ofDays(15)) // .build(); Builder<Token> builder = PasetoBuilders.V2.publicService(SecurityConstants.PROVIDER, Token.class); TokenService<Token> tokenService = builder.build(); ||< これでトークンを生成してみます。以下のようになります。確かにv2.publicから始まってます。 >|| v2.public.eyJpc3MiOiJhcmNhbnVtLmpwIiwic3ViIjoibnlhc2JhIiwiZXhwIjoiMjAxOS0wNS0zMVQwNDo0OToxMCswOTowMCIsImp0aSI6InVuaXF1IGlkIGF0IEpXVCJ9_8r7ttGRjJfFm7sy4Fje_vq3kkPJTDCKIVOLWvcsqJNdOA5bjqX9lzoHuugfu2BiW9JmZjmppsE0WghORKMgAw.eyJhdXRoIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwibmFtZSI6Inp1bmtvIiwiYWdlIjoxN30
これをクレームとフッターに分けてBASE64デコードしてみます
クレーム
フッター
たしかにクレーム、フッタとも暗号化されておらず、BASE64デコードでJSONで内容がみれます。クレームの最後についたのは認証情報かな?
クレームにカスタムな情報を入れる
さて、RFCではクレームには先で書いたaudなどの予約語はあるが任意の情報を入れる事ができるとあります。でもどうやって入れるのでしょうか?フッターと同様、カスタムのトークンクラスを作成します。カスタムトークンは先ほどのカスタムフッターと同様Bean仕様にするようです。また予約語の件があるためTokenクラスを継承するのが良いようですね。その際、Tokenクラスがequals( ), hashCode( ), toString( )をオーバーライドしているため、同じようにオーバーライドする必要があります。(以下では略してますが動きます)
public class CustomToken extends Token { private String message; public String getMessage() { return message; } public Token setMessage(String msg) { message = msg; return this; } }
エンコード時にはTokenを指定した場所をCustomTokenを指定するようにします。サイトではToken.classを任意の(この場合CustomToken)Classインスタンスを指定すれば良いと書いてますが、実際にはジェネリクスでTokenを指定した場所全ても書き換える必要があります(まぁあたりまえですが)
PASETOAuthenticationFilter.java
//Builder<Token> builder = PasetoBuilders.V2.publicService(SecurityConstants.PROVIDER, Token.class); //TokenService<Token> tokenService = builder.build(); Builder<CustomToken> builder = PasetoBuilders.V2.publicService(SecurityConstants.PROVIDER, CustomToken.class); TokenService<CustomToken> tokenService = builder.build(); //Token pasetoToken = new Token(); CustomToken pasetoToken = new CustomToken(); pasetoToken.setMessage("hello paseto world!!");
PASETOAuthorizationFilter.java
//Builder<Token> builder = PasetoBuilders.V2.publicService(SecurityConstants.PROVIDER, Token.class); //TokenService<Token> tokenService = builder.build(); //Token pToken = tokenService.decode(token); Builder<CustomToken> builder = PasetoBuilders.V2.publicService(SecurityConstants.PROVIDER, CustomToken.class); TokenService<CustomToken> tokenService = builder.build(); //Token pToken = tokenService.decode(token); CustomToken pToken = tokenService.decode(token); String message = pToken.getMessage(); String user = pToken.getSubject();
実際に以下のようなトークンになります。
v2.public.eyJpc3MiOiJhcmNhbnVtLmpwIiwic3ViIjoibnlhc2JhIiwiZXhwIjoiMjAxOS0wNS0zMVQwNTo0NDowNSswOTowMCIsImp0aSI6InVuaXF1IGlkIGF0IEpXVCIsIm1lc3NhZ2UiOiJoZWxsbyBwYXNldG8gd29ybGQhISJ9-uIsrADYrmnMwbNc2pwRu_racCki2ySmIytkWg04Tg4xhQrI_NjMr84YF4zZvPXlIBBMmiNKsIYVgl6n_wymBw.eyJhdXRoIjpbIlJPTEVfVVNFUiIsIlJPTEVfQURNSU4iXSwibmFtZSI6Inp1bmtvIiwiYWdlIjoxN30
BASE64デコードすると以下になります。任意の情報(この場合message)が入っているのがわかります。
今回作成したものは以下のリポジトリにあります。
(色々といじっているままなのでこのままでは動きません)
github.com
PASETOは使えるのか?
JWTを検索すればするほど、JWTは危険云々っていうブログやら記事やらが出てきて、目についたPASETOにカッとなって飛びついてみましたが、正直PASETOで良いのでしょうか?という疑問は残ります。使い方に関して、正直PASETOの公式サイト、Javaであれば今回しらべたライブラリぐらいしか情報源としてはありません。stackoverflowにすら使い方の情報が殆ど出てこなかった。情報量が少なすぎて逆に使うのを躊躇してしまいますね。
東北3姉妹、ずん子さん、きりたん、イタコ姉のコースターセットを作る、その1
こちらの続きです。
前回は市販品のコルクコースターにレーザー彫刻を施したのですが、材料自身は良いものなのですが、単価がかかるため、大きいコルク板からコースターの形を切り出した方が経済的じゃね?俺天才!と考えていました。
しかし、実際コースターの形を切り出そうとすると、どう試行錯誤しても切れない。どうも、より硬いMDFの5mmなんかの材料は切れるのにはるかに柔らかいコルク4mmが切れない・・・なんですと!!
BOOTHなんかを見てると木材と思われるものやコルクに印刷を施したコースターを売っている方がいたりして、では木に印刷したコースターを作ってはと思いやってみました。
部材としてはFablabs仙台さんで教えていただいた、東京檜というところから仕入れました。
うは!結構高いです・・・これに送料ですね・・・こんな感じで発送されてきました。娘、なぜか大喜びで箱開けます。カミさん、また何かやらかすつもりだわ・・・と疑惑の目、、、
中身はこんな感じで梱包されてきます。開けると檜の香りがするのですね。娘またもや謎の大喜びです。ホント、小学5年にもなってこう言うの見ると喜ぶって、、、お前は、、子供か!(子供だよ)
寸法はわかっていましたが、手に持って見ると意外と小さいですね。
この寸法から取り出せるコースターとコースター立てを設計してみます。結構ギリギリですね。絵柄はずん子さん公式から浴衣で統一してみました。浴衣に和柄を配置しています。下の画像はクリッピング処理をしていないのでまだ、コースター外側に色があったりします。
下の方のカットは、コースタースタンドです。ただ、この1枚だけ作るとものすごい高上がりなコースターが出来上がるため、何枚か同じものを作って見ましょうと。先ほどの絵を見るに切る時間は大体1枚あたり10分〜15分かなと思いましたので2枚ほど作って見ます。余った時間は何か彫刻でもしましょう。
さっそく切ります。施設のイラレからHAJIMEのソフトであるHARUKAに転送します。
こんな風に切れました。これは1枚目でよく見ると縦長な部品が余計に切ってしまっています。ちょい失敗。そこを現場で直して2枚目を切りました。
ちなみにこんな風にコースターを立てられます。
2枚切って30分もかからなかったのでおまけとして先日も作ったコルクコースターを彫刻します。
これに日を改めてUVプリンターで印刷していきます。1毎あたり7分として2毎で14分、部材のセット時間を含め、30分はかかりませんね、、、どうしよう、、、
次回に続く
SpringBootでWebAPI認証を学ぶのメモ、JWT
SpringBootでAPIを作成する際、認証などの仕組みが必要になります。そのため何がいいのか検索していたところ、JWTというのがある、とわかり、キータでサンプルを公開していた方がいたので、それをベースに勉強しました。
こちらを参考にしています。
qiita.com
サンプルソースはGithubにあるようなのでこちらをクローンして確認してみます。
github.com
JWTは色々なサイトで書いている方がいますが、簡単にいうとトークンに署名がしてあり改ざんに強いとのことです。
webbibouroku.com
JWTはpom.xmlに以下の追加を行いますが、Javaの実行バージョンによりエラーになります。
<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
※アクセス時、次のエラーが出る
java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583) ~[na:na] at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178) ~[na:na] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521) ~[na:na] at io.jsonwebtoken.impl.Base64Codec.encode(Base64Codec.java:21) ~[jjwt-0.9.0.jar:0.9.0] ・・・省略
その場合は次をpom.xmlに追加します。Java9以上はこの追加でOKなようです。
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> </dependency>
こちらに書いてありました。
github.com
実行すると確かに/user/login で認証が行われ、レスポンスヘッダにJWTのトークンが入ってきます。そのトークンを元に/privateにアクセスすると、トークンの情報からユーザーが特定され、ログに出力されます。ここまではあっというまにできました。
認証処理はどこでやるか
ここから作られたコードがどの順番で実行されるのだろうと疑問が浮かびます。エントリでは認証に関してはJWTAuthenticationFilterが責任を持つとありますが他はどのような順番なのでしょうか?ログはって追ってみたところ、以下の順番のようです。
①JWTAuthenticationFilter#attemptAuthentication( )が呼び出される
②UserDetailsServiceImpl#loadUserByUsername( )が呼び出される
③JWTAuthenticationFilter#successfulAuthentication( )が呼び出される
サンプルではあまり言及されていませんが、この流れの①が認証本体のようです。リクエストからID/PASSを取得していますので、本来はここでID/PASSの検証をするようです。検証がOKならAuthenticationオブジェクトを返します。この部分ですね。
return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( userForm.getLoginId(), userForm.getPass(), new ArrayList<>())
ただ、サンプルではここでUsernamePasswordAuthenticationTokenのコンストラクタ第3引数に配列0のオブジェクトを入れていますが、第3引数はここでは指定しなくとも良いようです。
はじめ、各ユーザーが持つロールの配列(List<GrantedAuthority>)を入れるタイミングがサンプルでは2箇所あり、どこで入れるのが正しいのかなと思っていましたが、UserDetailsServiceImpl#loadUserByUsername( )で返すUserDetailsオブジェクトにセットして返すのが正解のようです。
①の処理が行われ、UsernamePasswordAuthenticationTokenオブジェクトをauthenticationManager.authenticate( )に渡した中で、②の処理が行われ、返却されたUserDetailsにセットされたList<GrantedAuthority>がAuthenticationにセットされ①の処理は完了(Authenticationを返す)、その後、③の処理の引数として引き渡されるようです。これは、①の処理でList<GrantedAuthority>をセット、②では空を返す、にすると③には引き渡されず、というので確認しています。また②の処理でList<GrantedAuthority>がnullで返すとエラーになります。
また、この流れの中でユーザが検索できないなどのエラーの場合は素直にUsernameNotFoundExceptionオブジェクトをスローすればレスポンスは403になります。ここは便利ですね。
認証処理のレスポンスで任意の情報を含める
サンプルではトークンにユーザの情報としてuseridを入れていますが、他に任意の情報も含められます。実験としてロールも含めてみます。この方法が良いかは別として任意の情報としてロールをセットする方法を書いてみます。ロールは先ほどの流れの中でAuthenticationオブジェクトにList
String token = Jwts.builder() .setSubject(((User)auth.getPrincipal()).getUsername()) // usernameだけを設定する .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET.getBytes()) .claim("role", auth.getAuthorities()) // <---------------------ここに1行足した .compact();
サンプルでも言及されていますがauth.getPrincipal( )は先ほどの②の処理で返したUserDetailsそのものなので、こちらにも同様の情報があるため以下のようにも書き直すことができます。
User user = (User)auth.getPrincipal(); String token = Jwts.builder() .setSubject(user.getUsername()) // usernameだけを設定する .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) .signWith(SignatureAlgorithm.HS512, SECRET.getBytes()) .claim("role", user.getAuthorities()) .compact();
この追加のあと/user/loginにアクセスしてできたトークンは以下になります。
eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJueWFzYmEiLCJleHAiOjE1NTg2ODM1NjUsInJvbGUiOlt7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifSx7ImF1dGhvcml0eSI6IlJPTEVfVVNFUiJ9XX0.2NYpAF6BH1Pkskj2MYTKCsPABRozFlm-IDxaBSnVI6naJXHv53OgDbkvpwljtTQzK7fbzqqeS_fVJ5EtsF9jJQ
これを以下のサイトでデコードしてみます。
jwt.io
デコードしてみるとロールが追加されていますね。通常であればこんな大事な情報はすぐデコードできるような部分には追加しないと思いますが、例ということで・・・
トークンから任意のクレームを取り出す
今度はヘッダにトークンを追加されてきたときの認証部分です。ソースのJWTAuthorizationFilterというフィルタ処理がそうで、このクラスのgetAuthentication( )でクレームからの情報取得をしています。
この処理を以下のように書き換えました。
token = token.replace(HEADER_STRING, ""); token = token.replace(TOKEN_PREFIX, "").trim(); JwtParser parser = Jwts.parser().setSigningKey(SECRET.getBytes()); Claims claims = parser.parseClaimsJws(token).getBody(); String user = claims.getSubject(); List grants = (List) claims.get("role"); String[] arrayRole = new String[grants.size()]; for (int i = 0 ; i < grants.size(); i++) { LinkedHashMap grant = (LinkedHashMap) grants.get(i); String rolestr = (String) grant.get("authority"); arrayRole[i] = rolestr; } List<GrantedAuthority> roles = AuthorityUtils.createAuthorityList(arrayRole); if (user != null) { return new UsernamePasswordAuthenticationToken(user, null, roles); }
あと、SampleController#privateApi( )も以下のようにトークンに入れた情報がわかるように修正します。
@GetMapping(value = "/private") public String privateApi() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // JWTAuthenticationFilter#successfulAuthenticationで設定したusernameを取り出す String username = (String) (authentication.getPrincipal()); return "this is private for " + username + " auth: " + authentication.getAuthorities(); // <------修正 }
これをサンプルの通りにトークンをヘッダに入れて実行してみます。
curl -X GET -H "Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJueWFzYmEiLCJleHAiOjE1NTg2ODM1NjUsInJvbGUiOlt7ImF1dGhvcml0eSI6IlJPTEVfQURNSU4ifSx7ImF1dGhvcml0eSI6IlJPTEVfVVNFUiJ9XX0.2NYpAF6BH1Pkskj2MYTKCsPABRozFlm-IDxaBSnVI6naJXHv53OgDbkvpwljtTQzK7fbzqqeS_fVJ5EtsF9jJQ" "http://localhost:8080/private"
こんな風に取得できました。トークンに任意の情報(この場合ロールの名前ですが)を入れてまたデコードすることが確認できました。
this is private for nyasba auth: [ROLE_ADMIN, ROLE_USER]
ロールによるURLの制限
さて、ここでトークンを介したロールの保存と複合ができたので、ユーザーにおけるロールによるURLの制限確認をしておわります。以下の修正をして確認します。/user/myurlは”ROLE_USER”、”ROLE_ADMIN”がアクセス可能、/adminは”ROLE_ADMIN”がアクセス可能という前提です。
SampleController.java に以下のパスを追加します。
@GetMapping(value = "/user/myurl") public void myurl(@Valid @RequestBody UserForm user) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = (String) (authentication.getPrincipal()); return "this is /user/myurl for " + username + " auth: " + authentication.getAuthorities(); } @GetMapping(value = "/admin") public void admin(@Valid @RequestBody UserForm user) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); String username = (String) (authentication.getPrincipal()); return "this is /admin for " + username + " auth: " + authentication.getAuthorities(); }
WebSecurityConfig.java に以下の修正をします。
@Override protected void configure(HttpSecurity http) throws Exception { http .cors() .and().authorizeRequests() .antMatchers("/public", SIGNUP_URL, LOGIN_URL).permitAll() .antMatchers("/user/myurl").hasAnyRole("USER", "ADMIN") // <---- 追加 .antMatchers("/admin").hasAnyRole("ADMIN") // <---- 追加 .anyRequest().authenticated() .and().logout() .and().csrf().disable() .addFilter(new JWTAuthenticationFilter(authenticationManager(), bCryptPasswordEncoder())) .addFilter(new JWTAuthorizationFilter(authenticationManager())) .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }
さて、実行すると、トークンに入ったロールによってアクセス制限ができることが確認できました。
今回サンプルとした元ソースをForkして修正したものがこちらです。
※元リポジトリはGradleでしたがこちらではMavenに変更しています。またその際に発生したエラーの影響でテストは削除しています。
github.com