본문 바로가기
ToyProject

스프링 게시판 만들기 - 8 (1) (소셜 로그인 [구글,네이버,카카오,페이스북] )

by 완두완두콩 2022. 8. 1.

이번에는 최근 여러 웹사이트에서 흔히 볼 수 있는

소셜 로그인을 구현해 볼까 한다.

게시판 기능을 좀 더 많이 구현해보고 이쁘게 꾸미고, 코드 리팩토링도 해보고.. 하려고 했는데

현재 진행중인 한이음 대외활동에서 소셜 로그인을 이용해서 구현을 해보고자 했기에 

우선 토이프로젝트로 진행중이였던 게시판에서 구현을 해보고 대외활동 프로젝트에 적용해보려 한다!..

 

 

각 소셜 서비스 등록

 


소셜 로그인을 구현하기 위해서는 각 플랫폼마다 존재하는 OAuth 서비스를 이용해서 등록해야 한다!

 

구글


새로운 프로젝트를 하나 만들고, OAuth 동의 화면으로 이동해서 외부를 선택해준다.

 

위의 정보를 작성하고, 하단에 개발자 연락처를 추가하고 다음으로 넘어간다.

범위 추가 또는 삭제 클릭 -> 상위 두개를 선택 후 업데이트 버튼.

테스트 사용자는 패스하면 완료!

사용자 인증 정보 화면으로 넘어가서 OAuth 클라이언트 ID 생성을 클릭

다음과 같이 작성하고 만들기!

만들고 나면 다음과 같이 클라이언트ID 와 비밀번호가 생성되는데 해당 부분을

저장하거나, 따로 관리하는 걸 추천드립니다!

 

 

네이버


네이버 개발자 센터로 접속한 뒤 , 애플리케이션 등록 선택!

위와 같이 설정하고 등록하기 버튼!

위의 정보들을 앞선 구글에서 처럼 따로 저장!

 

카카오


카카오 개발자 센터에 접속 후, 로그인.

 

만든 애플리케이션을 클릭 후, 동의 항목을 다음과 같이 설정.

카카오 로그인에 들어가서 활성화 상태 ON!

다시 카카오 로그인으로 돌아간 뒤, 리다이렉트 URI 를 위와 같이 설정!!

 

여기까지가 이제 기본적으로 OAuth 를 사용하기 위한 설정이였습니다!!

 

참고 사항


승인된 리디렉션 URI 설정

승인된 리디렉션 URI 는 인증에 성공하면 각 구글, 카카오, 네이버에서 리다이렉트 할 URL 인데,

해당 부분에 대해서는 위 처럼 설정하면 크게 신경쓰지 않아도 괜찮다.

 

그 이유는,

스프링 시큐리티에서 기본적으로

{도메인}/login/oauth2/code/{소셜서비스코드}로

리다이렉트 URL을 지원하고 있기에 우리는 따로 컨트롤러를 만들어서 연결하거나

할 필요가 없다!!

 

 

코드 작성


spring.security.oauth2.client.registration.google.client-id=클라이언트ID
spring.security.oauth2.client.registration.google.client-secret=클라이언트시크릿
spring.security.oauth2.client.registration.google.scope=profile,email

# registration naver
spring.security.oauth2.client.registration.naver.client-id=클라이언트ID
spring.security.oauth2.client.registration.naver.client-secret=클라이언트시크릿
spring.security.oauth2.client.registration.naver.redirect-uri={baseUrl}/{action}/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.naver.authorization_grant_type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email,profile_image
spring.security.oauth2.client.registration.naver.client-name=Naver

# provider
spring.security.oauth2.client.provider.naver.authorization_uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token_uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user_name_attribute=response


scope=profile,email

application-oauth.properties 를 작성 후, 위와 같이 각자 본인의 클라이언트 정보를

작성해준다!

(카카오는 추후 다시 추가할 예정!)

작성한 후에는, application.properties 에 다음 코드를 작성해서 oauth.properties 를 추가해준다!

spring.profiles.include=oauth

 

 

Member.java

package toyproject.board.domain;

import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "member")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseTimeEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    //    @NotBlank(message = "아이디를 입력해주세요.")
//    @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "아이디를 3~12자로 입력해주세요. [특수문자 X]")
    @Column
    private String username;
    //    @NotBlank(message = "비밀번호를 입력해주세요.")
