인프런 커뮤니티 질문&답변

유요한님의 프로필 이미지
유요한

작성한 질문수

스프링 시큐리티 OAuth2

OAuth2 + JWT : 소셜 로그인 시 JWT 발급

작성

·

2.3K

·

수정됨

1

제가 일반적은 시큐리티와 JWT 로그인하는 방법과 소셜 로그인 2개를 구현해서 어느 방법으로 하던 accessToken을 발급해주고 accessToken을 통해서 게시판이라던지 해당 유저 인지 판별할 때 accessToken으로 판별하려고 합니다.

질문 1:

해당 수업이 OAuth2가 메인이고 OAuth2와 JWT 토큰과는 별개인 것은 알고 있지만 찾아봐도 못찾겠어서 질문을 남깁니다. accessToken을 발급받으면 클라이언트가 header에 accessToken을 같이 요청을 보내 줄 때 보내주고

(저 같은 경우 TokenDTO와 TokenEntitty를 만들어줘서 DB에 넣어줌 )

DB에 담긴 정보:

  • grantType

  • accessToken

  • refreshToken

  • userEmail

  • nickName

서버(백엔드)에서는 그 accessToken을 받아서 유효성 검사를 통과하면 컨트롤러나 서비스에서 뭔가 해주지 않아도 클라이언트 요청을 실행해줌

// 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로
// UsernamePasswordAuthenticationFiler 이전에 실행된다.
// 이전에 실행된다는 뜻은 JwtAuthenticationFilter를 통과하면
// UsernamePasswordAuthenticationFilter 이후의 필터는 통과한 것으로 본다는 뜻이다.
// 쉽게 말해서, Username + Password를 통한 인증을 Jwt를 통해 수행한다는 것이다.

// JWT 방식은 세션과 다르게 Filter 하나를 추가해야 합니다.
// 이제 사용자가 로그인을 했을 때, Request에 가지고 있는 Token을 해석해주는 로직이 필요합니다.
// 이 역할을 해주는것이 JwtAuthenticationFilter입니다.
// 세부 비즈니스 로직들은 TokenProvider에 적어둡니다. 일종의 service 클래스라고 생각하면 편합니다.
// 1. 사용자의 Request Header에 토큰을 가져옵니다.
// 2. 해당 토큰의 유효성 검사를 실시하고 유효하면
// 3. Authentication 인증 객체를 만들고
// 4. ContextHolder에 저장해줍니다.
// 5. 해당 Filter 과정이 끝나면 이제 시큐리티에 다음 Filter로 이동하게 됩니다.

@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends GenericFilterBean {

    public static final String HEADER_AUTHORIZATION = "Authorization";
    private final JwtProvider jwtProvider;

    // doFilter는 토큰의 인증정보를 SecurityContext에 저장하는 역할 수행

    @Override
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // Request Header에서 JWT 토큰을 추출
        String jwt = resolveToken(httpServletRequest);
        String requestURI = httpServletRequest.getRequestURI();

        if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)){
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            Authentication authentication = jwtProvider.getAuthentication(jwt);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            log.info("Security Context에 '{}' 인증 정보를 저장했습니다., uri : {}",
                    authentication.getName(), requestURI);
        } else {
            log.debug("유효한 JWT 토큰이 없습니다. uri : {}", requestURI);
        }
        chain.doFilter(request, response);
    }


    // Request Header 에서 토큰 정보를 꺼내오기 위한 메소드
    private String resolveToken(HttpServletRequest httpServletRequest) {
        String bearerToken = httpServletRequest.getHeader(HEADER_AUTHORIZATION);

        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        } else {
            return null;
        }
    }
}

여기서 필요하거나 자세히 진행하고 싶다면 DB에서 체크해서

DB에서 accessToken과 비교해서 맞으면

UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userEmail, userPw);

로 아이디와 비밀번호를 기반으로 생성했으니 해당 유저가 맞으므로 요청을 성공적으로 받아준다. 이게 맞는 흐름인가요?

 

