人は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
SpringBootでID/PASSによる認証を学ぶ
簡単なサンプルを自分で作って見て、SpringBootにおける認証処理、ユーザーごとの権限処理を学びます。以下はWebで結構な方が書いているので色々見たのを自分なりにメモしているものです。ソース自体はどこかの記事で見かけた物を流用したりしています。
まず、基本となる画面を作る
例としては、こんなURLでアクセスした場合の画面を作っていきます。
/user | ユーザーがログインした時にしかアクセスできない領域 |
/admin | 管理者がログインした時にしかアクセスできない領域 |
/etc | ログインしなくとも誰でも見れる領域 |
/mylogin | ログイン用の画面 |
はじめに、とりあえず上記のURLでアクセスできるものを作ります、
pom.xmlは以下が必要になります。
spring-boot-starter-thymeleaf spring-boot-starter-web
画面はこんな感じ
/user.html
<html> <body> <h1>USER!! page</h1> </body> </html>
/admin.html
<html> <body> <h1>ADMIN!! page</h1> </body> </html>
/etc.html
<html> <body> <h1>etc...!! page</h1> </body> </html>
/mylogin.html
<html> <body> <form id="login_form" method="post" action="@{'/login'}"> <label>login id</label> <input type="text" id="login_id" name="login_id" /> <br> <label>password</label> <input type="password" id="login_password" name="login_password" /> <br> <input id="login_button" type="submit" value="send" /> </form> </body> </html>
とりあえず画面へのアクセスは1つのクラスにまとめておきました。(この設計が良いか悪いかは別とします。)
PageController.java
@Controller public class PageController { @RequestMapping(value="/user") public ModelAndView pageUser(ModelAndView mv) { mv.setViewName("user"); return mv; } @RequestMapping(value="/admin") public ModelAndView pageAdmin(ModelAndView mv) { mv.setViewName("admin"); return mv; } @RequestMapping(value="/etc") public ModelAndView pageEtc(ModelAndView mv) { mv.setViewName("etc"); return mv; } @RequestMapping(value="/mylogin") public ModelAndView pageLogin(ModelAndView mv) { mv.setViewName("mylogin"); return mv; } }
これでサクッと全ページアクセスできました。
認証処理を作る
次に、/admin, /userはログインしていないとアクセスができないようにします。認証処理をSpringBootで使うにはpom.xmlに次を追加すればいいようです。
spring-boot-starter-security
とりあえず追加してみて再起動してみます。あれ?御呼びでないページが出てきました。パスが/loginでこちらの期待するパスでもありません。SpringBootが出すデフォルトのログイン画面みたいですね。しかもどのページを開いてもこの画面になってしまいます。権限の設定などをしないとダメ見たいですね。
/user, /admin はログイン後に表示できるページという風にしたいので、未ログインの状態ではログイン画面に飛ぶようにしてみます。その場合、WebSecurityConfigurerAdapterのサブクラスを作って認証処理の設定を作り込むようです。認証処理はこのクラスがキモのようで、このクラスに色々と追加していくと認証が完成していくみたいです。その際、ログイン者の権限として"USER", "ADMIN"を作ります
MySecurityConfig.java
@Configuration public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 認可の設定 http.authorizeRequests() .antMatchers("/user").hasAnyRole("USER", "ADMIN") .antMatchers("/admin").hasAnyRole("ADMIN"); // 認証処理を行うインスタンスを設定 http.authenticationProvider(new MyAuthenticationProvider()); // ログイン設定 http.formLogin() .loginProcessingUrl("/login") // 認証処理のパス .loginPage("/mylogin") // ログインフォームのパス .failureHandler(new MyAuthenticationFailureHandler()) // 認証失敗時に呼ばれるハンドラクラス .defaultSuccessUrl("/user") // 認証成功時の遷移先 .usernameParameter("login_id").passwordParameter("login_password"); // ユーザー名、パスワードのパラメータ名 } } ||< <b>MyAuthenticationProvider.java</b> >|| public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure( HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { String errorId = "ERR_001"; if(exception instanceof BadCredentialsException){ errorId = "LOGIN_001"; } // ログイン画面にリダイレクトする response.sendRedirect("/mylogin?error=" + errorId); } }
MyAuthenticationProvider.java
public class MyAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { private static final List<GrantedAuthority> AUTH_USER = AuthorityUtils.createAuthorityList("ROLE_USER"); private static final List<GrantedAuthority> AUTH_ADMIN = AuthorityUtils.createAuthorityList("ROLE_USER", "ROLE_ADMIN"); @Override protected void additionalAuthenticationChecks( UserDetails userDetails, UsernamePasswordAuthenticationToken authentication ) throws AuthenticationException { // nothing to do } @Override protected UserDetails retrieveUser( String username, UsernamePasswordAuthenticationToken authentication ) throws AuthenticationException { String password = (String) authentication.getCredentials(); // ユーザIDとパスワードをチェック // データベースのユーザー情報などに"ROLE_AUTH", "ROLE_ADMIN"などを保存しておき取得 boolean isValid = true; //AuthApi.isValidUserIdAndPassword(username, password); if (!isValid) { throw new UsernameNotFoundException(username); } // とりあえずadminだったら管理者 List<GrantedAuthority> auth = AUTH_USER; if (username.equals("admin")) { auth = AUTH_ADMIN; } // UserDetailsの実装(User)を生成し戻り値とする return new User(username, "dummy_pass", auth); } }
注意点として、作ったロールですが、 MySecurityConfigでは"USER", "ADMIN"、MyAuthenticationProviderでは"ROLE_USER", "ROLE_ADMIN"と名前が異なっています。どうやらWebSecurityConfigurerAdapterでパスにロールを設定する際、hasRole( ), hasAnyRole( )を使った場合、勝手に"ROLE_"というプレフィクスをつけるようですね。紛らわしい
キーホルダー作成・ずん子さん、小松姫、その他
前回のキーホルダー作成時、Fablab仙台の月間会員となっていたため(5000円を払った日から1か月間、HAJIMEが無料となる)10日ほど残った日数がもったいなくじゃぁキーホルダーをもう一回作るか、という事になりました。
同じように作ってもしょうがないので、今回の目標は
①表側は2度塗り
②裏側にも絵を塗る
というものを立てました。計画ではこんな感じに印刷します。1回7分ぐらいなので全部1度塗りであれば7+7+10+7で31分前後、表と白を2度塗りでやっても+14分で45分程度のはず、データ、部材のセットも含めれば1時間で①は十分間に合うはず。
一番上の絵の部分が今回の目標②の部分です。一番下のガイドはアクリルに印刷するのではなく、UVプリンタ印刷台に敷いた紙に印刷します。
月額会員分があと10日ほど残っていると言ってもその日付でキーホルダのデザインを全て作る事は難しく初めにキーホルダーの大きさだけ切っておいてあとからゆっくりデザインを決めてUVプリンターで印刷という手はずにしました。
切りのデータから切っておきます。前回のサイズがキーホルダーとしてはちょっと大きいかな?と思っていたので今回はその反省を踏まえ、一回り小さくしています。時間はこれでHAJIMEで20分ぐらい。以外とかかりますね。時間貸しなので、あと余った時間はずん子さんのコルクコースターでも作ります。だいたい15分ぐらいでしょうか。色々部材のセットとかやってこれで大体1時間です。
切ってる最中です。
さて困ったのは塗りのデータ作成です。最近和柄がいいなと思い、頑張ってイラレで和柄のスゥオッチを作成して使ってみます。東北三姉妹と小松姫、あと鳥獣戯画、ミュシャから百合を入れてみました。お!令和と言う文字が・・・ちょうど発表の時期にデザインを考え中でしたので入れてみました。(わざとらしい)
さてこれを印刷します。はじめにUVプリンタの印刷台に敷いてある紙にガイド線=切りの線を印刷します。この線に沿ってアクリルを載せるのですね。
↓はガイド線に沿ってアクリルを置いたものです。
この状態になって初めて絵を印刷します。毎回緊張します。ここで失敗すれば今までの苦労がパーですからね。データ作ってアクリル切って、絵をデザインして、この工程だけで何日もかかっています。
今回は二度塗りの設定をしました。大体1回塗ると7分で2度塗りをするとその倍かかると思っていたのですが、施設の方いわく、印刷ヘッドが2つあるらしく、2度塗りをしながら印刷するとのことでそんなには時間はかからないよと言う事でした。ホっ・・・
これに白で上塗りしていきます。せっかく塗れた絵が消えていきます。キャー!!ちなみに、白も2度塗りをしていきます。
予定ではこの後にもう一度絵柄を塗る予定でしたがこれで時間切れとなってしまいました。ちょっと時間が合わない・・・2度塗りは結構時間がかかるのかな?悔しいですね。
出来上がりです。これにナスカンを付けて完成です。
さて、2度塗りの実力ですが、、、結構濃ゆいですね、、大江戸ちゃんこ、結構濃くでてます。
単純な比較はできませんが前回1度塗りでやったキーホルダーと比べてみます。左が今回作った2度塗り、右が前回作った時の1度塗りです。明らかに色が濃いですね。特にスカート部分。
上が2度塗り、下が1度塗りです。濃すぎますね・・
上が2度塗りです。こちらも濃いですね・・
2度塗りの弊害として、裏側のホワイトを塗った部分がちょっと盛り上がってます。キーホルダーとしては1度塗りで良いような気がします。
さて、個別に見ていきましょう。
きりたんです。ナスカンにはキーホルダーとチャームをつけています。星型のそれです。丸いのは実はチャームではないのですがチャームっぽかったのでつけてみました。個人的には左のキーホルダがデザイン、色合いともお気に入りです。
短冊形のキーホルダです。前回の作ったのは形としてすごく気に入ったのですが、やや大きかったので一回り程ちいさくしてみました。結果、大きさとしてはバッチリです。どれも甲乙つけがたいですが、鹿の子模様にねじり梅のバックのきりたんがいいですね。
イタコ姉と大江戸ちゃんこ、です。ちゃんこかわいい!イタコはひょうきんな感じにするととてもいいですね。いじりがいがありそうです。
さて。丸形です。令和のデザインを作ってみました。あと左下は小松姫というキャラクターで、信之・小松姫プロジェクト と言う所で出しています(エントリ下のリンク参照)。以前から可愛いと思っていたのと、Twitterでの絵がなんとも面白くウォッチしていました。あとはずん子さんですね。このずん子さん、ほんと苦労しました。デザインが二転三転してどうすればいいんだろうと悩んだものです。まだまだですが・・・
どんなデザインにするかもうネタ切れの頃、入れたい絵もなくこんなのも入れてみました。鳥獣戯画については高山寺が権利登録を行っているみたいですが個人利用の範疇で作ってます。ミュシャについては既にパブリックドメイン入りしているのですね、、知らなかったですが、これも個人の利用の範囲で作っています。
祭りの後。残ったのはこのフレームだけです。これはボキボキと折ってゴミ箱行きです。ありがとう、君の事は忘れないよ。
いくつかは順次、BOOTHにて販売していきます。
ご興味のある方はどうぞ。出ていないもので購入されたい方はTwitterなどのDMで連絡いただければ、XX様専用としてBOOTHに作ります。価格は1000円+送料350円(BOOTH安心パック、ネコポス)です。