//    @Pattern(regexp = "^[a-zA-Z0-9]{3,12}$", message = "비밀번호를 3~12자로 입력해주세요.")
    @Column
    private String password;
    @Column
    private String email;

    /**
     * picture 은 로그인한 사용자의 프로필을 받기 위한 컬럼!
     */
    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    private Role role;

    @OneToMany(mappedBy = "member") // 주인 필드 명
    private List<Board> boardList = new ArrayList<>();

    @OneToMany(mappedBy = "member")
    private List<BoardComment> boardCommentList = new ArrayList<>();

    @Builder
    public Member(String username, String password, String email, List<Board> boardList, List<BoardComment> boardCommentList,Role role) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.boardList = boardList;
        this.boardCommentList = boardCommentList;
        this.role = role;
    }

    public Member update(String username,String picture){
        this.username = username;
        this.picture = picture;
        return this;
    }
    public String getRoleKey(){
        return this.role.getKey();
    }

}

 

@Enumerated(EnumType.STRING) 

을 사용한 이유는 스프링에서는 Enum 값을 DB 에 저장할 때 기본적으로 Int 형으로 저장하게 됩니다.

그렇게 되면, 해당 enum 이 무엇을 가리키는지 알 수가 없기 때문에 위 설정을 이용해서

String 타입으로 저장하도록 합니다!

 

 

Role.java

package toyproject.board.domain;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum Role {
    ADMIN("ROLE_ADMIN", "관리자"),
    USER("ROLE_USER", "일반 사용자");

    private final String value;
    private final String key;
}

 

스프링 시큐리티에서는 권한 코드에 항상 ROLE_이 앞에 있어야합니다.

그래서 코드별 키 값을 ROLE_ADMIN, ROLE_USER 등으로 지정했습니다.

 

*enum 사용 이유 : enum 외의 값을 못 받도록 하기 위해(남/여 같이 범위 한정)

*스프링 시큐리티에서는 권한을 비회원(GUEST), 준회원(USER), 정회원(MEMBER), 관리자(ADMIN) 4가지로 나눴다. 비회원은 그 누구든 접근할 수 있고, 준회원은 로그인을 한 이용자, 정회원은 로그인 한 이용자 중에 정회원 권한을 가진 이용자, 관리자는 로그인 한 이용자 중에 관리자 권한을 가진 이용자만 접근할 수 있다.

 

MebmerRepository.java

Optional<Member> findByEmail(String email);

이미 가입한 사용자인지, 처음 가입한 사용자인지 판별하기 위한 findByEmail 을 추가해줍니다.

 

build.gradle

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

oauth2 를 사용하기 위해 위 문장을 추가해줍니다!

config/auth 폴더를 만들어서 폴더 내부에 Oauth 관련 내용들을 작성하도록 하겠습니다.

 

OauthSecurityConfig.java

package toyproject.board.config.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import toyproject.board.domain.Role;

@RequiredArgsConstructor
@EnableWebSecurity
public class OauthSecurityConfig extends WebSecurityConfigurerAdapter {

    private final CustomOAuth2MemberService customOAuth2MemberService;

    // 비밀번호 암호화
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    // 인증 무시 -> static 디렉토리의 파일들은 항상 인증 무시 (통과)
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
    }

    // Request 가 들어오는 경우 권한 설정 & 로그인 & 로그아웃 처리
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeRequests()
                .antMatchers("/admin/**").hasRole(Role.USER.name())
                .antMatchers("/member/myinfo").hasRole(Role.ADMIN.name())
                .antMatchers("/**").permitAll()
//                .antMatchers("/api/v1/**").hasRole(Role.USER.name())
//                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/member/memberLoginForm")
                .defaultSuccessUrl("/member/memberLoginResult")
                .permitAll()
                .and()
                .logout()
//                .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
                .logoutSuccessUrl("/member/logoutResult")
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .and()
                .exceptionHandling().accessDeniedPage("/member/denied")
                .and()
                .oauth2Login()
                .userInfoEndpoint()
                .userService(customOAuth2MemberService);
    }
}

 

 

