본문 바로가기
개발/Spring

[Spring] 스프링시큐리티 OAuth2.0 적용해보기(Google)

by devhooney 2022. 8. 31.
728x90

구글, 페이스북, 네이버 로그인을 적용해보려고 한다.

지난 포스팅에 사용했던 코드에 이어서 작업하려한다.

https://devhooney.tistory.com/99

 

[Spring] 스프링시큐리티 예제

이전에 작성한 포스팅은 단순히 책에 있는 내용을 정리한 것이었는데, 이번엔 직접 코드를 짜보면서 복습해보려고 한다. 1. 라이브러리 설치 implementation 'org.springframework.boot:spring-boot-starter-secur..

devhooney.tistory.com

 

1.  구글 api console

https://console.cloud.google.com/apis/dashboard

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

- 프로젝트를 만들어준다.

- 사용자 인증정보 클릭하면 클라이언트 아이디와 시크릿 키(보안 비밀)가 있다.

- 승인된 리디렉션URI에는 꼭 저렇게 넣어주어야 한다. 만약 고유의 URL이 있을 경우 localhost:8080 대신 고유 값을 넣어주면 된다.

 

2. application.yml 수정

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 넣어준다.
            client-secret: 넣어준다.
            scope: email, profile

 

3. SecurityConfig 수정

@Autowired
private PrincipalOauth2UserService principalOauth2UserService;

// 해당 메소드의 리턴되는 오브젝트를 IoC로 등록해준다.
@Bean
public BCryptPasswordEncoder encodePwd() {
    return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf().disable();
    http.authorizeRequests()
            .antMatchers("/user/**").authenticated() // 인증만 도면 접근 가능
            .antMatchers("/manager/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
            .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
            .anyRequest().permitAll()
            .and()
            .formLogin()
            .loginPage("/loginForm")
            .loginProcessingUrl("/login") // /login 주소가 호출이 되면 시큐리티가 낚아채서 대신 로그인을 진행해준다.
            .defaultSuccessUrl("/")
            // 여기부터 추가됨
            .and()
            .oauth2Login()
            .loginPage("/loginForm")
            .userInfoEndpoint()
            .userService(principalOauth2UserService)
    ;
    return http.build();
}

 

4. 일반 로그인과 OAuth 로그인 구분

- 폴더구조 변경

- oauth 패키지 안에 PrincipalOauth2UserService 생성

- 여기서 OAuth 로그인 후 회원가입 진행

- DefaultOAuth2UserService 상속받아 메소드 재정의

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserRepository userRepository;

    // 구글로부터 받은 userRequest 데이터에 대한 후처리되는 함수
    // 함수 종료 시  @AuthenticationPrincipal 어노테이션이 만들어진다.
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        // 구글 로그인 버튼 클릭 -> 구글 로그인창 -> 로그인 완료 -> 코드 리턴 -> 액세스 토큰 요청
        // userRequest 정보 -> loadUser함수 호출 -> 구글로부터 회원 프로필을 받아준다.

        OAuth2User oAuth2User = super.loadUser(userRequest);
        OAuth2UserInfo oAuth2UserInfo = null;
        if (userRequest.getClientRegistration().getRegistrationId().equals("google")) {
            oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes());
        } else {
            System.out.println("Only Google");
        }

        String provider = Objects.requireNonNull(oAuth2UserInfo).getProvider();
        String providerId = oAuth2UserInfo.getProviderId();
        String username = provider + "_" + providerId;
        String password = bCryptPasswordEncoder.encode("비밀번호");
        String email = oAuth2UserInfo.getEmail();
        String role = "RULE_USER";

        User userEntity = userRepository.findByUsername(username);

        if (userEntity == null) {
            userEntity = User.builder()
                    .username(username)
                    .password(password)
                    .email(email)
                    .role(role)
                    .provider(provider)
                    .providerId(providerId)
                    .build();
            userRepository.save(userEntity);
        }

        return new PrincipalDetails(userEntity, oAuth2User.getAttributes());
    }
}

 

- User 클래스에 빌드패턴과 OAuth 회원가입 구분을 위한 수정

@Entity
@Getter
@Setter
// 추가됨
@NoArgsConstructor
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    private String username;
    private String password;
    private String email;
    private String role; // ROLE_ADMIN, ROLE_USER

    // 추가됨
    private String provider;
    private String providerId;

    @CreationTimestamp
    private Timestamp createDate;

    // 추가됨
    @Builder
    public User( String username, String password, String email, String role, String provider, String providerId, Timestamp createDate) {
        this.username = username;
        this.password = password;
        this.email = email;
        this.role = role;
        this.provider = provider;
        this.providerId = providerId;
        this.createDate = createDate;
    }
}

 

- oauth 패키지 안에 provider 패키지 추가 후 OAuth2UserInfo 인터페이스 추가

public interface OAuth2UserInfo {
    String getProviderId();
    String getProvider();
    String getEmail();
    String getName();
}

 

- GoogleUserInfo 클래스 추가

- OAUth2UserInfo 상속받아 메소드 재정의해준다.

- 구글 로그인 정보는 attributes()에 들어있다.(userRequest에 있는 것을 받음)

public class GoogleUserInfo implements OAuth2UserInfo{

    private Map<String, Object> attributes; // getAttributes()

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

    @Override
    public String getProviderId() {
        return (String) attributes.get("sub");
    }

    @Override
    public String getProvider() {
        return "google";
    }

    @Override
    public String getEmail() {
        return (String) attributes.get("email");
    }

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

 

- PrincipalDetails에 OAuth 관련 내용 추가

@Data
public class PrincipalDetails implements UserDetails, OAuth2User { // OAuth2User 추가

    private User user; // 컴포지션
    private Map<String, Object> attributes; // 추가됨

    // 일반 로그인
    public PrincipalDetails(User user) {
        this.user = user;
    }

    // OAuth 로그인 // 추가됨
    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

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

    // 해당 User의 권한을 리턴하는 곳!
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collect = new ArrayList<>();
        collect.add((GrantedAuthority) () -> user.getRole());
        return collect;
    }

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

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

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

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

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

    @Override
    public boolean isEnabled() {

        // 사이트에서 1년동안 회원이 로그인을 안했을 경우 휴면 계정으로 하기로 함.
        // 현재 시간 - 로그인 시간 => 1년을 초과하면 return false;

        return true;
    }

    @Override // 추가됨 -- 사용은 X
    public String getName() {
        return null;
    }
}

 

- 실행 후 순환참조 오류가 발생한다면 SecurityConfig 수정

@Autowired
private PrincipalOauth2UserService principalOauth2UserService;

 

5. html 수정 후 테스트

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>로그인 페이지</title>
</head>
<body>
<h1>로그인 페이지</h1>
<hr>
<form action="/login" method="POST">
    <input type="text" name="username" placeholder="Username"><br>
    <input type="password" name="password" placeholder="Password"><br>
    <button>로그인</button>
</form>
<a href="/oauth2/authorization/google">구글 로그인</a> <!-- 추가됨 -->
<a href="/joinForm">회원가입을 아직 하지 않으셨나요?</a>
</body>
</html>

 

- DB 조회

 

 

728x90