基本的にはドキュメント通りにするんだけど、一部はまったとこのメモ。
そもそもの話
公式ドキュメントはどこ?
公式ドキュメントの16章に「こっちをみてね」くらいしか書いてないので非常にわかりにくい。
https://micronaut-projects.github.io/micronaut-security/latest/guide/
あと、こちらのガイドも参考になる。
https://guides.micronaut.io/latest/micronaut-security-basicauth-maven-java.html
どういう動きになればいいの?
ざっくりとした流れはMicronaut SecurityにあるBASIC認証のシーケンス図を参照。要は401レスポンスを返せばいいらしいが、それでだけではダメでWWW-Authenticateヘッダを一緒に返す必要があるらしい。
導入
1. まずはMicronaut Security の導入
pom.xml に "micronaut-security" を指定すればよい。
ただし、導入時点で「(static-resourceを含む)すべてのリクエストを拒否する」動きになるのでちょっとびっくりする。ブラウザでHTTPレスポンスを確認してればわかるのだけど、初見だとちょっとつらい。
2. 導入方針を決める
制御方法がいくつかあり、どうやら優先順はあるものの「誰かが許可したら"許可"、誰かが拒否したら"拒否"」という動きになっている模様。
どういう制限をしたいか、によってベストな方法が変わってくると思われる。
「基本的にホワイトリスト/ブラックリスト運用する」以外は、混ぜないほうがよさそう。
- Controllerをだけ使う
- Securedアノテーションを用いる方法がいいのかも。ただし、static-resourceは前述の「明示的に許可されていないので、暫定拒否」扱いになるので、その場合後者。
https://micronaut-projects.github.io/micronaut-security/latest/guide/#secured - static-resourceも制御する
- Intercept URL Mapで実現するのがよさそう。
https://micronaut-projects.github.io/micronaut-security/latest/guide/#interceptUrlMap - いろいろ条件を組み合わせたい
- 独自のSecurityRuleを実装する(後述)
3. SecurityRuleベースでを実装する
自前で条件を決めるのならばほぼ一択かと。
次の3つのファイルがあればよい。
3a. ユーザー認証
公式に基本コードあり。AuthenticationProvider を継承したクラスを用意すればよさそう。今回は単純に Properties.load() で読み込んだユーザー情報で判定する形に改造した。
参考: https://guides.micronaut.io/latest/micronaut-security-basicauth-maven-java.html#authentication-provider
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.AuthenticationProvider;
import io.micronaut.security.authentication.AuthenticationRequest;
import io.micronaut.security.authentication.AuthenticationResponse;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import java.io.IOException;
import java.util.Properties;
@Singleton
public class AuthenticationProviderUserPassword implements AuthenticationProvider {
private Properties users;
public AuthenticationProviderUserPassword() {
users = new Properties();
try {
users.load(AuthenticationProviderUserPassword.class.getResourceAsStream("/users.properties"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest,
AuthenticationRequest<?, ?> authenticationRequest) {
return Flux.create(emitter -> {
String password = (String)users.get(authenticationRequest.getIdentity());
if( password != null && authenticationRequest.getSecret().equals(password) ) {
emitter.next(AuthenticationResponse.success((String) authenticationRequest.getIdentity()));
emitter.complete();
} else {
emitter.error(AuthenticationResponse.exception());
}
}, FluxSink.OverflowStrategy.ERROR);
}
}
3b. ルール適用
ルールの判定処理。
ユーザー認証していない場合は authentication に null が渡されるので、AuthorizationException を投げて中断させる。
ユーザー認証している場合は「誰なのか?」の情報が authentication でわたってくるので、必要ならそれで判断すればよさそう。
前述のとおり、全アクセスについて判定しようとされることに注意。
また、ファイルが存在するかどうか(404エラー)は考慮しなくてよい。
参考: https://blog.wick.technology/micronaut-security-rule/
import io.micronaut.http.HttpRequest;
import io.micronaut.security.authentication.Authentication;
import io.micronaut.security.authentication.AuthorizationException;
import io.micronaut.security.rules.AbstractSecurityRule;
import io.micronaut.security.rules.SecurityRuleResult;
import io.micronaut.security.token.RolesFinder;
import io.micronaut.web.router.RouteMatch;
import jakarta.inject.Singleton;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
@Singleton
public class CustomSecurityRule extends AbstractSecurityRule {
public CustomSecurityRule(RolesFinder rolesFinder) {
super(rolesFinder);
}
@Override
public Publisher<SecurityRuleResult> check(HttpRequest<?> request, RouteMatch<?> routeMatch, Authentication authentication) {
String path = request.getPath();
if( path.startsWith("/admin/") && authentication == null ) {
throw new AuthorizationException(null);
}
return Mono.just(SecurityRuleResult.ALLOWED);
}
}
3c. HTTPレスポンス処理
公式にコードあり。AuthorizationException が発行されると実行されるハンドラ。
真ん中のif文は「お前が誰だかわかったけど、お前には使わせない」ケースと思われる。
参考: https://guides.micronaut.io/latest/micronaut-security-basicauth-maven-java.html#www-authenticate
import io.micronaut.context.annotation.Replaces;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MutableHttpResponse;
import io.micronaut.security.authentication.AuthorizationException;
import io.micronaut.security.authentication.DefaultAuthorizationExceptionHandler;
import jakarta.inject.Singleton;
import static io.micronaut.http.HttpHeaders.WWW_AUTHENTICATE;
import static io.micronaut.http.HttpStatus.FORBIDDEN;
import static io.micronaut.http.HttpStatus.UNAUTHORIZED;
@Singleton
@Replaces(DefaultAuthorizationExceptionHandler.class)
public class AuthorizationExceptionHandler extends DefaultAuthorizationExceptionHandler {
@Override
protected MutableHttpResponse<?> httpResponseWithStatus(HttpRequest request,
AuthorizationException e) {
if (e.isForbidden()) {
return HttpResponse.status(FORBIDDEN);
}
return HttpResponse.status(UNAUTHORIZED)
.header(WWW_AUTHENTICATE, "Basic realm=\"hoge\"");
}
}
おまけ:HTTPエラーレスポンスの実装方法
400 Bad Request
HttpStatusException(HttpStatus.BAD_REQUEST) を投げて、このハンドラを実装すればOK
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.annotation.Produces;
@Controller
@Produces(MediaType.TEXT_PLAIN)
public class BadRequestController {
@Error(status = HttpStatus.BAD_REQUEST, global = true)
public HttpResponse<?> badRequest(HttpRequest<?> request) {
return HttpResponse.badRequest("Bad Request");
}
}
404 Page Not Found
static-resourceが存在しないのもここに飛んでくる。
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Error;
import io.micronaut.http.annotation.Produces;
@Controller
@Produces(MediaType.TEXT_PLAIN)
public class NotFoundController {
@Error(status = HttpStatus.NOT_FOUND, global = true)
public HttpResponse<?> notFound(HttpRequest<?> request) {
return HttpResponse.notFound("Page Not Found");
}
}
例外が発生したら 500 Internal Server Error
Exceptionクラスに対してハンドラを実装すればOK。
上記の各種例外は飛んでこないので安心。
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Produces;
import io.micronaut.http.server.exceptions.ExceptionHandler;
import jakarta.inject.Singleton;
@Produces(MediaType.TEXT_PLAIN)
@Singleton
@Requires(classes = {Exception.class, ExceptionHandler.class})
public class DefaultExceptionHandler implements ExceptionHandler<Exception, HttpResponse> {
@Override
public HttpResponse handle(HttpRequest request, Exception exception) {
return HttpResponse.serverError("Internal Server Error");
}
}