본문 바로가기
Side Project

[Spring Boot] chatGPT를 활용한 AI 사이트 (2) - 소셜 로그인 구현

by jisayDeveloper 2023. 9. 18.
728x90
반응형
SMALL

회원가입, 로그인 기능은 Spring Security 를 사용해서 구현할 예정이고, 로그인은 소셜로그인만 하도록 하겠습니다. (구글, 네이버, 카카오)


SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    public void configure(WebSecurity web) throws Exception {
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        firewall.setAllowUrlEncodedDoubleSlash(true);
        web.httpFirewall(firewall);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {


        http.csrf().disable()
                .authorizeRequests()
                .regexMatchers("^.*(?<!/)/{2}.*$").permitAll()

                .and().formLogin()
                .loginPage("/loginPage")
                .defaultSuccessUrl("/index")
                .failureUrl("/loginPage")
                .usernameParameter("memberId")
                .permitAll()

                .and()
                .oauth2Login()
                .defaultSuccessUrl("/")
                .userInfoEndpoint()
                .userService(customOAuth2UserService)
                .and()


                .and().logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")



                .and().requiresChannel()
                .requestMatchers(r -> r.getHeader("X-Forwarded-Proto") != null)
                .requiresSecure()



                .and()
                .sessionManagement()
                .maximumSessions(1) 
                .maxSessionsPreventsLogin(false);



    }
}

CustomOAuth2UserService

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

    private final SNSMemberRepository snsMemberRepository;
    private final HttpSession httpSession;


    public CustomOAuth2UserService(SNSMemberRepository snsMemberRepository, HttpSession httpSession) {
        this.snsMemberRepository = snsMemberRepository;
        this.httpSession = httpSession;
    }

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId();
        String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
                .getUserInfoEndpoint().getUserNameAttributeName();

        OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName
                , oAuth2User.getAttributes());

        Member member = saveOrUpdate(attributes);
        httpSession.setAttribute("SNSmember", member);


        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(member.getRole())),
                attributes.getAttributes(),
                attributes.getNameAttributeKey()
        );
    }

    private Member saveOrUpdate(OAuthAttributes attributes) {
        Member member = snsMemberRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName()))
                .orElse(attributes.toEntity());


        return snsMemberRepository.save(member);
    }
}

OAuth2UserService는 Spring Security에서 OAuth 2.0 기반의 사용자 정보를 가져오는 데 사용되는 인터페이스입니다.

 

loadUser 메서드는 OAuth 2.0 프로토콜을 사용하여 사용자 정보를 가져오는 작업을 수행합니다. OAuth2UserRequest는 OAuth 2.0 사용자 정보를 가져오기 위한 요청 정보를 나타내는 클래스이고, OAuth2User는 가져온 사용자 정보를 나타내는 클래스입니다.

 

따라서 CustomOAuth2UserService 클래스는 이 OAuth2UserService 인터페이스를 구현하여 OAuth 2.0 기반의 소셜 로그인에서 사용자 정보를 가져오는 작업을 수행하고 구체적으로 loadUser 메서드를 재정의하여 OAuth 2.0 프로토콜을 사용하여 사용자 정보를 가져오고 처리합니다.

이렇게 구현된 CustomOAuth2UserService 클래스는 Spring Security에서 소셜 로그인에 사용되며, OAuth 2.0 플로우를 따라 사용자 정보를 가져올 수 있게 됩니다. 이 정보를 기반으로 사용자를 인증하고, 세션에 저장하거나 다른 로직에 활용할 수 있습니다.


OAuthAttributes

@Getter
@Builder
public class OAuthAttributes {

    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;

    public static OAuthAttributes of(String registrationId, String userNameAttributeName
            , Map<String, Object> attributes){


        if("naver".equals(registrationId)){
            return ofNaver("response", attributes);

        }else if("kakao".equals(registrationId)){
            return ofKakao("id", attributes);
        }
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes){
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    private static OAuthAttributes ofNaver(String userNameAttributeName, Map<String, Object> attributes){
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");

        return OAuthAttributes.builder()
                .name((String) response.get("name"))
                .email((String) response.get("email"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    private static OAuthAttributes ofKakao(String userNameAttributeName, Map<String, Object> attributes){
        Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
        Map<String, Object> kakaoProfile = (Map<String, Object>) kakaoAccount.get("profile");

        return OAuthAttributes.builder()
                .name((String) kakaoProfile.get("nickname"))
                .email((String) kakaoAccount.get("email"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public Member toEntity(){
        return Member.builder()
                .name(name)
                .email(email)
                .role(MemberRole.USER.getValue())
                .build();
    }
}

소셜 로그인 시에 제공 업체에 따라 다양한 형식으로 제공되는 사용자 정보를 효과적으로 처리하고, 이 정보를 Member 엔티티로 변환하여 데이터베이스에 저장하는 데 사용됩니다.

 

이를 통해 소셜 로그인한 사용자를 애플리케이션의 일반 회원으로 등록하고 인증할 수 있습니다.

 

 


Member (Entity)

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class Member implements UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mno;
    private String name;
    private String email;
    

    private String role;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        ArrayList<GrantedAuthority> auth = new ArrayList<GrantedAuthority>();
        auth.add(new SimpleGrantedAuthority(role));
        return auth;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return loginId;
    }

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

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

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

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


    public Member(String name, String email, String role) {
        this.name = name;
        this.email = email;
        this.role = role;

    }

}

@Getter
public enum MemberRole {

    USER("ROLE_USER"), ADMIN("ROLE_ADMIN"), MANAGER("ROLE_MANAGER");
    MemberRole(String value) {

        this.value = value;
    }
    private String value;
}

소셜 로그인만 사용할 것이기에 getPassword 메서드는 null로 두면 된다.


SNSMemberRepository

public interface SNSMemberRepository extends JpaRepository<Member, Long> {

    Optional<Member> findByEmail(String email);
}

JpaRepository는 Spring Data JPA 프레임워크의 일부로, JPA(Java Persistence API)를 사용하여 데이터베이스와 상호작용하는데 도움을 주는 인터페이스입니다. JpaRepository를 사용하면 개발자가 데이터베이스에 접근하고 데이터를 조작하는데 필요한 많은 기능을 자동으로 생성하고 관리할 수 있습니다.

 

이제 소셜로그인을 위한 준비가 끝났으니 간단히 View단을 만들어 확인해보겠습니다.

 

이제 소셜로그인 버튼을 누르면

 

configure에 /loginPage로 요청이 들어가면

각각의 소셜로 로그인 하면 동의와 함께 완료되는 동시에 index 페이지로 가게되고

 

따로 logout 버튼을 만들지 않았지만 get 방식으로 /logout을 요청하면 

로그아웃 되는 동시에 /loginPage로 리다이렉트 됩니다.

 

이제 DB를 확인해보면 

구글, 네이버, 카카오 순으로 insert 돼 있는 걸 확인할 수 있습니다.

728x90
반응형
LIST