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

유요한님의 프로필 이미지

작성한 질문수

스프링부트 시큐리티 & JWT 강의

소셜 로그인 후 JWT 발급

작성

·

598

0

아무리 해도 해결이 안되네요...

package com.example.project1.config.oauth2;

import com.example.project1.config.auth.PrincipalDetails;
import com.example.project1.config.oauth2.provider.GoogleUserInfo;
import com.example.project1.config.oauth2.provider.NaverUserInfo;
import com.example.project1.config.oauth2.provider.OAuth2UserInfo;
import com.example.project1.domain.member.UserType;
import com.example.project1.entity.member.MemberEntity;
import com.example.project1.repository.member.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    private final MemberRepository memberRepository;


    // 구글로부터 받은 userReuest 데이터에 대한 후처리되는 함수
    @Override
    public PrincipalDetails loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // registrationId로 어떤 OAuth로 로그인 했는지 확인가능
        log.info("clientRegistration in PrincipalOauth2UserService : " + userRequest.getClientRegistration() );
        log.info("accessToken in PrincipalOauth2UserService : " + userRequest.getAccessToken().getTokenValue() );

        OAuth2User oAuth2User = super.loadUser(userRequest);

        // 구글 로그인 버튼 클릭 →구글 로그인 창 → 로그인 완료 → code 를 리턴(OAuth-Client 라이브러리) → AccessToken 요청
        // userRequest 정보 → 회원 프로필 받아야함(loadUser 함수 호출) → 구글로부터 회원 프로필을 받아준다.
        log.info("getAttributes in PrincipalOauth2UserService : " + oAuth2User.getAttributes());

        // 회원가입을 강제로 진행
        OAuth2UserInfo oAuth2UserInfo = null;

        if(userRequest.getClientRegistration().getRegistrationId().equals("google")) {
            log.info("구글 로그인 요청");
            oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
        } else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) {
            log.info("네이버 로그인 요청");
            // 네이버는 response를 json으로 리턴을 해주는데 아래의 코드가 받아오는 코드다.
            // response={id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영}
            // 위의 정보를 NaverUserInfo에 넘기면
            oAuth2UserInfo = new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"));
        } else {
            log.info("구글과 네이버만 지원합니다.");
        }

        // 사용자가 로그인한 소셜 서비스(provider)를 가져옵니다.
        // 예를 들어, "google" 또는 "naver"와 같은 값을 가질 수 있습니다.
        String provider = oAuth2UserInfo.getProvider();
        // 사용자의 소셜 서비스(provider)에서 발급된 고유한 식별자를 가져옵니다.
        // 이 값은 해당 소셜 서비스에서 유니크한 사용자를 식별하는 용도로 사용됩니다.
        String providerId = oAuth2UserInfo.getProviderId();
        // 예) google_109742856182916427686
        String userName = provider + "_" + providerId;
        String password = bCryptPasswordEncoder.encode("get");
        // 사용자의 이메일 주소를 가져옵니다. 소셜 서비스에서 제공하는 이메일 정보를 사용합니다.
        String email = oAuth2UserInfo.getEmail();
        // 사용자의 권한 정보를 설정합니다. UserType.
        // 여기서는 소셜로그인으로 가입하면 무조건 User로 권한을 주는 방식으로 했습니다.
        UserType role = UserType.USER;

        // 이메일 주소를 사용하여 이미 해당 이메일로 가입된 사용자가 있는지 데이터베이스에서 조회합니다.
        MemberEntity member = memberRepository.findByUserEmail(email);

        if(member == null) {
            log.info("OAuth 로그인이 최초입니다.");
            log.info("↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓");
            log.info("OAuth 자동 회원가입을 진행합니다.");


            member = MemberEntity.builder()
                    .userName(userName)
                    .userPw(password)
                    .userEmail(email)
                    .userType(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();

            log.info("userEmail in PrincipalOauth2UserService : " + member.getUserEmail());
            log.info("userName in PrincipalOauth2UserService : " + member.getUserName());
            log.info("userPw in PrincipalOauth2UserService : " + member.getUserPw());
            log.info("userType in PrincipalOauth2UserService : " + member.getUserType());
            log.info("provider in PrincipalOauth2UserService : " + member.getProvider());
            log.info("providerId in PrincipalOauth2UserService : " + member.getProviderId());

            memberRepository.save(member);
        } else {
            log.info("로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다.");
            MemberEntity findUser = memberRepository.findByUserEmail(email);
            log.info("findUser in PrincipalOauth2UserService : " + findUser);
        }

        OAuth2User oAuth2User1 = super.loadUser(userRequest);
        log.info("getAttributes in PrincipalOauth2UserService : " + oAuth2User1.getAttributes());
        // attributes가 있는 생성자를 사용하여 PrincipalDetails 객체 생성
        // 소셜 로그인인 경우에는 attributes도 함께 가지고 있는 PrincipalDetails 객체를 생성하게 됩니다.
        PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes());
        log.info("principalDetails in PrincipalOauth2UserService : " + principalDetails);
        return principalDetails;
    }
}
package com.example.project1.config.oauth2.provider;

