0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React(SPA),REST API(Spring Boot)での認証認可

Last updated at Posted at 2025-05-06

この記事は未完成,未検証です

  • roleによるUserのバリデーション未実装
  • CSRF対策未実装

概要

React(SPA),Spring Boot(REST API)の構成において,認証認可を実装する.
JWTによる認証を行い,保護されたAPI呼び出しを行う.

構成

  • React + React Router v7 + useSWR
  • Spring Boot + Spring Security
  • PostgreSQL

認証認可の流れ

[React Login Page]
   ↓ POST /api/login
   ↓
[Spring Boot]
   認証成功 → JWT発行 → Cookieにセット (HttpOnly)
   ↓
[React]
   useSWRによるAPI呼び出し
   ↓ GET /api/easys
   ↓
[Spring Boot]
   Cookie内のJWTを検証 → 認証OK → Easy一覧返却

Spring Boot 側(認証+保護されたAPI)

application.properties

spring.datasource.url=jdbc:postgresql://localhost:5432/your_db
spring.datasource.username=your_username
spring.datasource.password=your_password
jwt.secret=十分長いランダム文字列
app.cookie.secure=false // ローカル環境
app.cookie.secure=true // 本番環境
cors.allowed-origins=http://localhost:3000

Entity

  • AppUser.java

  • Easy.java

    AppUser.java
    public class AppUser {
        private Long id;
        private String email;
        private String password;
        private String role;
        // Getter, Setter
    }
    
    Easy.java
    public class Easy {
        private Long id;
        private String title;
        private String detail;
        // Getter, Setter
    }
    

Controller

  • AuthUserController.java

    • LoginRequest.java(DTO)
    • RegisterRequest.java(DTO)
  • EasyController.java

    AuthUserController.java
    @RestController
    @RequestMapping("/api")
    public class AuthUserController {
    
        @Autowired
        private AuthUserService authUserService;
    
        @Autowired
        private AuthUserRepository authUserRepository;
    
        @Value("${app.cookie.secure}")
        private boolean secureCookie;
    
      @PostMapping("/register")
        public ResponseEntity<String> register(@RequestBody RegisterRequest req) {
            if (authUserRepository.findByEmail(req.getEmail()).isPresent()) {
                return ResponseEntity
                    .status(HttpStatus.CONFLICT)
                    .body("このメールアドレスは既に使用されています");
            }
    
            try {
                authUserService.register(req.getEmail(), req.getPassword(), req.getRole());
                return ResponseEntity
                    .status(HttpStatus.CREATED)
                    .body("ユーザーを登録しました");
            } catch (DuplicateKeyException e) {
                // 複合ユニーク制約違反など
                return ResponseEntity
                    .status(HttpStatus.CONFLICT)
                    .body("登録済みのデータです");
            } catch (DataAccessException e) {
                return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("データベースエラーが発生しました");
            } catch (Exception e) {
                return ResponseEntity
                    .status(HttpStatus.INTERNAL_SERVER_ERROR)
                    .body("サーバー内部でエラーが発生しました");
            }
        }
    
        @PostMapping("/login")
        public ResponseEntity<Void> login(
                @RequestBody LoginRequest request,
                HttpServletResponse response) throws IOException {
            try {
                authUserService.authenticate(request.getEmail(), request.getPassword());
                String token = authUserService.generateToken(request.getEmail());
    
                Cookie cookie = new Cookie("jwt", token);
                cookie.setHttpOnly(true);
                cookie.setPath("/");
                cookie.setMaxAge(3600);
                cookie.setSecure(secureCookie);
                cookie.setSameSite("None");
                
                response.addCookie(cookie);
                return ResponseEntity.ok().build();
            } catch (AuthenticationException e) {
                return ResponseEntity
                    .status(HttpStatus.UNAUTHORIZED)
                    .build();
            }
        }
    }
    
    LoginRequest.java
    public class LoginRequest {
        private String email;
        private String password;
        // getter/setter
    }
    
    RegisterRequest.java
    public class RegisterRequest {
        private String email;
        private String password;
        private String role;
        // getter / setter
    }
    
    EasyController.java
    @RestController
    @RequestMapping("/api/easys")
    public class EasyController {
    
        @Autowired private EasyService easyService;
    
        @GetMapping
        public ResponseEntity<List<Easy>> getEasys() {
            return ResponseEntity.ok(easyService.getAllEasys());
        }
    }
    

