종우의 삶 (전체 공개)

Market Project - 4 // 인증 후에는 어떤 것들을 할 수 있는지? 본문

개발/Spring

Market Project - 4 // 인증 후에는 어떤 것들을 할 수 있는지?

jonggae 2024. 2. 21. 00:53

 

지난 포스트에서 결과적으로 인증을 통과하고, 

SecurityContextHolderAuthentication 객체가 저장되고, 

LoginSuccessHandler를 통해 JWT 가 생성되어 어플리케이션 내에서 사용 가능한 상태가 되었다.

 

이제 JWT를 가지고 회원 관련 기능을 사용해보자.

 

우선 추후 개발항 상품 구매, 주문 기능 이전에 나의 정보를 표시하는 기능을 하나 만들어본다.

 

이번에는 테스트코드가 아닌 실제 Spring 서버를 돌려 API 테스트를 진행한다.

 

CustomerService에 이러한 메서드를 하나 생성한다.

 public CustomerDto getCustomerName() {
        return CustomerDto.from(
                SecurityUtil.getCurrentCustomerName()
                        .flatMap(customerRepository::findOneWithAuthoritiesByCustomerName)
                        .orElseThrow(() -> new NotFoundMemberException("회원을 찾을 수 없습니다"))
        );
    }

 

코드를 보면 알 수 있듯

비밀번호를 제외한 CustomerDto 객체를 보여주어 회원가입시 나의 정보를 확인할 수 있게 해준다

 

SecurityUtilSecurityContextHolder에 들어있는 정보를 추출하기 위해 사용하는 클래스이므로, 아래와 같이 구현해준다.

 

// 아래 getCurrentUsername() 메서드를 이용하여 인증된 Authentication 객체 내부의 요소들을 가져옴 (eg. customerName 같은)
public class SecurityUtil {

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

    private SecurityUtil() {}

    public static Optional<String> getCurrentCustomerName() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication == null) {
            logger.debug("Security context에 인증 정보가 없습니다.");
            return Optional.empty();
        }

        String username = null;

        if (authentication.getPrincipal() instanceof UserDetails) {
            UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
            username = springSecurityUser.getUsername();
        } else if (authentication.getPrincipal() instanceof String) {
            username = (String) authentication.getPrincipal();
        }
        return Optional.ofNullable(username);
    }
}

 

위 코드에서 Authentication 객체가 또한 중요하게 작용한다.

회원가입 후 DB에 저장된 데이터와 로그인 시도한 데이터가 일치한다면 JWT토큰이 발급되어 이것을 사용한 my-info 페이지 접근이 가능할 것이다.

 

만약 JWT 토큰 값이 일치하지 않거나 올바르지 않다면,

TokenProvidervalidateToken() 메서드에서 오류 메시지를 출력해줄 것이다.

 @GetMapping("/my-info")
    @PreAuthorize("hasAnyRole('USER','ADMIN')")
    public CustomerDto getCustomerInfo() {
        return customerService.getCustomerInfo();
    }

 

Controller에서 적당히 URL을 만들어준다. 이 /my-info 에 접근하면 어떻게 되는지 확인해보자.

 

Spring 서버를 띄워주고, 테스트를 진행한다.

 

1. 회원가입

 

회원가입 (register)의 경우 가입 성공 시 가입한 내용을 보여주었다.

 

2. 로그인

로그인 시에는 customerName과 password를 요구한다.

가입시 입력한 password를 정확히 입력해준다.

패스워드가 일치하지 않는다면 401을 반환한다.

 

 

3. my-info 페이지 접근

생성된 token 값을 가지고 my-info URL에 접근해보자.

 

{{jwt_tutorial_token}} 은 

로그인 시 생성된 토큰값을 가져온다.

아래 세팅을 통해 적용할 수 있다. (다른 user로 로그인 하여도 그 JWT 토큰이 적용된다)


 

올바른 토큰 값을 가지고 있다면 성공적으로 나의 데이터가 출력된다.

 

토큰 값이 없다면 401, 잘못되었다면 잘못된 이유를 로그로 출력해준다.


정리

BASIC 토큰을 이용하여 사용자의 정보를 받아오는.. 조언은 아직 해결하지 못하였다. 

하지만 이러한 것보다 우선은 로직이 정상적으로 작동을 하고 있다는 것에 의의를 두겠다.

-> JwtAuthenticationFilter에서 Basic을 이용한 로직을 완성하였다.

 

더보기
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private final LoginProvider loginProvider;

    public JwtAuthenticationFilter(LoginProvider loginProvider, LoginSuccessHandler successHandler, LoginFailureHandler failureHandler) {
        super();
        this.loginProvider = loginProvider;
        // 성공 핸들러와 실패 핸들러 설정
        this.setAuthenticationSuccessHandler(successHandler);
        this.setAuthenticationFailureHandler(failureHandler);

        // 로그인 URL 설정
        setFilterProcessesUrl("/api/customer/login");
    }

    @Override //로그인 시도
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        String header = request.getHeader("Authorization");

        if (header != null && header.startsWith("Basic ")) {

            String base64Credentials = header.substring("Basic ".length()).trim();
            byte[] credDecoded = Base64.getDecoder().decode(base64Credentials);
            String credentials = new String(credDecoded, StandardCharsets.UTF_8);

            final String[] values = credentials.split(":", 2);

            if (values.length == 2) {
                String customerName = values[0];
                String password = values[1];
                Authentication authenticationToken = new UsernamePasswordAuthenticationToken(customerName, password);
                return loginProvider.authenticate(authenticationToken);
            }
        }
        throw new AuthenticationServiceException("로그인 실패");

    }
}

base64로 암호화 시켜 요청을 보내면 로그인을 시도할 때 그것을 decode하여 사용자의 요청 값을 알아낸다.

이후의 과정은 보통의 로그인과 똑같다. 

basic header를 사용하더라도 보안에 취약한 점은 있기때문에 (단순히 해독하면 정보가 다 나옴) HTTPS를 추가하는 것을 목표로 해봐야겠다.


문제점

api 테스트를 진행한 결과 JWT 검증이 두번 일어나는 것을 발견했다. 

GET으로 요청한 나의 실제 요청 /api/customer/my-info 에서 한 번,GET /error 에서 한 번, 총 두 번이었다.

 

뭔가 정상적이지 않은 것이 느껴졌고, 내용을 찾아보니 SecurityConfig에서 비슷한 필터체인이 중복으로 설정되었을 가능성이 있다고 한다.

 

아니면 첫번째 검증에서 실패하여 에러 처리로 이동할 때,"GET /error" 여기에서도 동일한 검증을 시도하고 있다는 것이되겠다.

 

이전에 순환참조 오류를 겪은적이 있는데, 아마 코드 내에서 무언가 비슷한 로직이 섞여있는 것 같다.그것을 찾기위해 저번 포스트를 적었던 것인데, 아무래도 SecurityConfig 에서의 로직을 다시한번 살펴봐야 하겠다.

 

Comments