제 앞선 포스팅을 따라오시는 분이시라면 위 코드를 전체 복붙 하시면 되고,

그렇지 않으시면 하단의 protected void configure 만 복붙하시면 될 것 같습니다!

 

csrf().disable().headers().frameOptions().disable()

스프링 시큐리티에서는 h2 db 접근을 자동으로 block 해서 오류가 발생한다.

이런 오류를 막기 위해서 작성.

 

authorizeRequests()

URL 별 권한을 설정하기 위해 작성하는 시작점?!.. 이라고 생각하시면 될 것 같습니다.

 

.antMatchers

해당 링크에는 다음 role 을 가진 사람만 접근이 가능하도록 설정할 수 있습니다.

 

.formLogin

제 프로젝트의 경우, 직접 회원가입해서 form 에 아이디, 비밀번호를 입력받아서 로그인 할 수 있게 했기 때문에

.formLogin 을 설정해주었습니다.

 

.logout

로그아웃 했을 시에 리다이렉트 할 URI 설정입니다.

 

.oauth2Login()

oauth2 로그인 기능을 구현하기 위해 필요한 코드입니다.

 

.userInfoEndPoint()

oauth2 를 이용해서 로그인한 후 , 사용자 정보를 가져올 때 설정을 담당하는 코드입니다.

 

.userService()

oauth2 를 이용해서 로그인을 성공한 뒤, 여러 기능들을 구현해서 적용하기 위한 코드입니다.

 

이제 본격적인 코드를 구현하기에 앞서!..

코드들이 어떻게 동작하는지에 대해 완벽하게는 아니여도 최대한 파볼수 있을 만큼

파서 이해를 하고 작성을 해보려 한다.

 

(구글 로그인 구현은 성공했으나,내가 제대로 된 이해를 하고 짠 코드도 아니고

이해도 안되는데 그냥 복붙해서 붙여놓은 코드기에..)

 

또한 그냥 구현하는 것이 아니라, 쿠키-세션-JWT 의 차이점에 대해서 알아야하고 

AccessToken 과, RefreshToken 등 꼭 알아야 할 내용들을 미리 숙지하고 가야 한다고 생각한다.

 

하단 블로그에 알기 쉽게 잘 설명돼 있으니 참고!

 

[WEB] 📚 JWT 토큰 인증 이란? - 💯 이해하기 쉽게 정리

인증 방식 종류 (Cookie & Session & Token) 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해

inpa.tistory.com

앞선 OauthSecurityConfig.java 에서 마지막 줄에 .userService 로 

CustomOAuth2MemberService 를 넣었습니다.

 

해당 부분이 로그인한 뒤의 기능을 구현하는 곳이라고 말했는데, 해당 서비스에서

테스트를 해보고 구현을 진행 하겠습니다.

 

CustomOAuth2MemberService.java

OAuth2UserService 를 상속받고, 파라미터로 두개의 값을 받고 있다.

 

loadUser 는 로그인 한 뒤의 로그인 정보를 가져오는 메소드라고 생각하면 좋을 것 같다.

우선 OAuth2UserService 는 어떻게 구성돼있는지 확인을 해보자.

위와 같은 상속 관계를 가지고 있으며, OAuth2User 의 경우에는

getAttributes 를 구현해놓은 것을 볼 수 있다.

OAuthUserRequest 의 경우, 다음 메소드들을 구현해놓았다.

그럼 과연 각각의 메소드들은 어떤 값을 나타내는지 한번 출력해보려 합니다!..

그 전에, Oauth2UserService 객체를 생성하고, loadUser 를 통해 현재 로그인한 사용자의 정보를

oAuth2User 에 담습니다.

진행하는 상세 과정은 다음과 같습니다!

 

출력을 해보면 다음과 같습니다.

 

userRequest 를 통해 토큰 값과, 어떤 소셜을 통해 로그인했는지,  클라이언트 ID 와 클라이언트 비밀번호 등의 정보가 포함됨을

확인할 수 있습니다.

특히, Registration 을 통해 어떤 OAuth 로 로그인을 했는지 확인이 가능합니다!

 

oAuth2User 를 통해서 토큰과, 내가 로그인한 구글 계정에 대한 정보, 그리고 현재 로그인 했을 때