Service

  • AuthUserService.java

    • JwtUtil.java
      • JWTの生成,検証を担当するユーティリティクラス
      • application.propertiesのsecretをキーとして読み込む
      • generateToken(): JWTを発行
      • extractEmail(): JWTからsubject(email)を取り出す
      • isTokenValid(): 署名&有効期限チェック+ユーザ一致確認
    • JwtCookieFilter.java
      • リクエストごとにJWTをチェックし,認証コンテキストを設定するフィルタ
      1. Cookie から "jwt" を取得
      2. JwtUtil でトークン検証
      3. 検証成功時はSecurityContextに認証情報をセット
  • EasyService.java

    AuthUserService.java
    @Service
    public class AuthUserService {
        @Autowired private AuthUserRepository authUserRepository;
        @Autowired private PasswordEncoder passwordEncoder;
        @Autowired private AuthenticationManager authenticationManager;
        @Autowired private JwtUtil jwtUtil;
    
        public void authenticate(String email, String password) {
            Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(email, password)
            );
        }
    
        
        public void register(String email, String rawPassword, String role) {
            String hashed = passwordEncoder.encode(rawPassword);
            authUserRepository.save(email, hashed, role);
        }
        
        public String generateToken(String email) {
            return jwtUtil.generateToken(email);
        }
    }
    
    JwtUtil.java
    @Component
    public class JwtUtil {
    
        private final Key key;
    
        public JwtUtil(@Value("${jwt.secret}") String secret) {
            byte[] keyBytes = Decoders.BASE64.decode(secret);
            this.key = Keys.hmacShaKeyFor(keyBytes);
        }
    
        public String generateToken(String email) {
            return Jwts.builder()
                .setSubject(email)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 3600_000))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();
        }
    
        public String extractEmail(String token) {
            return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
        }
    
        public boolean isTokenValid(String token, UserDetails userDetails) {
            Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
            boolean sameUser = claims.getSubject().equals(userDetails.getUsername());
            boolean notExpired = claims.getExpiration().after(new Date());
            return sameUser && notExpired;
        }
    }
    
    JwtCookieFilter.java
    @Component
    public class JwtCookieFilter extends OncePerRequestFilter {
        @Autowired private JwtUtil jwtUtil;
        @Autowired private UserDetailsService userDetailsService;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
                throws ServletException, IOException {
            try {
                Cookie[] cookies = request.getCookies();
                if (cookies != null) {
                    for (Cookie cookie : cookies) {
                        if ("jwt".equals(cookie.getName())) {
                            String token = cookie.getValue();
                            String email = jwtUtil.extractEmail(token);
                            if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                                UserDetails userDetails = userDetailsService.loadUserByUsername(email);
                                if (jwtUtil.isTokenValid(token, userDetails)) {
                                    UsernamePasswordAuthenticationToken auth =
                                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                                    SecurityContextHolder.getContext().setAuthentication(auth);
                                }
                            }
                        }
                    }
                }
            } catch (JwtException | IllegalArgumentException e) {
                // ログを出力する(環境に合わせて,適切な出力先を設定してください)
                System.err.println("JWT検証失敗: " + e.getMessage());
            }
            chain.doFilter(request, response);
        }
    }
    
    EasyService.java
    @Service
    public class EasyService {
    
        @Autowired private EasyRepository easyRepository;
    
        public List<Easy> getAllEasys() {
            return easyRepository.findAll();
        }
    }
    

Repository

  • AuthUserRepository.java

  • EasyRepository.java

    AuthUserRepository.java
    @Repository
    public class AuthUserRepository {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public Optional<AppUser> findByEmail(String email) {
            String sql = "SELECT * FROM app_user WHERE email = ?";
            try {
                AppUser user = jdbcTemplate.queryForObject(sql, new Object[]{email}, (rs, rowNum) -> {
                    AppUser u = new AppUser();
                    u.setId(rs.getLong("id"));
                    u.setEmail(rs.getString("email"));
                    u.setPassword(rs.getString("password"));
                    u.setRole(rs.getString("role"));
                    return u;
                });
                return Optional.of(user);
            } catch (EmptyResultDataAccessException e) {
                return Optional.empty();
            }
        }
    
        public void save(String email, String password, String role) {
            String sql = "INSERT INTO app_user(email, password, role) VALUES (?, ?, ?)";
            jdbcTemplate.update(sql, email, password, role);
        }
    }
    
    EasyRepository.java
    @Repository
    public class EasyRepository {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        public List<Easy> findAll() {
            String sql = "SELECT * FROM easy ORDER BY id";
            return jdbcTemplate.query(sql, (rs, rowNum) -> {
                Easy e = new Easy();
                e.setId(rs.getLong("id"));
                e.setTitle(rs.getString("title"));
                e.setDetail(rs.getString("detail"));
                return e;
            });
        }
    }
    

