Post

JWT를 활용한 React와 Spring Boot의 인증 처리

JWT(JSON Web Token)

JWT는 토큰 기반의 인증 방식으로, 서버에서 클라이언트에게 JWT를 발급하고, 클라이언트는 이 토큰을 API 요청 시마다 서버에 전달하여 인증을 받는 방식입니다.

Spring Boot에서 JWT 설정하기

Spring Boot에서 JWT를 사용해 로그인과 회원가입을 처리하는 API를 구현한 부분입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/api/auth")
public class AuthController {
    
    @PostMapping("/login")
    public ResponseEntity<?> authenticateUser(@RequestBody LoginRequest loginRequest) {
        // 인증 로직 (username과 password 검증)
        // JWT 생성
        String jwt = jwtProvider.generateToken(loginRequest.getUsername());
        return ResponseEntity.ok(new JwtResponse(jwt));
    }

    @PostMapping("/signup")
    public ResponseEntity<String> SignUp(@RequestBody User user) throws UserException {
        try {
            authService.saveUser(user);
            return ResponseEntity.ok("유저 정보 저장 성공!!");
        } catch(UserException e) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
        }
    }
}

authService에서는 사용자 정보를 검증하고 JWT를 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void saveUser(User user) throws UserException {
    User existingUser = userRepository.findByUserName(user.getUserName());
    if (existingUser != null) {
        throw UserException.duplicateUserException();
    }
    userRepository.save(user);
}

public void loginUser(User user) throws UserException {
    User existingUser = userRepository.findByUserNameAndUserPwd(user.getUserName(), user.getUserPwd());
    if (existingUser == null) {
        throw UserException.invalidUserException();
    }
}

React에서 JWT로 API 호출하기

React에서는 로그인 후 받은 JWT 토큰을 localStorage에 저장하고, 이후 API를 호출할 때마다 이 토큰을 헤더에 추가하는 방식으로 인증을 처리했습니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function login(username, password) {
  axios.post('/api/auth/login', { username, password })
    .then(response => {
      localStorage.setItem('token', response.data.token);
    })
    .catch(error => {
      console.error('Login error', error);
    });
}

function fetchToken() {
  const token = localStorage.getItem('token');
  axios.get('/api/protected-resource', {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  }).then(response => {
    console.log(response.data);
  }).catch(error => {
    console.error('Fetching error', error);
  });
}

문제

  • 토큰 만료 처리 : 처음에는 토큰 만료 시 처리 방법에 대해 고민이 있었습니다. 최종적으로는 만료된 토큰으로 API 호출 시 서버에서 401 Unauthorized 응답을 받을 경우, 클라이언트에서 자동으로 로그아웃하고, localStorage에서 토큰을 삭제하는 방식으로 구현했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const handleLogin = (event) => {
  event.preventDefault();
  // 사용자 정보를 객체로 생성
  let user = {
    userName: name,
    userPwd: pwd,
  };

  // 로그인 API 호출
  UserService.login(user)
    .then((res) => {
      localStorage.setItem("accessToken", res.data.token);
      navigate(`/`);
    })
    .catch((error) => {
      if (error.response && error.response.status === 401) {
        window.alert("아이디나 비밀번호가 다릅니다.");
        localStorage.removeItem('accessToken');
        navigate('/login');
      } else {
        console.log("Login error:", error);
        window.alert("로그인 중 오류가 발생했습니다.");
      }
    });
};
  • 성공 처리 : 로그인에 성공하면, 응답에서 받은 JWT 토큰을 localStorage에 저장하고 메인 페이지로 리다이렉트합니다.
  • 오류 처리
    • 401 오류 처리 : 로그인 요청 시 401 Unauthorized 응답이 오면, window.alert로 에러 메시지를 보여주고, 저장된 토큰을 삭제한 후 로그인 페이지로 리다이렉트합니다.
    • 기타 오류 : 다른 종류의 오류가 발생한 경우, 해당 오류를 콘솔에 기록하고 일반적인 에러 메시지를 보여줍니다.

      고민

  • 보안 : JWT를 localStorage에 저장하는 것은 보안에 취약하다는 점을 알게 되었습니다. 이 문제는 다음 포스트에서 Spring Security를 활용하여 개선할 계획입니다.

  • Refresh Token : 리프레시 토큰을 사용하여 세션을 유지하는 방법도 고려했지만, 초기에는 리프레시 토큰을 관리하는 로직이 복잡하여 많은 시간을 소모했습니다. 현재는 토큰 만료 시 사용자에게 다시 로그인하도록 유도하는 방식으로 처리하고 있습니다.
This post is licensed under CC BY 4.0 by the author.