🛩️

【Spring Boot】Spring for GraphQLで関数ごとに認証の有無を設定できるようにする

2024/07/03に公開

概要

Spring for GraphQLではチュートリアルにある通り、リクエストのUTLのパスは基本的に単一であり、関数ごとに認証をかけたい場合は少し工夫が必要です。
取れる対応案としては複数あると思いますが、今回はGraphQL backend — authorization & authenticationの記事で紹介されているような方法で実装してみたので、その紹介です。

前提

  • 上記記事ではKotlinでの実装ですが、この記事ではJavaで実装します。
  • 使用したspring-boot-starter-graphqlのバージョンは3.3.0です。

実装

まずは認証部分の実装となるRequestManagerです。
ここでは、ヘッダーからのトークン取得と、一度認証した情報を取得できる関数を実装しています。

RequestManager
@Component
public class RequestManager {
    @Autowired
    UserAccountService userAccountService;

    private final String LOGIN_USER_ID_SESSION = "userAccountId";

    // Authorizationヘッダーからトークンをdecodeして保存する処理
    public void saveUserAccountIdSession(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        // Authorizationヘッダーが設定されているか判定
        if (authHeader != null && authHeader.split(" ").length > 1) {
            String bearerToken = authHeader.split(" ")[1];
            try {
                // トークンの検証(実装内容は割愛)
                String userAccountId = userAccountService.getUserIdFromAuthToken(bearerToken);
                request.setAttribute(LOGIN_USER_ID_SESSION, userAccountId);
            } catch (Exception e) {
                // 失敗した場合は何もしない
            }
        }
    }

    // ContextHolderからユーザ情報を取得する処理
    public String getUserAccountIdSession() {
        Object session = RequestContextHolder
                .getRequestAttributes().getAttribute(LOGIN_USER_ID_SESSION, RequestAttributes.SCOPE_REQUEST);
        return (String) session;
    }

}

次にRequestFilterです。filterにてRequestManagerのヘッダー取得処理を呼び出しています。

RequestFilter
@Component
public class RequestFilter extends OncePerRequestFilter {
    @Autowired
    RequestManager requestManager;

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        // リクエストヘッダーのトークンを検証してユーザIDを格納する処理
        requestManager.saveUserAccountIdSession(request);
        filterChain.doFilter(request, response);
    }

}

次にアノテーションの実装です。
今回はLoggedInOnlyというアノテーションを用意して、このアノテーションが付与されたら認証チェックが行われるようにします。

LoggedInOnly
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LoggedInOnly {}
AuthAspect
@Aspect
@Component
public class AuthAspect {
    @Autowired
    RequestManager requestManager;

    @Before("@annotation(com.sample.annotation.loginStatus.LoggedInOnly)")
    public void checkLoggedInOnly() throws GraphqlErrorException {
        String userAccountId = requestManager.getUserAccountIdSession();
        if (userAccountId == null) {
            throw GraphqlErrorException
                    .newErrorException()
                    .errorClassification(ErrorType.FORBIDDEN)
                    .message("Not Login")
                    .build();
        }
    }
}

最後に関数へアノテーションを設定する部分の実装です。

PostCategoryController
@Controller
public class PostCategoryController {
    @Autowired
    PostCategoryService postCategoryService;
    @Autowired
    RequestManager requestManager;

    @MutationMapping
    @LoggedInOnly
    public Boolean addPostCategory(@Argument String name) throws GraphqlErrorException {
        String userAccountId = requestManager.getUserAccountIdSession();
        return postCategoryService.addPostCategory(name);
    }
}

Discussion