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

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

작성한 질문수

스프링 시큐리티 OAuth2

OAuth 2.0 Social Login 연동 구현 (4)

소셜로그인 정보가져오기

작성

·

837

0

강사님 수업을 보면 정보를 다 가지고 오는 것을 볼 수 있는데

 

package com.example.oauth.config.OAuth2;

import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.List;
import java.util.Map;

public interface ProviderUser {
    String getId();
    String getUserName();
    String getPassword();
    String getEmail();
    String getProvider();
    List<SimpleGrantedAuthority> getAuthorities();
    Map<String, Object> getAttributes();
}
package com.example.oauth.config.OAuth2;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;

// 여기는 공통적인 부분만 추상화해서 모아놓은 곳이다.
// Google, Naver에서 다른 부분들은 따로 만들어줄 것이다.
public abstract class OAuth2ProviderUser implements ProviderUser{

    private Map<String, Object> attributes;
    private OAuth2User oAuth2User;
    private ClientRegistration clientRegistration;

    public OAuth2ProviderUser(Map<String, Object> attributes,
                              OAuth2User oAuth2User,
                              ClientRegistration clientRegistration) {
        this.attributes = attributes;
        this.oAuth2User = oAuth2User;
        this.clientRegistration = clientRegistration;
    }

    @Override
    public String getPassword() {
        return UUID.randomUUID().toString();
    }

    @Override
    public String getEmail() {
        return (String) getAttributes().get("email");
    }

    @Override
    public List<SimpleGrantedAuthority> getAuthorities() {
        return oAuth2User.getAuthorities().stream()
                .map(authority -> new SimpleGrantedAuthority(authority.getAuthority()))
                .collect(Collectors.toList());
    }

    @Override
    public String getProvider() {
        return clientRegistration.getRegistrationId();
    }

    @Override
    public Map<String, Object> getAttributes() {
        return attributes;
    }
}
package com.example.oauth.config.OAuth2;

import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.Map;

public class NaverUser extends OAuth2ProviderUser{

    public NaverUser(OAuth2User oAuth2User, ClientRegistration clientRegistration) {
        super((
                Map<String, Object>) oAuth2User.getAttributes().get("response"),
                oAuth2User,
                clientRegistration);
    }

    @Override
    public String getId() {
        return (String) getAttributes().get("id");
    }

    @Override
    public String getUserName() {
        return (String) getAttributes().get("name");
    }
}
package com.example.oauth.config.OAuth2;

import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.user.OAuth2User;

public class GoogleUser extends OAuth2ProviderUser{

    public GoogleUser(OAuth2User oAuth2User, ClientRegistration clientRegistration) {
        // 사용자의 정보는 oAuth2User.getAttributes() 여기에 담겨져 있다.
        // 여기는 클레임 형식 즉, Map 형식으로 되어 있다.
        super(oAuth2User.getAttributes(), oAuth2User, clientRegistration);
    }

    @Override
    // 식별자의 역할
    public String getId() {
        return (String) getAttributes().get("sub");
    }

    @Override
    // 유저 id
    public String getUserName() {
        return (String) getAttributes().get("name");
    }
}
package com.example.oauth.config.OAuth2;

import com.example.oauth.repository.UserRepository;
import com.example.oauth.service.UserService;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
// OAuth2UserService : Spring Security에서 OAuth 2.0을 사용하여 인증한 사용자 정보를 가져오기 위한 인터페이스입니다.
// 이 인터페이스를 구현하여 사용자 정보를 가져오는 방법을 정의하고,
// OAuth 2.0 프로바이더(예: Google, Facebook, GitHub 등)로부터 인증된 사용자 정보를 추출할 수 있습니다.

// OAuth2UserRequest : 이 객체는 OAuth 2.0 클라이언트 정보, 권한 부여 코드, 액세스 토큰 등을 포함합니다.
// 이 정보를 사용하여 사용자 정보를 요청하고 처리할 수 있습니다.

// OAuth2User : OAuth 2.0 프로바이더(인증 제공자)로부터 가져온 인증된 사용자 정보를 나타냅니다.
// 이 정보는 사용자의 프로필 데이터, 권한(스코프), 사용자 ID 등을 포함할 수 있습니다.
public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {


    public CustomOAuth2UserService(UserRepository userRepository, UserService userService) {
        super(userRepository, userService);
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // ClientRegistration은 Spring Security에서 OAuth 2.0 또는 OpenID Connect (OIDC) 클라이언트
        // 애플리케이션의 등록 정보를 나타내는 클래스입니다. 클라이언트 애플리케이션의 설정 및 속성을 포함합니다.

        // userRequest.getClientRegistration()은 인증 및 인가된 사용자 정보를 가져오는
        // Spring Security에서 제공하는 메서드입니다.
        ClientRegistration clientRegistration = userRequest.getClientRegistration();
        OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService =
                new DefaultOAuth2UserService();
        OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);

        // 여기에는 구글, 네이버 정보가 담겨져 있다.
        ProviderUser providerUser = super.providerUser(clientRegistration, oAuth2User);

        // 회원가입
        super.register(providerUser, userRequest);

        return oAuth2User;
    }
}
package com.example.oauth.config.OAuth2;

