WEB/Security

OAuth 2.0 Client - Social Login (Google, Naver, KaKao) + FormLogin

Tony Lim 2022. 12. 19. 15:33
728x90

SocalLogin 연동 구현

public interface ProviderUser {

    public String getId();
    public String getUsername();
    public String getPassword();
    public String getEmail();
    public String getProvider();
    public List<? extends GrantedAuthority> getAuthorities();
    public Map<String, Object> getAttributes();
}
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)attributes.get("email");
    }

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

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

각 provider마다 userinfo endpoint에서 들고오는 OAuth2User 정보가 다르기 때문에 공통적인 부분을 정의하고 세세한것으로 나뉘도록 한다.

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("email");
    }
}

 

https://developers.naver.com/docs/login/devguide/devguide.md#3-4-5-%EC%A0%91%EA%B7%BC-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%9D%B4%EC%9A%A9%ED%95%98%EC%97%AC-%ED%94%84%EB%A1%9C%ED%95%84-api-%ED%98%B8%EC%B6%9C%ED%95%98%EA%B8%B0

 

네이버 로그인 개발가이드 - LOGIN

네이버 로그인 개발가이드 1. 개요 4,200만 네이버 회원을 여러분의 사용자로! 네이버 회원이라면, 여러분의 사이트를 간편하게 이용할 수 있습니다. 전 국민 모두가 가지고 있는 네이버 아이디

developers.naver.com

여기에 나와있듯이 id 나 email 이 response/id 이런식으로 되있는것을 알 수 있다. response를 key로 값을 가져온다음에 원하는 값을 추가로 가져와야한다.

    @Bean
    SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests((requests) -> requests
                .antMatchers("/api/user")
                .access("hasAnyRole('SCOPE_profile','SCOPE_email')")
//                .access("hasAuthority('SCOPE_profile')")
                .antMatchers("/api/oidc")
                .access("hasRole('SCOPE_openid')")
                //.access("hasAuthority('SCOPE_openid')")
                .antMatchers("/")
                .permitAll()
                .anyRequest().authenticated());
        http.oauth2Login(oauth2 -> oauth2.userInfoEndpoint(
                userInfoEndpointConfig -> userInfoEndpointConfig
                        .userService(customOAuth2UserService)
                        .oidcUserService(customOidcUserService)));
        http.logout().logoutSuccessUrl("/");
        return http.build();
   }

기본적으로 제공되는것 말고 customOAuth2UserService를 등록을 해줘야한다. open id service도 마찬가지이다. 

@Service
public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {

        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;
    }
}

oAuth2UserService#loadUser를 통해서 user endpoint를 접속하여 OAuth2User를 가져온후에 각 provider에 맞게 super.providerUser를 통해 다시 상세한 providerUser로 변경한다.

이후 register를 통해 db에 등록하게 된다.

@Service
@Getter
public abstract class AbstractOAuth2UserService {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    public void register(ProviderUser providerUser, OAuth2UserRequest userRequest){
        User user = userRepository.findByUsername(providerUser.getUsername());

        if(user == null){
            ClientRegistration clientRegistration = userRequest.getClientRegistration();
            userService.register(clientRegistration.getRegistrationId(),providerUser);
        }else{
            System.out.println("userRequest = " + userRequest);
        }
    }

    public ProviderUser providerUser(ClientRegistration clientRegistration, OAuth2User oAuth2User){

        String registrationId = clientRegistration.getRegistrationId();
        if(registrationId.equals("keycloak")){
            return new KeycloakUser(oAuth2User,clientRegistration);

        }else if(registrationId.equals("google")){
            return new GoogleUser(oAuth2User,clientRegistration);
        }
        else if(registrationId.equals("naver")){
            return new NaverUser(oAuth2User,clientRegistration);
        }
        return null;
    }
}
@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    public void register(String registrationId, ProviderUser providerUser) {

        User user = User.builder().registrationId(registrationId)
                .id(providerUser.getId())
                .username(providerUser.getUsername())
                .password(providerUser.getPassword())
                .authorities(providerUser.getAuthorities())
                .provider(providerUser.getProvider())
                .email(providerUser.getEmail())
                .build();

        userRepository.register(user);
    }
}

repo에 저장하게 되는데 실제 db가 아닌 Map에 저장한다. User는 Entity역할을 한다.

 

google의 경우에는 open id를 지원함으로 application.yaml 에서 scope에 openid를 설정을 해놓으면 OidcAuthorizationCodeAuthenticationProvider 에서 인증처리를 하게 된다. 