질문 2:

먼저, 로그인 시 accessToken과 refreshToken을 발급해주는 것은 구현을 했는데 소셜 로그인에서 JWT를 발급해주는 것이 헷갈리더군요.

 

    // 로그인
    @PostMapping("/login")
    public ResponseEntity<TokenDTO> login(@RequestBody MemberDTO memberDTO) throws Exception {
        try {
            return memberService.login(memberDTO.getUserEmail(), memberDTO.getUserPw());
        } catch (Exception e) {
            return ResponseEntity.badRequest().build();
        }
    }
// 로그인
    public ResponseEntity<TokenDTO>  login(String userEmail, String userPw) throws Exception {
        // Login ID/PW를 기반으로 UsernamePasswordAuthenticationToken 생성
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(userEmail, userPw);

        // 실제 검증(사용자 비밀번호 체크)이 이루어지는 부분
        // authenticateToken을 이용해서 Authentication 객체를 생성하고
        // authentication 메서드가 실행될 때
        // CustomUserDetailsService에서 만든 loadUserbyUsername 메서드가 실행
        Authentication authentication = authenticationManagerBuilder
                .getObject()
                .authenticate(authenticationToken);

        // 해당 객체를 SecurityContextHolder에 저장
        SecurityContextHolder.getContext().setAuthentication(authentication);

        // authentication 객체를 createToken 메소드를 통해서 생성
        // 인증 정보를 기반으로 생성
        TokenDTO tokenDTO = jwtProvider.createToken(authentication);

        HttpHeaders headers = new HttpHeaders();

        // response header에 jwt token을 넣어줌
        headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + tokenDTO);

        MemberEntity member = memberRepository.findByUserEmail(userEmail);
        log.info("member : " + member);

        TokenEntity tokenEntity = TokenEntity.builder()
                .grantType(tokenDTO.getGrantType())
                .accessToken(tokenDTO.getAccessToken())
                .refreshToken(tokenDTO.getRefreshToken())
                .userEmail(tokenDTO.getUserEmail())
                .nickName(member.getNickName())
                .build();

        log.info("token : " + tokenEntity);

        tokenRepository.save(tokenEntity);

        TokenDTO token = TokenDTO.toTokenDTO(tokenEntity);

        return new ResponseEntity<>(token, headers, HttpStatus.OK);
    }

(JWT 설정을 생략...)

  소셜 로그인 같은 경우는 프론트에서 특정 URL로 보내주잖아요

<a href="/oauth2/authorization/google">구글 로그인</a>

위의 방법은 그냥 아이디, 비밀번호를 치면 되는 방법이고 소셜 로그인도 1차 인증을 받고 JWT를 발급해주면 된다고 알고있는데 위에서 구현한 방법으로 사용하기에는 URL이 다르잖아요? 프론트 URL은 고정으로 저 방법을 사용해야 한다고 알고 있는데...

 

그러면 소셜 로그인을 여러개 사용하면 예를들어, 구글, 카카오톡, 네이버 이런식으로 사용하면 컨트롤러에 각각의 URL로 위의 방식처럼 만들어야 JWT 발급해주는 기능을 구현할 수 있는건가요?

 

답변 1

1

정수원님의 프로필 이미지
정수원
지식공유자

제가 질문에 대한 정확한 이해를 하고 있는지 잘 모르겠지만 일단 답변을 드리자면 이렇습니다

1번과 2번 질문이 서로 연관이 있어 같이 답변드리도록 하겠습니다

먼저 1번 질문이 소셜로그인으로부터 access token 을 발급받고 나서 이 토큰을 가지고 사용자의 인증여부를 계속 판단하신다는 의미가 맞으실까요?

만약 맞다면 이것은 스프링 시큐리티가 기본적으로 해 주고는 있습니다

다만 시큐리티는 access token 을 발급받고 나서 다시 사용자의 정보를 가지고 와서 인증처리를 한 다음에 시큐리티 컨텍스에 인증객체를 저장하고 있습니다 그리고 세션에 담아버립니다 그리고 이후의 사용자 요청은 세션 기반으로 인증 여부를 판단합니다