public interface OAuth2UserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}
package com.example.project1.config.oauth2.provider;

import java.util.Map;

public class GoogleUserInfo implements OAuth2UserInfo{

    // getAttributes()를 받음
    private Map<String, Object> attributes;

    public GoogleUserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProviderId() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getProvider() {
        return "google";
    }

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

    @Override
    public String getName() {
        return (String) attributes.get("name");
    }
}
package com.example.project1.config.oauth2.provider;

import java.util.Map;

public class NaverUserInfo  implements OAuth2UserInfo{
    // oauth2User.getAttributes()를 받음
    private Map<String,Object> attributes;

    // PrincipalOauth2UserService에서 new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"))로
    // Oauth2 네이버 로그인 정보를 받아온다.
    // → {id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영}
    public NaverUserInfo(Map<String, Object> attributes) {
        this.attributes = attributes;
    }

    @Override
    public String getProviderId() {
        return (String)attributes.get("id");
    }

    @Override
    public String getProvider() {
        return "naver";
    }

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

    @Override
    public String getName() {
        return (String)attributes.get("name");
    }
}
package com.example.project1.config.auth;

import com.example.project1.entity.member.MemberEntity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

@Setter
@Getter
@ToString
@NoArgsConstructor
@Slf4j
public class PrincipalDetails implements UserDetails, OAuth2User {

    // 일반 로그인 정보를 저장하기 위한 필드
    private MemberEntity member;
    // OAuth2 로그인 정보를 저장하기 위한 필드
    // attributes는 Spring Security에서 OAuth2 인증을 수행한 후에 사용자에 대한 추가 정보를 저장하는 데 사용되는 맵(Map)입니다.
    // OAuth2 인증은 사용자 인증 후에 액세스 토큰(Access Token)을 발급받게 되는데,
    // 이 토큰을 사용하여 OAuth2 서비스(provider)로부터 사용자의 프로필 정보를 요청할 수 있습니다.
    // 예를 들어, 소셜 로그인을 사용한 경우에는 attributes에는 사용자의 소셜 서비스(provider)에서 제공하는 프로필 정보가 담겨 있습니다.
    // 소셜 로그인 서비스(provider)마다 제공하는 프로필 정보가 다를 수 있습니다.
    // 일반적으로 attributes에는 사용자의 아이디(ID), 이름, 이메일 주소, 프로필 사진 URL 등의 정보가 포함됩니다.
    /*
    *   구글의 경우
    *   {
            "sub": "100882758450498962866", // 구글에서 발급하는 고유 사용자 ID
            "name": "John Doe", // 사용자 이름
            "given_name": "John", // 이름(이름 부분)
            "family_name": "Doe", // 성(성(성) 부분)
            "picture": "https://lh3.googleusercontent.com/a/AAcHTtdzQomNwZCruCcM0Eurcf8hAgBHcgwvbXEBQdw3olPkSg=s96-c", // 프로필 사진 URL
            "email": "johndoe@example.com", // 이메일 주소
            "email_verified": true, // 이메일 주소 인증 여부
            "locale": "en" // 지역 설정
        }
    * */
    private Map<String, Object> attributes;


