LoginSignup
0
0

More than 1 year has passed since last update.

MicronautでBASIC認証を使う方法(とHTTPエラーレスポンス)

Posted at

基本的にはドキュメント通りにするんだけど、一部はまったとこのメモ。

そもそもの話

公式ドキュメントはどこ?

公式ドキュメントの16章に「こっちをみてね」くらいしか書いてないので非常にわかりにくい。:cry:
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

AuthenticationProviderUserPassword.java
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/

CustomSecurityRule.java
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

AuthorizationExceptionHandler.java
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

BadRequestController.java
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が存在しないのもここに飛んでくる。

NotFoundController.java
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。
上記の各種例外は飛んでこないので安心。

DefaultExceptionHandler.java
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");
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0