설정된 권한과 숫자로 이루어진 이름 값을 확인할 수 있습니다!

.getName() 은 무슨 값이냐!.. 하면 구글에서 생성한 내 계정의 id 값이라고 생각하면 될 것 같다.(primary key)

107441082743080990935 가 .getAttributes() 에도 똑같이 반복됨을 확인할 수 있습니다.

 

정보를 보면, 이름, 성, 프로필사진, 그리고 뒤쪽에 추가로 이메일, locale 정보 등이 포함돼 있다.

지금부터는 앞서 보여드렸던 Member 도메인에서 이 값들을 이용해서 로그인을 진행할 예정입니다.

 

username = google_107441082743080990935

password = "암호화(dokong)"

email = 본인 구글 이메일 주소

role = "ROLE_USER"

provider = "google"

providerId = 107441082743080990935

 

[우리는 비밀번호를 쳐서 로그인하는 것이 아니기에, password 는 null 만 아니면 상관 없는 값이다!]

 

이 모든 정보들은 oAuth2User.getAttributes() 에 담겨있기 때문에 해당 값을 이용해서 회원가입을 진행한다!

 

그 전에!.. 스프링 시큐리티에 대해서도 다시 진행을 해보고자 한다!..

스프링 시큐리티의 구조에 대해서는 앞선 게시판 만들기에서 공부했었지만 정확한 코드에 대해서는 잘 모르고 넘어갔었다.

이번 기회에 스프링 시큐리티 코드에 대해서도 다시!..

 

PrincipalDetailsService.java

package toyproject.board.config.auth;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.parameters.P;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import toyproject.board.detail.PrincipalDetails;
import toyproject.board.domain.Member;
import toyproject.board.domain.Role;
import toyproject.board.repository.MemberRepository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

//시큐리티 설정에서 loginProcessingUrl("")
// login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어 있는 loadUserByUsername 함수가 실행.
@Service
public class PrincipalDetailsService implements UserDetailsService {

    @Autowired
    private MemberRepository memberRepository;

    //username 이란 무엇인가? -> html 에서 input name 으로 값을 받을때 username 으로 동일해야 값이 받아짐. username2 로 html 에서 입력했으면 값이 안받아진다.
    // 시큐리티 세션 = Authenticaiton(내부 member 들어감)
    // 시큐리티 세션 (Authenticaiton(내부 member 들어감))
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Member member = memberRepository.findByUsername(username).get();
        if (member != null){
            return new PrincipalDetails(member);
        }
        return null;
    }
//    @Override
//    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        Optional<Member> findName = memberRepository.findByUsername(username);
//        Member member = findName.get();
//
//        List<GrantedAuthority> authorities = new ArrayList<>();
//
//        if(("admin").equals(username)){
//            authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getValue()));
//        }else {
//            authorities.add(new SimpleGrantedAuthority(Role.USER.getValue()));
//        }
////        return new User(member.getUsername(), member.getPassword() , authorities);
//        return new PrincipalDetails(member);
//    }

}

 

 

기존의 코드는 아래의 주석처리 된 부분이고, 위 부분이 새로 작성한 코드이다.

하단의 loginProcessingUrl 의 링크로 내가 로그인을 진행하면, 자동으로 PrincipalDetailsService 가 호출되고

스프링 시큐리티가 로그인을 대신 진행하게 된다.

참고 해야할 부분은, loadByUsername에서 String username 으로 매개변수를 받고 있는데, 

다음과 같이 이름이 username 으로 일치해야한다.

만약 이름을 다른 변수로 지정하게 되면 앞서 작성한 OauthSecurityConfig 에서 설정을 해주어야 합니다!

PrincipalDetails 를 리턴하는 것으로 돼있는데 그럼 PrincipalDetails 는 무엇인가?!..

 

앞선 스프링 게시판 만들기에서는 MemberDetail 을 생성해서 코드를 작성했는데 해당 부분을

PrincipalDetails 로 대체한다.

 

PrincipalDetails.java

package toyproject.board.detail;


// 시큐리티가 login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
// 로그인 진행이 완료되면 시큐리티 session 을 만들어 준다. ( Security ContextHolder 에 세션 정보를 저장)
// 오브젝트 -> Authenticaiton 타입 객체만 들어갈 수 있다.
// Authentication 안에 user 정보가 있어야 함
// user 오브젝트 타입 -> UserDetails 타입 객체

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import toyproject.board.domain.Member;

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

