이번에는 지난 시간에 한 oauth2 로그인에 이어, Database 와 연동을 해보겠다.

이번에 구현 할 것은, 1 계정 - 1소셜 이다

무슨 말이냐 하면, 유저 1 : N 소셜 이 아니라, 소셜 계정이 즉 유저계정이 된다는 것이다.

따라서, User 가 소셜 일 수도있고, 아닐수도 있다는 이야기이다.

먼저, 소셜 타입을 정의하자.

package ziponia.spring.security;

public enum SocialProvider {
    KAKAO,
    FACEBOOK,
    GITHUB
}

다음으로 User 테이블에 SocialProvider 를 추가하자

package ziponia.spring.security;

import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name = "tbl_users")
@Getter
@Setter
public class UserEntity {

    @Id
    @GeneratedValue
    private Integer idx;

    private String username;

    private String password;

    @Enumerated(EnumType.STRING)
    private SocialProvider sns; // new
}

다음으로, 권한 및 인증 추출기를 만들겠다.

프로젝트가 조금 복잡해지니, social 이라는 패키지를 만들고 그 안에 facebook / github / kakao 라는 패키지를 만든다. 그 다음 social 패키지 아래에 BasePrincipalExtractor 라는 추상화 객체를 만들겠다.

쿼리를 중복해서 써야 하는 부분을 제거하기 위함이다.

그럼 최종적으로

BasePrincipalExtractor 라는 추상화 객체가 있고, BasePrincipalExtractor 는 PrincipalExtractor 를 구현한 후

FacebookPrincipalExtractor, GithubPrincipalExtractor, KakaoPrincipalExtractor 클래스들이 BasePrincipalExtractor 를 상속하는 것이다.

public abstract class BasePrincipalExtractor implements PrincipalExtractor {
}

@Component
public class FacebookPrincipalExtractor extends BasePrincipalExtractor {

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

@Component
public class KakaoPrincipalExtractor extends BasePrincipalExtractor {

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

@Component
public class GithubPrincipalExtractor extends BasePrincipalExtractor {

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

그리고, Database 로 접근 할 수 있는, JpaRepository를 하나 만들자.

다시한번 얘기하지만, JPA 는 지금 언급하지 않겠다.

UserRepository.java

public interface UserRepository extends JpaRepository<UserEntity, Integer> {
}

이제 BasePrincipalExtractor 에서 UserRepository 를 주입시켜준 후, Social 계정을 받아 처리 할 수 있는, 쿼리를 만들고,

최종적으로 Principal 을 대신해서 생성 해 주는 부분 까지 생성하자.

public abstract class BasePrincipalExtractor implements PrincipalExtractor {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username",
            "userid", "user_id", "login", "id", "name" };

    @Transactional
    public void saveSocialUser(String id, SocialProvider provider) {
        UserEntity userEntity = userRepository.findByUsernameAndSns(id, provider);
        if (userEntity == null) {
            userEntity = new UserEntity();
            userEntity.setUsername(id);
            userEntity.setSns(provider);
            userEntity.setPassword(passwordEncoder.encode(UUID.randomUUID().toString()));
        }

        userRepository.save(userEntity);
    }

    protected Object createPrincipal(Map<String, Object> map) {
        for (String key : PRINCIPAL_KEYS) {
            if (map.containsKey(key)) {
                return map.get(key);
            }
        }
        return null;
    }
}

이제 각각, Facebook, github, kakao PrincipalExtractor 에서 saveSocialUser 메서드를 불러와 사용하자.

반복코드니, 카카오 예시 하나만 보겠다.

@Component
public class KakaoPrincipalExtractor extends BasePrincipalExtractor {

    @Override
    public Object extractPrincipal(Map<String, Object> map) {
        String id = map.get("id").toString();
        this.saveSocialUser(id, SocialProvider.KAKAO); // 이부분만 각각 맞는걸로 교체 해 주면 된다.
        return this.createPrincipal(map);
    }
}

extractPrincipal method 에서 각각 소셜로그인 후 가지고 온 정보를 가지고, DB 로 등록 해주면 된다.

마지막으로, WebSecurityConfig 를 살짝 건드려 주면 된다.

WebSecurityConfig.java

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private FacebookPrincipalExtractor facebookPrincipalExtractor;

    @Autowired
    private KakaoPrincipalExtractor kakaoPrincipalExtractor;

    @Autowired
    private GithubPrincipalExtractor githubPrincipalExtractor;

    // ...
    private Filter ssoFilter() {
        CompositeFilter filter = new CompositeFilter();
        List<Filter> filters = new ArrayList<>();
        filters.add(ssoFilter(facebook(), "/login/facebook", facebookPrincipalExtractor));
        filters.add(ssoFilter(github(), "/login/github", githubPrincipalExtractor));
        filters.add(ssoFilter(kakao(), "/login/kakao", kakaoPrincipalExtractor));
        filter.setFilters(filters);
        return filter;
    }

    private Filter ssoFilter(ClientResources client, String path, PrincipalExtractor extractor) {
        OAuth2ClientAuthenticationProcessingFilter filter = new OAuth2ClientAuthenticationProcessingFilter(path);
        OAuth2RestTemplate template = new OAuth2RestTemplate(client.getClient(), auth2ClientContext);
        filter.setRestTemplate(template);
        UserInfoTokenServices tokenServices = new UserInfoTokenServices(
                client.getResource().getUserInfoUri(), client.getClient().getClientId());
        tokenServices.setRestTemplate(template);
        tokenServices.setPrincipalExtractor(extractor);
        filter.setTokenServices(tokenServices);
        return filter;
    }

    // ...
}

이제 각각 로그인을 하면서, 확인 해 보면 새로운 레코드가 추가 되는걸 볼 수 있다.

최초

최초이미지

페이스북 로그인 후

페이스북추가

깃허브 로그인 후

깃허브추가

카카오 로그인은 직접 해보자

소스는

https://github.com/ziponia/spring-security-example

tag v3 로 가자