    // 일반 로그인
    public PrincipalDetails(MemberEntity member) {
        this.member = member;
    }


    // OAuth2 로그인
    public PrincipalDetails(MemberEntity member, Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }

    // 해당 유저의 권한을 리턴하는 곳
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new SimpleGrantedAuthority("ROLE_" + member.getUserType().toString()));
        log.info("collection : " + collection);
        return collection;
    }


    // 사용자 패스워드를 반환
    @Override
    public String getPassword() {
        log.info("password : "+ member.getUserPw());
        return member.getUserPw();
    }

    // 사용자 이름 반환
    @Override
    public String getUsername() {
        log.info("id : " + member.getUserEmail());
        return member.getUserEmail();
    }


    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        // 만료되었는지 확인하는 로직
        // true = 만료되지 않음
        return true;
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        // true = 잠금되지 않음
        return true;
    }

    // 패스워드의 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        // 패스워드가 만료되었는지 확인하는 로직
        // true = 만료되지 않음
        return true;
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        // 계정이 사용 가능한지 확인하는 로직
        // true = 사용 가능
        return true;
    }

    @Override
    public Map<String, Object> getAttributes() {
        log.info("attributes : " + attributes);
        return attributes;
    }

    @Override
    // OAuth2 인증에서는 사용되지 않는 메서드이므로 null 반환
    public String getName() {
        return null;
    }
}
   // Oauth2 google로 JWT 발급
    @GetMapping("/success-oauth")
    public ResponseEntity<?> createTokenForGoogle(@AuthenticationPrincipal OAuth2User oAuth2User
            , @RequestBody MemberDTO member) {
        Object email = oAuth2User.getAttribute("email");
        log.info("oAuth2User : " + email);

        if(oAuth2User == null) {
            log.info("받아올 정보가 없습니다 ㅠㅠ");
            return ResponseEntity.status(HttpStatus.NOT_FOUND).body("정보가 없어....");
        } else {
            // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성합니다.
            ResponseEntity<TokenDTO> token = memberService.createToken((String) email, member);
            log.info("token : " + token);

            return ResponseEntity.ok().body(token);
        }
    }
  // 소셜 로그인 성공시 jwt 반환
    // OAuth2User에서 필요한 정보를 추출하여 UserDetails 객체를 생성하는 메서드
    public ResponseEntity<TokenDTO> createToken(String userEmail, MemberDTO member) {
        MemberEntity findUser = memberRepository.findByUserEmail(userEmail);
        log.info("findUser in MemberService : " + findUser);

        findUser = MemberEntity.builder()
                // id를 식별해서 수정
                // 이거 없으면 새로 저장하기 됨
                // findUser꺼를 쓰면 db에 입력된거를 사용하기 때문에
                // 클라이언트에서 userEmail을 전달하더라도 서버에서 기존 값으로 업데이트가 이루어질 것입니다.
                // 이렇게 하면 userEmail을 수정하지 못하게 할 수 있습니다.
                .userId(findUser.getUserId())
                .userEmail(findUser.getUserEmail())
                .userPw(passwordEncoder.encode(findUser.getUserPw()))
                .userName(findUser.getUserName())
                .nickName(member.getNickName())
                .userType(findUser.getUserType())
                .address(AddressEntity.builder()
                        .userAddr(member.getAddressDTO().getUserAddr())
                        .userAddrDetail(member.getAddressDTO().getUserAddrDetail())
                        .userAddrEtc(member.getAddressDTO().getUserAddrEtc())
                        .build())
                .build();

        memberRepository.save(findUser);

        //  권한 정보 추출
        List<GrantedAuthority> authorities = getAuthoritiesForUser(findUser);

        // UserDetails 객체 생성 (사용자의 아이디 정보를 활용)
        // 첫 번째 인자 : username 사용자 아이디
        // 두 번째 인자 : 사용자의 비밀번호
        // 세 번째 인자 : 사용자의 권한 정보를 담은 컬렉션
        UserDetails userDetails = new User(userEmail, null, authorities);
        log.info("userDetails in MemberService : " + userDetails);
        TokenDTO token = jwtProvider.createToken2(userDetails);
        log.info("token in MemberService : " + token);

        return ResponseEntity.ok().body(token);
    }
    // 소셜 로그인 성공시 JWT 발급
    public TokenDTO createToken2(UserDetails userDetails) {
        long now = (new Date()).getTime();
        Date now2 = new Date();

        // userDetails.getAuthorities()는 사용자의 권한(authorities) 정보를 가져오는 메서드입니다.
        // claims.put("roles", userDetails.getAuthorities()) 코드는 사용자의 권한 정보를 클레임에 추가하는 것입니다.
        // 클레임에는 "roles"라는 키로 사용자의 권한 정보가 저장되며, 해당 권한 정보는 JWT의 페이로드 부분에 포함됩니다.
        Claims claims = Jwts.claims().setSubject(userDetails.getUsername());
        claims.put(AUTHORITIES_KEY, userDetails.getAuthorities());

        log.info("claims : " + claims);

        // access token
        Date accessTokenExpire = new Date(now + this.accessTokenTime);
        String accessToken = Jwts.builder()
                .setSubject(userDetails.getUsername())
                .setClaims(claims)
                .setIssuedAt(now2)
                .setExpiration(accessTokenExpire)
                .signWith(key,SignatureAlgorithm.HS256)
                .compact();

        // RefreshToken 생성
        Date refreshTokenExpire = new Date(now + this.refreshTokenTime);
        String refreshToken = Jwts.builder()
                .setIssuedAt(now2)
                .setClaims(claims)
                .setExpiration(refreshTokenExpire)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        TokenDTO tokenDTO = TokenDTO.builder()
                .grantType("Bearer ")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .userEmail(userDetails.getUsername())
                .build();

        log.info("tokenDTO in JwtProvider : " + tokenDTO);
        return tokenDTO;
    }
    http
                // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다.
                .oauth2Login()
                    // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당
                    .userInfoEndpoint()
                        // OAuth2 로그인 성공 시, 후작업을 진행할 서비스
                        .userService(principalOauth2UserService)
                    .and()
                        .defaultSuccessUrl("/success-oauth");

