본문 바로가기
공부/Spring

[Spring] 스프링 Oauth2 구글 로그인과 jpa 사용하여 유저 정보 데이터베이스에 저장 및 권한 설정 (OAuth2 스프링 2편)

by 웅대 2023. 3. 24.
728x90
반응형

https://growth-coder.tistory.com/135

 

[Spring] 스프링 Oauth2 구글 로그인과 jpa 사용하여 유저 정보 데이터베이스에 저장 (OAuth2 스프링 1편

OAuth2 구글 로그인을 해 볼 예정인데 mysql 데이터베이스와 jpa를 사용하여 데이터베이스에 유저 정보를 저장해보려 한다. 먼저 OAuth2에 대해 간단하게 알아보자면 로그인, 회원가입 구현 과정의 번

growth-coder.tistory.com

이전 포스팅에 이어서 구글로부터 받은 정보를 데이터베이스에 저장하고 권한 설정을 해보려한다.

 

이전 포스팅에서 jpa 및 데이터베이스 세팅을 해뒀기 때문에 이번 포스팅에서는 코드만 작성하면 된다.

 

구글로부터 회원 정보를 받으면 DefaultOAuth2UserService를 확장한 클래스의 loadUser 메소드가 실행된다.

 

즉 회원 정보를 데이터베이스에 저장하는 과정은 이 메소드에서 일어나게 된다.

 

새로운 Member를 생성하여 데이터베이스에 저장해야하기 때문에 생성자를 만들어준다.

 

생성자는 Builder 패턴을 사용한다.

@Entity
@NoArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id; //기본키
    private String name; //유저 이름
    private String password; //유저 비밀번호
    private String email; //유저 구글 이메일
    private String role; //유저 권한 (일반 유저, 관리지ㅏ)
    private String provider; //공급자 (google, facebook ...)
    private String providerId; //공급 아이디
    @Builder
    public Member(String name, String password, String email, String role, String provider, String providerId) {
        this.name = name;
        this.password = password;
        this.email = email;
        this.role = role;
        this.provider = provider;
        this.providerId = providerId;
    }
}

 

OAuth2User를 사용해도 되지만 굳이 새로운 클래스를 만드는 이유는 OAuth2User에는 멤버 엔티티가 존재하지 않기 때문이다.

 

그래서 내부에 멤버 엔티티를 가지는 PrincipalDetails 클래스를 만드는 것이다.

 

@Getter
public class PrincipalDetails implements OAuth2User {
    private Member member;
    private Map<String, Object> attributes;

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

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


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return member.getRole();
            }
        });
        return collect;
    }

    @Override
    public String getName() {
        return "name";
    }
}

OAuth2로 로그인하는 것이 아니라 직접 로그인을 구현할 때는 UserDetails를 구현해야 한다.

만약 OAuth2도 사용하고 직접 로그인도 사용한다면 UserDetails와 OAuth2User를 동시에 구현하면 된다.

 

ex) public class PrincipalDetails extends UserDetails, OAuth2User {...}

 

 

BcryptPasswordEncoder를 빈으로 등록한다.

 

@Configuration 어노테이션이 붙은 곳에서 등록해도 되지만 간단하게 Application에서 등록을 했다.

@SpringBootApplication
public class Oauth2GoogleApplication {

   public static void main(String[] args) {
      SpringApplication.run(Oauth2GoogleApplication.class, args);
   }
   @Bean
   public BCryptPasswordEncoder bCryptPasswordEncoder() {
      return new BCryptPasswordEncoder();
   }

}

빈으로 등록한 encoder를 loadUser 메소드에서 사용한다.

@Service
@RequiredArgsConstructor
public class OAuth2MemberService extends DefaultOAuth2UserService {
    private final BCryptPasswordEncoder encoder;
    private final MemberRepository memberRepository;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);
        String provider = userRequest.getClientRegistration().getClientId();
        String providerId = oAuth2User.getAttribute("sub");
        String username = provider + "_" + providerId; //중복이 발생하지 않도록 provider와 providerId를 조합
        String email = oAuth2User.getAttribute("email");
        String role = "ROLE_USER"; //일반 유저
        Optional<Member> findMember = memberRepository.findByName(username);
        if (findMember.isEmpty()) { //찾지 못했다면
            Member member = Member.builder()
                    .name(username)
                    .email(email)
                    .password(encoder.encode("password"))
                    .role(role)
                    .provider(provider)
                    .providerId(providerId).build();
            memberRepository.save(member);
        }
        return oAuth2User;
    }
}

username을 provider와 providerId를 조합해서 만들면 이 username이 존재하는지 확인해야한다.

 

username을 검색해서 존재하지 않는다면 데이터베이스에 저장하고 존재한다면 저장하지 않는다.

 

그리고 이전 포스팅에서 관리자만 접근 가능한 페이지를 만들었는데 관리자만 접근을 할 수 있도록 설정을 하지 않아서 SecurityConfig에 해당 설정을 해준다.

 

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
    private final OAuth2MemberService oAuth2MemberService;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .requestMatchers("/private/**").authenticated() //private로 시작하는 uri는 로그인 필수
                .requestMatchers("/admin/**").access("hasRole('ROLE_ADMIN')") //admin으로 시작하는 uri는 관릴자 계정만 접근 가능
                .anyRequest().permitAll() //나머지 uri는 모든 접근 허용
                .and().oauth2Login()
                .loginPage("/loginForm") //로그인이 필요한데 로그인을 하지 않았다면 이동할 uri 설정
                .defaultSuccessUrl("/") //OAuth 구글 로그인이 성공하면 이동할 uri 설정
                .userInfoEndpoint()//로그인 완료 후 회원 정보 받기
                .userService(oAuth2MemberService).and().and().build(); //
    }
}

보다시피 admin 하위 경로는 role이 "ROLE_ADMIN"인 유저만 접근할 수 있다.

 

우선은 데이터베이스에 유저 정보를 저장할 때 무조건 "ROLE_USER"를 저장하도록 구현했는데 이 유저로 로그인하면 private는 접근 가능해야하고 admin은 접근할 수 없어야한다.

 

이후 데이터베이스에 저장된 유저 정보를 삭제하고 loadUser 메소드에서 "ROLE_USER"가 아닌 "ROLE_ADMIN"으로 바꿔서 구글 로그인을 통해 데이터베이스에 유저 정보를 저장하면 admin 경로에 접근이 가능하다.

 

참고

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0/dashboard

 

[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의

스프링부트 시큐리티에 대한 개념이 잡힙니다., - 강의 소개 | 인프런

www.inflearn.com

 

728x90
반응형

댓글