Config

  • SecurityConfig.java

    • Spring Securityの設定クラス
    • JWT認証フィルタの追加やCSRF/CORS 設定,認可ルール(login/registerは許可,それ以外は認証必須)を行う
  • AppUserDetails.java

    • Spring Securityがユーザ情報を扱うためのラッパー
    • AppUserのemail/password/roleをUserDetailsインタフェースにマッピングする
  • CorsConfig.java

    • CORS(クロスオリジン)設定クラス
    • allowedOrigins:フロントエンドのオリジンをapplication.properties から読み込み
    • allowCredentials:Cookieを含むリクエストを許可
    SecurityConfig.java
    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {
    
        @Autowired private JwtCookieFilter jwtFilter;
        @Autowired private AuthUserRepository authUserRepository;
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http
              .csrf(csrf -> csrf
                  .ignoringRequestMatchers(
                      new AntPathRequestMatcher("/api/login"),
                      new AntPathRequestMatcher("/api/register")
                  )
              )
              .cors().and()
              .authorizeHttpRequests(authz -> authz
                  .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                  .requestMatchers("/api/login", "/api/register").permitAll()
                  .anyRequest().authenticated()
              )
              .sessionManagement(sess -> sess
                  .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
              )
              .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService() {
            return username -> {
                AppUser u = authUserRepository.findByEmail(username)
                    .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
                return new AppUserDetails(u);
            };
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Bean
        public AuthenticationManager authManager(AuthenticationConfiguration config) throws Exception {
            return config.getAuthenticationManager();
        }
    }
    
    AppUserDetails.java
        public class AppUserDetails implements UserDetails {
    
        private final AppUser user;
    
        public AppUserDetails(AppUser user) {
            this.user = user;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            return List.of(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
        }
    
        @Override
        public String getPassword() {
            return user.getPassword();
        }
    
        @Override
        public String getUsername() {
            return user.getEmail();
        }
    
        @Override
        public boolean isAccountNonExpired() { return true; }
    
        @Override
        public boolean isAccountNonLocked() { return true; }
    
        @Override
        public boolean isCredentialsNonExpired() { return true; }
    
        @Override
        public boolean isEnabled() { return true; }
    
        public Long getId() {
            return user.getId();
        }
    }
    
    CorsConfig.java
    @Configuration
    public class CorsConfig {
        @Value("${cors.allowed-origins}")
        private List<String> allowedOrigins;
    
        @Bean
        public CorsConfigurationSource corsConfigurationSource() {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowedOrigins(allowedOrigins);
            config.setAllowedMethods(List.of("GET","POST","OPTIONS"));
            config.setAllowedHeaders(List.of("*"));
            config.setAllowCredentials(true);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", config);
            return source;
        }
    }
    

React側(認証とAPI呼び出し)

  • User.ts

    User.ts
    interface User {
      id: number,
      email: string,
      password: string,
      role: string
    }
    
  • Easy.ts

    Easy.ts
    interface Easy {
      id: number,
      title: string,
      detail: string
    }
    
  • React Router 構成

    <Routes>
      <Route path="/login" element={<LoginPage />} />
      <Route path="/easys" element={<EasysPage />} />
    </Routes>
    
  • api/login.ts

    api/axios.ts
     import axios from 'axios';
     
     export const apiClient = axios.create({
       baseURL: 'http://localhost:8080',
       withCredentials: true,
       headers: {
          'Content-Type': 'application/json',
       },
     });
    
    api/login.ts
    import { apiClient } from './axios';
    
    export const login = async (email: string, password: string) => {
      await apiClient.post('/api/login', { email, password });
    };
    

    { withCredentials: true }とすることで,CookieをHTTP headerに入れて扱うことができる.

  • useEasys.ts

    useEasys.ts
    import useSWR from 'swr';
    import { apiClient } from '../api/axios';    
    
    const fetcher = (path: string) =>
      apiClient.get(path).then(res => res.data);
    
    export const useEasys = () => {
      const { data, error, isLoading } = useSWR('/api/easys', fetcher);
    
      return {
        easys: data,
        isLoading,
        isError: error,
      };
    };
    

    Easyの一覧を取得するAPI

  • LoginPage.tsx

    LoginPage.tsx
    import { useState } from 'react';
    import { useNavigate } from "react-router";
    import { login } from '../api/login';
    
    export const LoginPage = () => {
      const [email, setEmail] = useState('');
      const [password, setPassword] = useState('');
      const navigate = useNavigate();
    
      const onSubmit = async () => {
        try {
          await login(email, password);
          navigate('/easys');
        } catch {
          alert('ログインに失敗しました');
        }
      };
    
      return (
        <div>
          <input value={email} onChange={e => setEmail(e.target.value)} />
          <input
            type="password"
            value={password}
            onChange={e => setPassword(e.target.value)}
          />
          <button onClick={onSubmit}>ログイン</button>
        </div>
      );
    };
    

    ログイン成功後,Easy一覧に遷移する.

  • EasysPage.tsx

    EasysPage.tsx
    import React from 'react';
    import { useEasys } from '../hooks/useEasys';
    
    export const EasysPage = () => {
      const { easys, isLoading, isError } = useEasys();
    
      if (isLoading) return <div>読み込み中...</div>;
      if (isError) return <div>エラーが発生しました</div>;
    
      return (
        <ul>
          {easys.map((easy: { id: number; title: string; detail: string }) => (
            <li key={easy.id}>
              {easy.title}: {easy.detail}
            </li>
          ))}
        </ul>
      );
    };
    
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?