포스트

JWT + 카카오 소셜 로그인 (2)

📌 React와 Spring Boot를 사용하여 JWT를 기반으로 로그인 세션을 관리하는 과정

1. React에서 API 요청

React 클라이언트에서는 이메일과 비밀번호를 입력 후 로그인 API를 호출하여 사용자 인증을 처리

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
import axios from 'axios';

class UserService {
    static BASE_URL = 'http://localhost:8088';

    static async login(email, password) {
        const response = await axios.post(`${this.BASE_URL}/user/login`, {
            email,
            password,
        });
        return response.data;
    }
}
  • axios.post를 사용하여 로그인 API를 호출

2. 로그인 및 토큰 재발급 컨트롤러

서버단 컨트롤러에서는 로그인 및 토큰 재발급 요청을 처리

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController {

    private UserService userService;

    @PostMapping("/login")
    public ResponseEntity<UserDTO> login(@RequestBody UserDTO userDTO) {
        return ResponseEntity.ok(userService.login(userDTO));
    }

    // 새로고침 토큰
    @PostMapping("/refresh")
    public ResponseEntity<UserDTO> refreshToken(@RequestBody UserDTO userDTO) {
        return ResponseEntity.ok(userService.refreshToken(userDTO));
    }
}
  • /login 엔드포인트에서 사용자 인증 후 JWT 토큰을 반환
  • /refresh 엔드포인트에서 새로고침 토큰을 사용해 새로운 JWT 토큰을 반환

3. UserService 클래스

UserService 클래스에서는 사용자 로그인 및 토큰 재발급 로직을 구현

코드 보기
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class UserService {
    public UserDTO login(UserDTO userDTO) {
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(userDTO.getEmail(), userDTO.getPassword())
        );

        UserEntity user = userRepository.findByEmail(userDTO.getEmail())
            .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다. 이메일: " + userDTO.getEmail()));

        // 조회된 사용자 정보를 바탕으로 JWT 토큰 생성
        String jwt = jwtUtils.generateToken(user);

        // 비어있는 맵과 사용자 정보를 바탕으로 새로고침 토큰 생성
        String refreshToken = jwtUtils.generateRefreshToken(new HashMap<>(), user);

        UserDTO response = UserDTO.toDTO(user);
        response.setToken(jwt);
        response.setRole(user.getRole());
        response.setRefreshToken(refreshToken);

        return response;
    }

    public UserDTO refreshToken(UserDTO userDTO) {
        UserDTO response = new UserDTO();

        // 토큰에서 사용자 이메일 추출
        String ourEmail = jwtUtils.extractUsername(userDTO.getToken());

        // 이메일로 사용자 정보 조회, 없으면 예외 발생
        UserEntity users = userRepository.findByEmail(ourEmail)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다. 이메일: " + ourEmail));

        // 토큰 유효성 검사 후 유효하다면 새로운 토큰 생성
        if (jwtUtils.isTokenValid(userDTO.getToken(), users)) {
            String jwt = jwtUtils.generateToken(users);
            response.setToken(jwt); 
            response.setRefreshToken(userDTO.getToken());   
        }

        return response;
    }

}
  • 로그인 시 사용자 인증을 처리하고 JWT 및 새로고침 토큰을 생성
  • 새로고침 토큰 유효성을 검사하고, 유효하다면 새로운 JWT를 생성

4. JWTUtils 클래스

JWTUtils 클래스에서는 JWT 생성 및 검증에 필요한 유틸리티 메서드를 구현

코드 보기
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.petstagram.service.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.function.Function;

@Component
public class JWTUtils {

    private SecretKey key;
    private static final long ACCESS_TOKEN_EXPIRATION_TIME = 1800000; // 30분
    private static final long REFRESH_TOKEN_EXPIRATION_TIME = 604800000; // 7일

    public JWTUtils(@Value("${jwt.secret}") String secretString) {
        byte[] keyBytes = Base64.getDecoder().decode(secretString.getBytes(StandardCharsets.UTF_8));
        this.key = new SecretKeySpec(keyBytes, "HmacSHA256");
    }

    public String generateToken(UserDetails userDetails){
        return Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + ACCESS_TOKEN_EXPIRATION_TIME))
                .signWith(key)
                .compact();
    }

    public String generateRefreshToken(HashMap<String, Object> claims, UserDetails userDetails){
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(userDetails.getUsername())
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + REFRESH_TOKEN_EXPIRATION_TIME))
                .signWith(key)
                .compact();
    }

    public String extractUsername(String token){
        return extractClaims(token, Claims::getSubject);
    }

    private <T> T extractClaims(String token, Function<Claims, T> claimsResolver){
        return claimsResolver.apply(Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody());
    }

    public boolean isTokenValid(String token, UserDetails userDetails){
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }

    public boolean isTokenExpired(String token){
        return extractClaims(token, Claims::getExpiration).before(new Date());
    }
}

  • generateToken 메서드는 액세스 토큰을 생성
  • generateRefreshToken 메서드는 새로고침 토큰을 생성
  • extractUsername 메서드는 토큰에서 사용자 이름을 추출
  • isTokenValid 메서드는 토큰 유효성을 검사합니다.출

5. OurUserDetailsService 클래스

OurUserDetailsService 클래스는 사용자 인증에 필요한 사용자 정보를 데이터베이스에서 조회

코드 보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.petstagram.service.utils;

import com.petstagram.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class OurUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findByEmail(username)
                .orElseThrow(() -> new IllegalArgumentException("사용자를 찾을 수 없습니다. email = " + username));
    }
}

  • loadUserByUsername 메서드는 이메일로 사용자 정보를 조회
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.