소셜 로그인 후 쇼핑몰 프로젝트라 주소가 필 수라서 JSON을 추가로 받아서 주소를 DB에 저장 후 JWT를 반환해주는 로직을 구성하려고 합니다. 소셜 로그인을 했을 때 PrincipalOauth2UserService여기서 모든 값이 제대로 들어갔고

 PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes());
        log.info("principalDetails in PrincipalOauth2UserService : " + principalDetails);
        return principalDetails;

로그를 찍어본 결과 제대로 값이 로그에 찍혔습니다. 이거를 principalDetails에 보내줬으니 principalDetails 클래스에서도 제대로 받아졌는지 확인해봤습니다. Oaut2User를 상속받아서 오버라이드 한

@Override
    public Map<String, Object> getAttributes() {
        log.info("attributes : " + attributes);
        return attributes;
    }

여기서도 로그에 제대로 값이 나왔습니다.

여기서부터가 문제인데 컨트롤러에서 @AuthenticationPrincipal OAuth2User oAuth2User로 소셜 로그인한 정보를 받아와서 토큰을 만들려고 하는데 null이 뜹니다. 별방법을 다했는데 다른 부분은 다 해결을 했는데 이부분이 해결이 안되네요 ㅠㅠ

 

답변 1

0

최주호님의 프로필 이미지
최주호
지식공유자

제 강의를 따라서 하셨을때는 잘 되셨나요?

지금 코드가 제 강의가 아닌것 같아요!! 그럼 제가 분석을 해야되서요!!

제 강의를 따라서 하셨던 부분은 잘되었는데..본인 프로젝트로 옮길때 잘 안된다는 말씀이신가요?