import com.example.oauth.entity.UserEntity;
import com.example.oauth.repository.UserRepository;
import com.example.oauth.service.UserService;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
@Getter
@RequiredArgsConstructor
@Log4j2
// 사용자 등록과 어떤 사용자인지 알아볼 수 있는 곳
public abstract class AbstractOAuth2UserService {

    private final UserRepository userRepository;
    private final UserService userService;

    protected void register(ProviderUser providerUser, OAuth2UserRequest userRequest) {
        UserEntity findUser = userRepository.findByUserName(providerUser.getUserName());

        if(findUser == null) {
            String registrationId = userRequest.getClientRegistration().getRegistrationId();
            userService.register(registrationId, providerUser);
        } else {
            log.info("user : " + findUser);
        }
    }

    protected ProviderUser providerUser(ClientRegistration clientRegistration, OAuth2User oAuth2User) {
        String registrationId = clientRegistration.getRegistrationId();

        if(registrationId.equals("google")) {
            return new GoogleUser(oAuth2User, clientRegistration);
        } else if(registrationId.equals("naver")) {
            return new NaverUser(oAuth2User, clientRegistration);
        } else {
            return null;
        }
    }
}

이거를 REST 방식으로 하려고

컨트롤러

    // 소셜 로그인
    @GetMapping("/api/v1/user/success")
    public ResponseEntity<?> socialLogin(Authentication authentication,
                                         @AuthenticationPrincipal OAuth2User oAuth2User) {
        OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;

        if (oAuth2AuthenticationToken != null) {
            ResponseEntity<?> oAuth2Login =
                    memberService.login(oAuth2AuthenticationToken, oAuth2User);
            return ResponseEntity.ok().body(oAuth2Login);
        }  else {
            return ResponseEntity.badRequest().build();
        }
    }

서비스

  // 소셜 로그인
    public ResponseEntity<?> login(OAuth2AuthenticationToken oAuth2AuthenticationToken,
                                   OAuth2User oAuth2User) {
        Map<String, Object> attributes = oAuth2User.getAttributes();
        log.info("attributes : " + attributes);

        String authorizedClientRegistrationId =
                oAuth2AuthenticationToken.getAuthorizedClientRegistrationId();

        String email = null;
        TokenDTO jwt = null;
        if (authorizedClientRegistrationId.equals("google")) {
            email = (String) attributes.get("email");
            log.info("email : " + email);

            jwt = createJWT(email);
            log.info("jwt : " + jwt);
        } else if (authorizedClientRegistrationId.equals("naver")) {
            Map<String, Object> response = (Map) attributes.get("response");

            email = (String) response.get("email");
            jwt = createJWT(email);
            log.info("jwt : " + jwt);
        } else {
            log.info("아무런 정보를 받지 못했습니다.");
        }
        return ResponseEntity.ok().body(jwt);
    }

    private TokenDTO createJWT(String email) {
        MemberEntity findUser = memberRepositroy.findByEmail(email);
        List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(findUser);
        TokenDTO tokenForOAuth2 = jwtProvider.createTokenForOAuth2(email, authoritiesForUser);
        TokenEntity findToken = tokenRepository.findByMemberEmail(tokenForOAuth2.getMemberEmail());

        if (findToken == null) {
            TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenForOAuth2);
            tokenRepository.save(tokenEntity);
            log.info("token : " + tokenForOAuth2);
        } else {
            tokenForOAuth2 = TokenDTO.builder()
                    .id(findToken.getId())
                    .grantType(tokenForOAuth2.getGrantType())
                    .accessToken(tokenForOAuth2.getAccessToken())
                    .refreshToken(tokenForOAuth2.getRefreshToken())
                    .memberEmail(tokenForOAuth2.getMemberEmail())
                    .build();

            TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenForOAuth2);
            tokenRepository.save(tokenEntity);
        }
        return tokenForOAuth2;
    }

