개발/Team Project

ep.12 JWT에서 정보를 추출하여 로그인한 회원 확인하기

김현중 (keemhing) 2024. 8. 21. 11:00

로그인 기능을 Spring Security와 JWT를 이용하여 구현했다.. 코드를 Git Repository에 Pull Request 하고...  팀원들과 공유했다.

각자의 개발환경에서 로그인 기능을 이용해본 3명의 팀원 모두 로그인 하고나서 회원 정보를 어떻게 알 수 있냐는 공통된 질문을 했다. 필자는 로그인 기능을 구현하면서 Spring Security와 JWT에 대해 공부를 하면서 작업했지만 팀원들은 각자의 기능에 대해 공부를 하면서 기능을 구현했기에 JWT가 무엇인지, Spring Security가 어떻게 작동되는지는 알지 못한다. 그래서 필자는 팀원들이 JWT에 포함되어 있는 회원의 계정 정보를 이용하여 회원의 모든 정보를 가져올 수 있는 코드를 작성해 보았다. 이제 코드를 보며 같이 알아가 보자.

 

JwtTokenProvider

필자는 JWT가 생성될 때 회원의 계정정보가 Payload안에 클레임으로 포함되게 작업하였다. (JWT 관련해서 는 이 포스팅에서 다루었다) 팀원들의 의견을 수렴하여 로그인을 하면 JWT가 생성되고 그 JWT안에 포함된 회원의 계정정보를 꺼내어 사용할 수 있도록 코드를 작성했다.

 

우선 JwtTokenProvider클래스의 getAuthentication 메서드를 보자. > JwtTokenProvider의 전체 코드

1. parseClaims메서드를 활용, Access 토큰을 복호화하여 토큰에 담긴 사용자 정보를 Authentication 객체로 변환한다.

2. UserDetails 객체로 사용자 정보를 추출하고 이 정보를 UsernamePasswordAuthenticationToken으로 래핑 하여 반환한다.

3. 이 메서드를 통해 서버는 토큰을 검증하고 토큰에 포함된 사용자 정보로 인증을 처리할 수 있다.

// 주어진 JWT 액세스 토큰을 복호화하여 토큰에 포함된 클레임(Claims) 정보를 추출하는 메서드
private Claims parseClaims(String accessToken) {
    try {
        // JWT 토큰을 복호화하고, 그 결과로 클레임(Claims) 객체를 반환
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
    } catch (ExpiredJwtException e) {
        // JWT 토큰이 만료된 경우 ExpiredJwtException이 발생하므로, 이 예외가 발생하면 만료된 클레임을 반환
        return e.getClaims();
    }
}
// JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 메서드
public Authentication getAuthentication(String accessToken) {
    // 토큰 복호화
    Claims claims = parseClaims(accessToken);

    // 사용자 계정이 null이거나 비어있는지 확인
    String userId = claims.getSubject();
    if (userId == null || userId.trim().isEmpty()) {
        throw new IllegalArgumentException("userId cannot be null or empty");
    }

    // 권한 정보가 null이거나 비어있는지 확인
    String authorityClaim = claims.get(AUTHORITIES_KEY, String.class);
    if (authorityClaim == null || authorityClaim.trim().isEmpty()) {
        throw new IllegalArgumentException("Authorities cannot be null or empty");
    }

    // 클레임에서 권한 정보 가져오기
    Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(authorityClaim.split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());

    // UserDetails 객체를 만들어서 Authentication 리턴
    UserDetails principal = new User(username, "", authorities);
    return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}

LoginService 작성

로그인 서비스에 추상 메서드를 작성해 주고...

package kr.co.vibevillage.user.model.service;

import kr.co.vibevillage.user.model.dto.UserDTO;

public interface LoginService {

    public String login(String userId, String userPassword);

    // 로그인한 유저 계정정보 가져오기
    public String getLoginUserId();

    // 로그인 유저 계정으로 데이터베이스에 있는 정보 가져오기
    public UserDTO getLoginUserInfo();
}

LoginServiceImpl 작성

추상 메서드를 구현해 준다.

1. Spring Security에서 제공하는 SecurityContextHolder를 활용하여. getContext() 메서드를 호출하여 현재 인증된 사용자에 대한 정보를 가져오고. getAuthentication() 메서드를 호출하여 Authentication객체를 추출한다. 이렇게 사용자의 인증 상태와 관련된 정보를 확인할 수 있다. 

2. Spring에서 제공하는 User 클래스를 이용해 추출해 온 정보를 user 객체에 초기화한다.

(User 클래스는 Spring Security에서 제공하는 UserDetails 인터페이스를 구현한 기본 구현체이다)

package kr.co.vibevillage.user.model.service;