즉 클라이언트로부터 access token 을 전달받아 인증여부를 계속 체크하는 방식은 아닙니다

만약 access token 을 사용해서 인증처리를 하고자 한다면 access token 이 만료되었을 때 소셜로그인 기관(네이버,구글 등) 에게 다시 토큰요청을 하는 기능을 별도로 구현해야 합니다

그리고 가장 큰 문제는 소셜기관으로부터 받은 access token 은 소셜기관에 속한 사용자 리소스에 접근할 수 있는 권한을 획득한 것이지 클라이언트의 백엔드에서 access token 자체의 유효성을 검사하여 사용자의 인증여부를 판단하는 근거로 사용해서는 안됩니다

그리고 access token 의 유효성 검사는 어떤 기준으로 할까요? 이 토큰의 유효성 검사는 토큰을 발급한 소셜기관에 요청을 보내서 하거나 소셜기관으로부터 토큰을 생성할 때 사용한 키의 정보를 얻어서 자체적으로 해야 하는데 이마저도 여러가지 보안상 제약이 따르거나 소셜기관마다 지원되는 정책도 다를 수 있어서 간단한 문제는 아닙니다

여기서 2번 질문과 연계해서 말씀드리자면 1번 질문에서 access token 을 발급하는 게 소셜기관이 아니라 자체적으로 발급한 경우라면 2번에서처럼 로그인 성공 후 토큰을 발급하고 1번에서처럼 서버에서 토큰의 유효성을 검증하는 식으로 하시면 됩니다

그렇다면 이건 스프링 시큐리티 OAuth2 인증과는 무관합니다

일반적으로 로그인처리는 시큐리티 OAuth2 Client 를 사용해서 구현하고 access token 은 자체 서버에서 발급한 jwt 를 사용해서 이후의 클라이언트 요청에 대해 인증여부를 판단하는 형태로 구현하기도 합니다

맨 마지막 질문의 답변은 이렇습니다

소셜로그인 기능을 사용한다는 것은 소셜기관으로부터 토큰을 획득한 다음 사용자의 정보를 가지고 와서 인증처리를 한다는 의미입니다

인증처리 이후에 사용자의 요청에 대해 어떻게 인증여부를 식별할 것인지에 대해서는 개발자의 몫입니다

세션을 사용하든, jwt 를 사용하든 모두 가능합니다

구글이나 네이버 등의 소셜로그인은 사용자의 최초 인증처리를 위한 하나의 공통된 과정이고 이후의 진행은 어떻게 사용자의 인증상태를 계속 유지할 수 있을 것인지 그 방법을 선택하는 문제입니다

그래서 구글이나 네이버 카카오 소셜로그인을 여러개 두더라도 인증처리는 시큐리티에 의해 동일하게 실행되므로 인증 이후의 흐름은 하나의 컨트롤러에서 구현해도 될 것 같습니다

유요한님의 프로필 이미지
유요한
질문자

access token은 소셜 로그인이 성공하면 서버에서 access token을 따로 발급해주는 것을 말하는 거였습니다. 소셜 기관에서 발급해주는 access token을 인증용으로 사용하면 안좋다고 들어서 성공시 jwt 발급해주는 로직을 따라 일반 로그인하면 jwt를 발급해주는 것처럼 소셜 로그인이 성공하면 똑같이 jwt를 발급해주는 것을 말하는거였습니다!

정수원님의 프로필 이미지
정수원
지식공유자

소셜로그인으로 로그인 한 후 성공하면 자체 서버에서 jwt 토큰을 발행하고 이를 클라이언트에게 전달해서 요청 시 토큰 유효성 검증을 하고 결과에 따라 리소스 접근 여부를 판단하는 식으로 구현하면 될 것 같습니다.

유요한님의 프로필 이미지
유요한

작성한 질문수

질문하기