// Security Session 영역 (Security ContextHolder) => Authentication 객체가 필요함 => 해당 객체 안에 유저 정보가 들어가 있음 => UserDetails타입
@Data
public class PrincipalDetails implements UserDetails{

    private Member member;

    public PrincipalDetails(Member member) {
        this.member = member;
    }

    //해당 유저의 권한을 return 하는 곳!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
//        member.getRole(); 이 기존인데, Collection 타입이므로 변경해야한다.
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return member.getRole();
            }
        });
        return collect;
    }

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

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

    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠겼는지 확인 -> true 면 아니요.
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isEnabled() {

        // 우리 사이트에서 1년동안 회원이 로그인을 안하면 휴면 계정으로 하기로 함.
        // domain 에 Timestamp 로 loginDate 를 찍어준다.
//        member.getLoginDate();
        // 현재시간 - 로그인 시간 -> 1년을 초과하면 return false 로 설정.
        return true;
    }
    
}

PrincipalDetails 는 UserDetails 를 상속 받습니다.

해당 메소드에 대한 설명은 주석으로 적어놓았고, getAuthorities의 부분은

return 타입에 맞추려다보니 작성된 코드입니다!

PrincipalDetails 는 어떻게 작동하는가 하면!..

스프링 시큐리티 안에는 스프링 시큐리티 세션이 있고, 그 안에는

Authentication 객체가 존재합니다.

 

로그인 진행이 완료되면 시큐리티 세션을 생성하고,  Security ContextHolder 에 세션 정보를 저장합니다.

Security Session 안에는 Authentication 객체가 필요한데, 해당 Authentication 객체 안에는

UserDetails 타입으로 유저 정보가 들어가 있습니다.(& OAuth2User 타입도 들어갈 수 있음)

(일반 로그인은 UserDetails 타입, 소셜 로그인은 OAuth2User 타입)

그렇기에, 우리는 UserDetails 를 상속받아서 메소드를 구현하고 그 안에 로그인한 정보를

집어넣습니다.

 

 

지금까지의 내용 정리

 

우선 WebSecurityConfigurerAdapter 를 상속받는 OauthSecurityConfig 를 구현했고,

그 안에 스프링 시큐리티의 기본 설정과 customOAuth2MemberService 를 이용해서 로그인 된 뒤의

기능을 작성했습니다.

 

CustomOAuth2MemberService 에서는 현재 로그인한 사용자의 정보를

단순 출력하는 것으로 확인을 해보았습니다.

 

그 다음은, Security Session 안에 있는 Authentication 객체 안에 값을 집어넣기 위해

PrincipalDetail 과 PrincipalDetailsService 를 구현했습니다.

Authentication 객체 안에는 UserDetails 타입으로 값이 들어가 있는 상태입니다.

 

이제는 값이 어떻게 찍히는지 확인을 해보고, 구글 로그인을 구현하려 합니다!

 

MemberController.java


우리가 앞서 구현했던 코드들을 통해, 로그인한 사용자의 정보를

Authentication 객체를 통해 직접 접근하거나, @AuthenticationPrincipal 을 통해 세션정보에 접근해서

정보를 가져올 수 있습니다.

@AuthenticationPrincipal 의 기본 타입은 UserDetails 인데 다음 그림처럼

우리가 정의한 PrincipalDetails 는 UserDetails 를 상속받고 있기 때문에 위와 같이 작성이 가능합니다.

Authentication 객체를 통해 직접 접근하거나, @AuthenticationPrincipal 을 통해 세션정보에 접근해서

정보를 가져올 수 있다고 말씀드렸는데 그럼 값이 정말 동일한지 테스트를 진행해보았습니다.

첫번째 줄과 마지막 줄의 멤버 값이 같음을 확인할 수 있었고, 로그인한 유저의 정보에 대해

잘 매칭돼서 나옴을 확인할 수 있습니다.

 

그렇다면 구글 로그인은 어떨까?!..

구글 로그인은 OAuth2 를 이용하기 때문에 @AuthenticationPrincipal 에도 OAuth2User 타입으로

