React×Spring Bootで認証・認可を実装した話

要約

この記事では、ReactとSpring Bootを使った認証・認可の設定方法について解説します。特に、フロントエンドで認証後にユーザー情報をバックエンドから受け取り、特定のページにアクセスできるようにする方法を説明します。

環境

今回使用した環境は以下の通りです。

・React: 18.3.1
・React Router DOM: 6.24.1
・Spring Boot: 3.3.1
・Spring Security: 6.3.1

知識のおさらい

認証(Authentication)とは?
認証は、ユーザーが誰であるかを確認するプロセスです。通常、ユーザー名とパスワードを使用してログインし、その資格情報を検証することで認証が行われます。

認可(Authorization)とは?
認可は、認証されたユーザーが特定のリソースや操作にアクセスできるかどうかを決定するプロセスです。認可は、ユーザーが特定の役割(例:管理者、一般ユーザー)を持っているかどうかに基づいて行われます。

現状の課題

現在、/loginページが存在し、/adminには認証ガードが設定されています。しかし、バックエンドで認証されたユーザー情報がフロントエンドに返されないため、/adminページへの遷移がうまくいっていません。

やりたいこと

フロントエンドで/adminにアクセスする際、/loginでユーザー認証を行った後に/adminページに遷移できるように、認可の設定を行います。また、認証されたユーザー情報をバックエンドからフロントエンドに返す必要があります。

実装方法

以下は、ReactとSpring Bootで認証・認可を実装するための具体的な手順です。

  1. React側の設定

・App.tsx

import React from "react";
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import Admin from "./components/Admin/Admin";
import Home from "./components/Home/Home";
import Login from "./components/Login/Login";
import PrivateRoute from "./components/PrivateRoute"; // 作成したPrivateRoute.tsx

const App: React.FC = () => {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route
          path="/admin"
          element={
            <PrivateRoute>
              <Admin />
            </PrivateRoute>
          }
        />
      </Routes>
    </Router>
  );
};

export default App;

・PrivateRoute.tsx

import React from "react";
import { Navigate } from "react-router-dom";

const PrivateRoute: React.FC<{ children: JSX.Element }> = ({ children }) => {
  const isAuthenticated = localStorage.getItem("isAuthenticated") === "true";

  return isAuthenticated ? children : <Navigate to="/" />;
};

export default PrivateRoute;

2.Spring Boot側の設定

package com.sample;

import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {

    private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .cors(cors -> cors
                        .configurationSource(request -> {
                            var corsConfiguration = new org.springframework.web.cors.CorsConfiguration();
                            corsConfiguration.setAllowedOrigins(List.of("http://localhost:3000"));
                            corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
                            corsConfiguration.setAllowCredentials(true);
                            corsConfiguration.setAllowedHeaders(List.of("*"));
                            return corsConfiguration;
                        }))
                .csrf(csrf -> csrf
                        .csrfTokenRepository(csrfTokenRepository())
                        .ignoringRequestMatchers("/csrf")
                        .ignoringRequestMatchers("/h2-console/**"))
                .formLogin(form -> form
                        .loginProcessingUrl("/login")
                        .successHandler(authenticationSuccessHandler())
                        .failureHandler(new SimpleUrlAuthenticationFailureHandler())
                        .permitAll())
                .logout(logout -> logout
                        .logoutSuccessUrl("/")
                        .permitAll())
                .authorizeHttpRequests(authz -> authz
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                        .requestMatchers("/", "/api/user", "/csrf").permitAll()
                        .requestMatchers("/admin").hasRole("ADMIN")
                        .requestMatchers("/h2-console/**").permitAll()
                        .anyRequest().authenticated())
                .headers(headers -> headers
                        .frameOptions(frameOptions -> frameOptions.sameOrigin()));

        return http.build();
    }

    @Bean
    public CsrfTokenRepository csrfTokenRepository() {
        HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
        repository.setParameterName("_csrf");
        repository.setHeaderName("X-CSRF-TOKEN");
        return repository;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
        return (request, response, authentication) -> {
            HttpSession session = request.getSession(false);
            if (session != null) {
                CsrfToken csrfToken = (CsrfToken) session.getAttribute(CsrfToken.class.getName());
                if (csrfToken != null) {
                    logger.info("CSRF Token from session: {}", csrfToken.getToken());
                } else {
                    logger.warn("No CSRF Token found in session");
                }
            } else {
                logger.warn("No session found");
            }

            // ユーザー情報を含むJSONレスポンスを返す
            String username = authentication.getName();
            response.setStatus(HttpServletResponse.SC_OK);
            response.setContentType("application/json");
            response.getWriter().write("{\"status\": \"success\", \"username\": \"" + username + "\"}");
            response.getWriter().flush();
        };
    }
}


説明

上記の設定では、Spring Securityの標準的なログインフォームを使用し、認証成功時にauthenticationSuccessHandler()メソッドが呼び出されるようにしています。このメソッドは、認証されたユーザーの情報を含むJSONレスポンスを返します。

このユーザー情報をフロントエンドで受け取り、セッションに保存することで、認証後に特定のページにアクセスできるようになります。具体的には、PrivateRouteコンポーネントがセッション情報を確認し、認証済みの場合にのみ子コンポーネントを表示するようになっています。

まとめ

ReactとSpring Bootを使った認証・認可の設定方法について説明しました。この記事を参考にすることで、フロントエンドとバックエンドの認証・認可の連携をスムーズに実装できるようになります。是非試してみてください。


この記事が気に入ったらサポートをしてみませんか?