画面に表示するIDに、データベースで自動採番されたものをそのまま使用する場合、利用者にいろいろと推察されてしまいます。(利用者数とかばれる)
そのような特殊なフィールドは、専用のバリューオブジェクトを用意しIDの変換を処理を内部に書くのが一般的かと思います。
しかし、複雑な事情でリクエストやレスポンスモデルの型が変えられないときに、アノテーションでどうにかできないかということで調べてみました。
作業環境はJava9、SpringBoot(2.0.0.RELEASE)です。(特別なことはしてないので、他のバージョンでも大丈夫だと思います)
要件
今回の要件は以下の通り。
- コントローラのリクエストモデル、レスポンスモデルの型は変えられない
- ただしアノテーションは追加できる
- クライアントとサーバどちらでも難読化を気にせずに扱えるようにしたい
- 難読化は
LongからLongへの変換
仕様
上の要件を受けて、以下のような仕様にしたいと思います。
- アノテーションは
Obfuscateとする - 対象とするのはフィールドのみ
- 今回は難読化で1を足す、簡素化で1を引く(本来はもっと複雑)
実装
シリアライザ
難読化を施すクラスです。(サーバから出て行くところ)
StdSerializerを継承して実装します。
nullの場合はそのまま終了します。(NotNull等は他のアノテーションで表現されるべきなので)
public class ObfuscateSerializer extends StdSerializer<Long> {
private static final long serialVersionUID = -6910413385029615313L;
protected ObfuscateSerializer() {
super(Long.class);
}
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider provider) throws IOException {
// 難読化ロジック
if (value == null) {
return;
}
gen.writeNumber(value + 1);
}
}
デシリアライザ
簡素化を施すクラスです。(サーバに入ってくるところ)
シリアライザ同様、StdDeserializerを使います。
public class ObfuscateDeserializer extends StdDeserializer<Long> {
private static final long serialVersionUID = 4303388430635458159L;
protected ObfuscateDeserializer() {
super(Long.class);
}
@Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// 簡素化ロジック
return Long.parseLong(p.getValueAsString()) - 1;
}
}
※ 入ってくる値が数値でなかった場合はパースエラーが発生しますが、デフォルトではSpringBootが以下のように400を返してくれます。独自のレスポンスにしたい場合はハンドラーを設定してください。
{
"timestamp": 1521424385168,
"status": 400,
"error": "Bad Request",
"message": "JSON parse error: For input string: \"hoge\"; nested exception is com.fasterxml.jackson.databind.JsonMappingException: For input string: \"hoge\" (through reference chain: com.example.Hoge[\"value\"])",
"path": "/api/sample/obfuscate"
}
アノテーション
マーカーアノテーションなので、特にプロパティ等は持ちません。
ターゲットは仕様からElementType.FIELDのみ設定します。
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD })
public @interface Obfuscate {
}
アノテーションインターセプタ
定義したアノテーションと、シリアライザ・デシリアライザを紐付けるクラスです。
JacksonAnnotationIntrospectorを継承して実装します。
このクラスはSpringBoot内のJacksonがjsonを指定されたクラスに変換する際に、対象のクラスやフィールド、メソッドにアノテーションがついている場合にその項目毎に呼ばれます。
今回はシリアライザとデシリアライザを独自のものにするので、findSerializerとfindDeserializerをオーバーライドします。
public class MyAnnotationInterceptor extends JacksonAnnotationIntrospector {
private static final long serialVersionUID = -7722925738908936096L;
@Override
public Object findSerializer(Annotated a) {
if (a.hasAnnotation(Obfuscate.class)) {
return ObfuscateSerializer.class;
}
return super.findSerializer(a);
}
@Override
public Object findDeserializer(Annotated a) {
if (a.hasAnnotation(Obfuscate.class)) {
return ObfuscateDeserializer.class;
}
return super.findDeserializer(a);
}
}
コンフィグ
上記のアノテーションインターセプタをSpringBootに登録します。
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.annotationIntrospector(new MyAnnotationInterceptor());
return builder;
}
}
使用例
準備は整ったので、あとはリクエストやレスポンスモデルのフィールドに@Obfuscateをつければ自動的に変換されます。
@lombok.Value
public class ObfuscateRequest {
@Obfuscate
Long id;
}
まとめ
今回はアノテーションを定義して、フィールドの難読化・簡素化をしてみました。
すでにリリース済みでなかなかクラスの変更が難しい場合や、モデルを使い回していて変更ができない場合など役立つ場面があるかと思います。
また、バリューオブジェクトの場合、jsonにすると階層が深くなったりと何かと不便なところもあったりする(回避もできますが)ので、選択肢としては割とありかなと思いました。
参考
https://qiita.com/nijuya_o/items/0f512792c4db27913c7a
https://qiita.com/k_ui/items/b6e1763d023da96ea46f