값을 받습니다. Authentication 은 동일!

 

구글 로그인도 테스트 해본 결과!..

두 개의 정보가 완벽히 일치한다는 것을 알 수 있습니다.

 

사용자 정보에 대한 테스트는 성공적으로 완료했지만 한가지 문제점이 있습니다.

 

기본 사용자로 로그인 한 경우에는 UserDetails 타입으로 받아야하고, 

Oauth2 로 로그인한 경우에는 OAuth2User 타입으로 받아야합니다.

 

근데 우리는 사용자가 어떤 방식으로 로그인하든 해당 기능을 작동시켜야 합니다.

그렇기에 PrincipalDetails 에 OAuth2User 를 상속받아서 이 문제를 해결합니다.

우리는 각각의 UserDetails 타입과 OAuth2User 타입을 선언할 필요가 없이,

이런 식으로 똑같이 PrincipalDetails 타입으로 받게 됐고

다음 그림과 같이 됨을 확인할 수 있습니다.

PrincipalDetails.java

package toyproject.board.detail;


// 시큐리티가 login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
// 로그인 진행이 완료되면 시큐리티 session 을 만들어 준다. ( Security ContextHolder 에 세션 정보를 저장)
// 오브젝트 -> Authenticaiton 타입 객체만 들어갈 수 있다.
// Authentication 안에 user 정보가 있어야 함
// user 오브젝트 타입 -> UserDetails 타입 객체

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import toyproject.board.domain.Member;
import toyproject.board.dto.MemberDto;

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

// Security Session 영역 (Security ContextHolder) => Authentication 객체가 필요함 => 해당 객체 안에 유저 정보가 들어가 있음 => UserDetails 타입
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private Member member;
    private Map<String, Object> attributes;

    // 일반 사용자
    public PrincipalDetails(Member member) {
        this.member = member;
    }

    // 소셜 로그인 사용자 Oauth 로그인
    public PrincipalDetails(Member member,Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }
    public PrincipalDetails(MemberDto memberDto, Map<String, Object> attributes) {
        this.member = member;
        this.attributes = attributes;
    }

    //해당 유저의 권한을 return 하는 곳!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
//        member.getRole(); 이 기존인데, Collection 타입이므로 변경해야한다.
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return member.getRole();
            }
        });
        return collect;
    }

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

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

    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정 잠겼는지 확인 -> true 면 아니요.
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    // 계정 만료됐는지 확인 -> true 면 아니요.
    @Override
    public boolean isEnabled() {

        // 우리 사이트에서 1년동안 회원이 로그인을 안하면 휴면 계정으로 하기로 함.
        // domain 에 Timestamp 로 loginDate 를 찍어준다.
//        member.getLoginDate();
        // 현재시간 - 로그인 시간 -> 1년을 초과하면 return false 로 설정.
        return true;
    }

    @Override
    public <A> A getAttribute(String name) {
        return OAuth2User.super.getAttribute(name);
    }

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

    //attributes.get("sub")
    @Override
    public String getName() {
        return null;
    }
}

위와 같이 코드를 수정해주면 Oauth2 로그인을 해도, 일반 사용자로 로그인해도 모두 

값이 정상적으로 출력됨을 확인할 수 있습니다.

 

내용이 길어져서 페이스북, 네이버, 카카오 로그인은

다음 포스팅에서 설명하도록 하겠습니다!

 

 

 


처음에 짠 코드와 차후 짠 코드가 많이 달라지고, 코드가 누락된 부분이 있어

혹여나 제 블로그를 읽으시는 분들께서는

혼란이 있을 것 같은데 양해 부탁드리겠습니다!..

 

혹시나 필요한 분이 계시다면 깃헙 주소를 남기겠습니다! :)

 

다음편 ! (실제 구현)

 

스프링 게시판 만들기 - 8 (2) (소셜 로그인 [구글,네이버,카카오,페이스북] )

스프링 게시판 만들기 - 8 (1) 편과 이어지는 내용입니다. 각각의 개발자 센터에 들어가서 클라이언트 Id 와 비밀번호를 받았다는 가정하에 진행합니다! 구글 CustomOAuth2MemberService.java package toyproject

dodokong.tistory.com

 

댓글