@GetMapping("/")
public String index(Model model, Authentication authentication,  @AuthenticationPrincipal OAuth2User oAuth2User){
    OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken)authentication;
    if(authenticationToken != null){
        Map<String, Object> attributes = oAuth2User.getAttributes();
        String userName = (String)attributes.get("name");
        if(authenticationToken.getAuthorizedClientRegistrationId().equals("naver")){
            Map<String, Object> response = (Map)attributes.get("response");
            userName = (String)response.get("name");
        }
        model.addAttribute("user", userName);
    }
    return "index";
}

네이버의 경우에 response로 한번 꺼낸후에 name, email, id 등에 접근이 가능하다.
또한 open id를 지원하지 않기에 OAuth2AuthorizationCodeAuthenticationProvider에서 해결한다.


Form 인증 + OAuth2.0 Soical Login

@Component
public final class DelegatingProviderUserConverter implements ProviderUserConverter<ProviderUserRequest,ProviderUser> {
    private final List<ProviderUserConverter<ProviderUserRequest, ProviderUser>> converters;

    public DelegatingProviderUserConverter() {

        List<ProviderUserConverter<ProviderUserRequest,ProviderUser>> providerUserConverters = Arrays.asList(
                new UserDetailsProviderUserConverter(),
                new OAuth2GoogleProviderUserConverter(),
                new OAuth2NaverProviderUserConverter(),
                new OAuth2KakaoProviderUserConverter(),
                new OAuth2KakaoOidcProviderUserConverter());

        this.converters = Collections.unmodifiableList(new LinkedList<>(providerUserConverters));
    }

    @Nullable
    @Override
    public ProviderUser convert(ProviderUserRequest providerUserRequest) {
        Assert.notNull(providerUserRequest, "providerUserRequest cannot be null");

        for (ProviderUserConverter<ProviderUserRequest,ProviderUser> converter : this.converters) {
            ProviderUser providerUser = converter.convert(providerUserRequest);
            if (providerUser != null) {
                return providerUser;
            }
        }
        return null;
    }
}

각 Provider에 맞게 User를 Converting하는 레이어를 만든것이다. converters를 돌면서 registrationid가 매칭되는 경우만 변환하고 매칭이 되지않으면 null을 return한다.

public record ProviderUserRequest (ClientRegistration clientRegistration, OAuth2User oAuth2User, User user){
    public ProviderUserRequest(ClientRegistration clientRegistration, OAuth2User oAuth2User){
        this(clientRegistration,oAuth2User,null);
    };

    public ProviderUserRequest(User user){
        this(null,null,user);
    };
}

인자로 ProviderUserRequest record가 주어지는데 ClientRegistration , OAuth2User , User를 필드로가지고 있는 불변클래스이다. DTO 개념으로 활용이된다.

 

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService extends AbstractOAuth2UserService implements UserDetailsService {
    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);

        if(user == null){
            user = User.builder()
                    .id("1")
                    .username("onjsdnjs")
                    .password("{noop}1234")
                    .authorities(AuthorityUtils.createAuthorityList("ROLE_USER"))
                    .email("onjsdnjs@gmail.com")
                    .build();
        }

        ProviderUserRequest providerUserRequest = new ProviderUserRequest(user);
        ProviderUser providerUser = providerUser(providerUserRequest);

        selfCertificate(providerUser);

        return new PrincipalUser(providerUser);
    }
}

form login은 UserDetailService#loadUserByUserName 메소드를 통해 repository에서 username으로 User를 찾아서 UserDetails으로 return 해야한다.

하지만 providerUser메소드에서 Provider별로 (google, naver , kakao) OAuth2User가 나오게 됨으로 UserDetail과 OAuth2User를 통일 해줘야한다.

public record PrincipalUser(ProviderUser providerUser) implements UserDetails, OidcUser, OAuth2User {

    @Override
    public String getName() {
        return providerUser.getUsername();
    }

    @Override
    public Map<String, Object> getAttributes() {
        return providerUser.getAttributes();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return providerUser.getAuthorities();
    }

    @Override
    public String getPassword() {
        return providerUser.getPassword();
    }

    @Override
    public String getUsername() {
        return providerUser.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Map<String, Object> getClaims() {
        return null;
    }

    @Override
    public OidcUserInfo getUserInfo() {
        return null;
    }

    @Override
    public OidcIdToken getIdToken() {
        return null;
    }
}

principleUser는 record 로 dto역할을 하게 되고 OAuth2User, UserDetails , OidcUser 3개의 타입을 동시에 처리할 수 있게된다.

  

 

 

728x90

'WEB > Security' 카테고리의 다른 글

OAuth 2.0 Resource Server API - jwt()  (0) 2022.12.22
OAuth 2.0 Resource Server  (0) 2022.12.19
OAuth 2.0 Client - oauth2Client()  (1) 2022.12.14
OAuth2.0 Client - oauth2Login() + API 커스텀 구현  (0) 2022.11.23
OAuth 2.0 Client - oauth2Login()  (1) 2022.11.22