import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.mapper.LoginMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class LoginServiceImpl implements LoginService{

    private final LoginMapper loginMapper;

    @Override
    public String login(String userId, String userPassword) {

        return loginMapper.login(userId, userPassword);
    }

    // 로그인한 사용자의 계정을 가져오는 메서드
    public String getLoginUserId() {
        // 현재 인증된 사용자 정보를 가져옴
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            // 인증된 사용자의 정보를 User 객체로 변환
            User user = (User) authentication.getPrincipal();
            return user.getUsername(); // 사용자 이름 반환
        } else {
            log.info("로그인 상태를 확인해주세요.");
            return null;
        }
    }

    // 로그인한 유저 계정으로 데이터베이스에서 정보 가져오기
    public UserDTO getLoginUserInfo() {
        String loginUserId = getLoginUserId();
        return loginMapper.getLoginUserInfo(loginUserId);
    }
}

LoginController

이제 로그인 컨트롤러에서 LoginServiceImpl 객체를 생성하고 getLoginUserId(), getLoginUserInfo() 메서드를 호출하여 로그인한 회원의 정보를 가져오면 된다.

package kr.co.vibevillage.user.controller;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;

import kr.co.vibevillage.jwt.config.JWTConfig;
import kr.co.vibevillage.jwt.provider.JwtTokenProvider;
import kr.co.vibevillage.user.model.dto.UserDTO;
import kr.co.vibevillage.user.model.service.LoginServiceImpl;

import lombok.RequiredArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.ArrayList;
import java.util.List;

@Slf4j
@Controller
@RequiredArgsConstructor // 초기화 되지 않은 final 필드나 @NonNull이 붙은 필드에 대한 생성자를 만들어줌
public class LoginController {

    private final LoginServiceImpl loginService;
    private final PasswordEncoder passwordEncoder;
    private final JWTConfig jwt;
    
    // AccessToken, RefreshToken 생성을 위한 객체 생성
    private final JwtTokenProvider jwtTokenProvider;

	// 로그인 유저 정보 가져오기 위한 객체 생성
    private final LoginServiceImpl loginServiceImpl;

    @Value("${jwt.secret}")
    private String secretKey;
    @Value("${jwt.expiration_time}")
    private Long expiredMs;

    @GetMapping("/login")
    public String login(@RequestParam("userId") String userId, @RequestParam("userPassword") String userPassword,
                        HttpServletResponse response, Model model) {

        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);

        String getPassword = loginService.login(userId, userPassword);

        List<String> authorities = new ArrayList<>();
        userDTO.setAuthorities(authorities);
        authorities.add("ROLE_USER"); // 기본적으로 "ROLE_USER" 권한을 추가

        if (passwordEncoder.matches(userPassword, getPassword)) {
            String token = jwt.createJwt(userDTO, secretKey, expiredMs);
            log.info("JWT: " + token);

            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            UserDTO.TokenResDto tokenResDto = jwtTokenProvider.generateToken(authentication);

            Cookie jwtCookie = new Cookie("JWT", token);
            jwtCookie.setHttpOnly(true);
            jwtCookie.setPath("/");
            response.addCookie(jwtCookie);

            Cookie accessTokenCookie = new Cookie("AccessToken", tokenResDto.getAccessToken());
            accessTokenCookie.setHttpOnly(true);
            accessTokenCookie.setPath("/");
            response.addCookie(accessTokenCookie);

            // 로그인한 사용자의 계정정보를 가져오기 위해 getLoggedInUsername 메서드 호출
            String loginUserId = loginServiceImpl.getLoginUserId();
            log.info("loginUserId: " + loginUserId);
            
            // 데이터 바인딩
            model.addAttribute("loginUserId", loginUserId);
			
            // 로그인한 사용자의 정보를 가져오기 위해 getLoggedInUsername 메서드 호출
            UserDTO loginUser = loginServiceImpl.getLoginUserInfo();
            log.info("유저번호: " + loginUser.getUserNo());
            log.info("닉네임: " + loginUser.getUserNickName());
            log.info("계정: " + loginUser.getUserId());
            log.info("암호: " + loginUser.getUserPassword());
            log.info("등급: " + loginUser.getUserLevel());

            return "redirect:/form";
        } else {
            return "redirect:/error";
        }
    }
}

 

로그인하여 테스트해 보기!

이제 로그인을 하면... 로그가 남는 걸 확인할 수 있다. 

 

JWT에서 회원정보를 추출하는 기능에 대해 포스팅해봤는데.... 이런 기능이 필요한데 어떻게 하면 구현이 가능할까?라고 계속 생각하며 내가 의도한 대로 코드를 작성한 뒤 결과가 만족스러울 때 가장 보람을 느낀다. 앞으로 남아있는 기능들도 열심히... 재미 붙여가며 구현해 봐야겠다.