이렇게 구성을 했는데 JWT는 제외하고 public ResponseEntity<?> socialLogin(Authentication authentication,
@AuthenticationPrincipal OAuth2User oAuth2User) { 이거로 정보를 가지고 오려고 했는데 정보를 안가지고 와지고 계속 JWT 검증 로직에 걸려서 잘못된 JWT라고 뜹니다 ㅠㅠ

 

package com.example.study01.config.jwt;

import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Log4j2
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    public static final String HEADER_AUTHORIZATION = "Authorization";
    private final JwtProvider jwtProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        // request header에서 JWT를 추출
        // 요청 헤더에서 JWT 토큰을 추출하는 역할
        String jwt = resovleToken(httpServletRequest);
        log.info("jwt in JwtAuthenticationFilter : " + jwt);

        // 어떤 경로로 요청을 했는지 보여줌
        String requestURI = httpServletRequest.getRequestURI();
        log.info("uri JwtAuthenticationFilter : " + requestURI);

        if(StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            Authentication authentication = jwtProvider.getAuthentication(jwt);
            log.info("authentication in JwtAuthenticationFilter : " + authentication);

            // Spring Security의 SecurityContextHolder를 사용하여 현재 인증 정보를 설정합니다.
            // 이를 통해 현재 사용자가 인증된 상태로 처리됩니다.
            // 위에서 jwtProvider.getAuthentication(jwt)가 반환이 UsernamePasswordAuthenticationToken로
            // SecurityContext에 저장이 되는데 SecurityContextHolder.getContext().setAuthentication(authentication);
            // 처리를 하는 이유는 다음과 같다.
            /*
             *   1.  인증 정보 검증: JWT 토큰이나 다른 인증 정보를 사용하여 사용자를 식별하고
             *       권한을 확인하기 위해서는 토큰을 해독하여 사용자 정보와 권한 정보를 추출해야 합니다.
             *       이 역할은 jwtProvider.getAuthentication(jwt)에서 수행됩니다.
             *       이 메서드는 JWT 토큰을 분석하여 사용자 정보와 권한 정보를 추출하고, 해당 정보로 인증 객체를 생성합니다.
             *
             *   2.  인증 정보 저장:
             *       검증된 인증 객체를 SecurityContextHolder.getContext().setAuthentication(authentication);를
             *       사용하여 SecurityContext에 저장하는 이유는, Spring Security에서 현재 사용자의 인증 정보를
             *       전역적으로 사용할 수 있도록 하기 위함입니다. 이렇게 하면 다른 부분에서도 현재 사용자의 인증 정보를 사용할 수 있게 되며,
             *       Spring Security가 제공하는 @AuthenticationPrincipal 어노테이션을 통해 현재 사용자 정보를 편리하게 가져올 수 있습니다.
             * */
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            log.error("유효한 JWT가 없습니다. : " + requestURI);
        }
        filterChain.doFilter(request, response);
    }

    // 토큰을 가져오기 위한 메소드
    // Authorization로 정의된 헤더 이름을 사용하여 토큰을 찾고
    // 토큰이 "Bearer "로 시작하거나 "Bearer "로 안온 것도 토큰 반환
    private String resovleToken(HttpServletRequest httpServletRequest) {
        String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION);

        // 토큰이 포함하거나 Bearer 로 시작하면 true
        if(StringUtils.hasText(token) && token.startsWith("Bearer ")) {
            return token.substring(7);
        } else if(StringUtils.hasText(token)) {
            return token;
        } else {
            return null;
        }
    }
}

이럴 경우는 어떻게 해야하나요... 컨트롤러에서 정보를 가지고 와서 그 정보로 JWT를 만들어 반환하려고 하는데 정보를 가지고 오는게 계속 실패를 해서...

답변 2

0

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

소스를 다운받아 실행해 보면 여러 군데서 에러가 납니다.

Could not resolve placeholder 'jwt.secret_key' in value "${jwt.secret_key}"
그리고 위 오류를 해결하더라도
Consider defining a bean of type 'org.springframework.security.oauth2.client.registration.ClientRegistrationRepository' in your configuration.

와 같은 에러가 납니다.
일단 소스 실행에 오류가 나지 않도록 재 업로드 부탁드립니다.

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

어라? 저는 그런 오류가 안나는데 저는 단순히 JWT 검증에서 걸리고 말씀하신 오류가 발생하지 않습니다.

일단 소셜 로그인을 할 경우 image이런식으로 정보를 가지고 오고

image이렇게 DB에도 들어가지만

 헤더로 소셜 로그인 토큰을 받으면 image이렇게 뜨고 토큰을 헤더에 안받으면 토큰이 없다고 정보를 가지고 올 수 없습니다...

https://github.com/YuYoHan/boot_study/tree/main

다시 올려봅니다. 혹시 oauth.yml 과 jwt.yml 이런 설정 파일이 없어서 그러는 걸까요? 그런거는 git 안올라가게 조심하라고 해서 안올라가게 막아 놨는데

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

그럼

oauth.yml 과 jwt.yml 의 내용을 알려 주시기 바랍니다

장상적인 실행이 되어야 테스트가 가능합니다

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

올려주신 oauth.yml 과 jwt.yml 은 이제 삭제해도 됩니다

0

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

가능하시면 전체소스를 공유 부탁드립니다

직접 실행해보면서 확인해야 할 것 같습니다

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

https://github.com/YuYoHan/boot_study

계속 안되서 구글 찾아보면서 OAuth2UserService좀 수정했는데 계속 안되네요 ㅠㅠ

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

작성한 질문수

질문하기