개발환경, 도구/오픈API

Google API Console - OAuth login

gu9gu 2023. 1. 10. 18:53

Google API Console 접속

 

1. 새 프로젝트 등록하여 선택

 - 프로젝트 이름 : springboot-oauth-google

 

2. 좌측 메뉴 - OAuth 동의 화면

 1) 외부에 체크하고 만들기

 2) 만들기 세부 설정 후 저장

 - 앱 이름 : springboot-oauth

 - 문의 email 2개에 입력

 

3. 좌측 메뉴 - 사용자 인증 정보

  1) 사용자 인증 정보 만들기 -  OAuth 클라이언트 ID

  2) 세부 설정 후 저장

   - 애플리케이션 유형 : 웹 애플리케이션

   - 이름 : springboot-oauth

   - 승인된 리디렉션 URI 추가 : http://localhost:8080/login/oauth2/code/google

     tip) oauth 라이브러리를 사용하면 login/oauth2/code/google 는 라이브러리에서 사용하는 고정 주소여서 따르 컨트롤러에 맵핑하지 않아  도 된다.

  3) 저장하면 나오는 클라이언트ID, 클라이언트 보안 비밀번호는 따로 관리 해야 함

 

 

4. 스프링 시큐리티 OAuth-client 라이브러리 사용

 - 스프링부트 프로젝트 만들 때 OAuth2 Client 체크 해서 만듬 ( pom.xml에서 해당 dependency 복사해서 사용)

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>

- application.yml에 OAuth google 설정 추가

security:
  oauth2:
    client:
      registration:
        google: # /oauth2/authorization/google 이 주소를 동작하게 한다.
          client-id: 구글 API 콘솔에서 프로젝트의 클라이언트ID를 찾아 입력한다.
          client-secret: 구글 API 콘솔에서 프로젝트의 클라이언트 보안 비밀번호를 찾아 입력한다.
          scope: # scope("openid", "profile", "email", "address", "phone")
            - email
            - profile

 

5. 구글로그인페이지를 띄운다.

 1) loginForm.html에 버튼 추가

<a href="/oauth2/authorization/google">Google</a>

 2)  seurityconfig에 oauth2 설정 추가

.and()
.oauth2Login()                  // OAuth2기반의 로그인인 경우
.loginPage("/loginForm")        // 인증이 필요한 URL에 접근하면 /loginForm으로 이동

 

6. 일반로그인, OAuth2 로그인 2가지를 한 곳에서 후처리하기 위해 PrincipalDetails에서 UserDetails, OAuth2User를 implements

package com.jig.security1.config.auth;

import com.jig.security1.model.User;
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 java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

/**
 * [로그인 처리 과정에서 사용되는 UserDetails]
 *
 *  - Security Session -> Authentication -> UserDetails
 *  - 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킨다.
 *  - 로그인 진행이 완료되면 시큐리티 session을 만든다. (Security ContextHolder에 session정보를 저장한다.)
 *  - Authentication안에 user정보를 담는다. User 오브젝트는 UserDetails 타입이여야 한다.
 *  - UserDetails를 구현하여 사용하는데, user Entity를 컴포지션(변수로 사용)하고 각 메서드를 overrride 한다.
 */

/**
 *  [일반로그인, OAuth2 로그인 2가지를 한 곳에서 후처리하기 위한 방법]
 *
 *  - 스프링 시큐리티에 session을 관리하는 security session이 있다.
 *  - 로그인 하면 security session에 Authentication이 들어간다.
 *  - Authentication은 각 메서드에서 DI해서 사용할 수 있다.
 *  - Authentication에는 UserDetails 타입, OAuth2User 타입이 들어갈 수 있다. 일반 로그인 한 경우에는 UserDetails, OAuth2로그인한 경우는 OAuth2User
 *  - 로그인 후처리를 위해  UserDetails 타입, OAuth2User 타입 2가지를 한 클래스에서 implements 해서 사용하면 된다.
 *
 *    ===> public class PrincipalDetails implements UserDetails, OAuths2User{}
 */
@Data
public class PrincipalDetails implements UserDetails, OAuth2User {

    private User user;

    private Map<String, Object> attributes;

    /**
     * 일반 로그인할 때 사용되는 생성자
     * @param user
     */
    public PrincipalDetails(User user) {
        this.user = user;
    }

    /**
     * OAuth 로그인할 때 사용되는 생성자
     * @param user
     * @param attributes
     */
    public PrincipalDetails(User user, Map<String, Object> attributes) {
        this.user = user;
        this.attributes = attributes;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        // user.getRole() 을 타입에 맞게 만들어준다.
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole();
            }
        });

        return collection;
    }

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

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

    /**
     * 계정이 만료된게 아닌지 ( true : 만료 안 됨)
     * @return
     */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /**
     * 계정이 잠긴게 아닌지 ( true : 안 잠김)
     * @return
     */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /**
     * 패스워드가 만료된게 아닌지 ( true : 만료 안 됨)
     * @return
     */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /**
     * 계정이 활성화 됐는지 (true : 활성화)
     * @return
     */
    @Override
    public boolean isEnabled() {

        /**
         * 사용 예시
         *  1년동안 로그인 안한 회원을 휴면계정으로 처리하기로 함
         *    1. User객체에 '로그인 시간' 변수를 둠.   ( user.getLoginTime() )
         *    2. '현재시간 - 로그인시간' => 1년을 초과하면 return false;
         */

        return true;
    }

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

    @Override
    public String getName() {
        return null;
    }
}
 
7. 구글 로그인 후 처리 로직 
package com.jig.security1.config.oauth;

import com.jig.security1.config.auth.PrincipalDetails;
import com.jig.security1.model.User;
import com.jig.security1.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
public class PrincipalOauth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private UserRepository userRepository;

    /**
     *  BCryptPasswordEncoder-loadUser, PrincipleService-loadUserByUsername
     *  가 없어도 내부적으로 동작하는데, PrincipalDetails반환해주기 위해 overrride 함.
     *
     * [구글로부터 받은 userRequest 데이터에 대한 후처리를 하는 함수]
     *
     *   구글 로그인 동작 흐름
     *    1. 구글 로그인 버튼 클릭
     *    2. 구글 로그인 창
     *    3. 로그인 완료
     *    4. code를 반환 받는데, OAuth2 Client 라이브러리가 code를 이용해서 AccessToken을 요청
     *    5. AccessToken과 userRequest 정보를 반환받는다. loadUser 함수에서 받환 받는다. ( userRequest에는 회원프로필 정보가 있다.)
     *
     * @param userRequest
     * @return OAuth2User
     * @throws OAuth2AuthenticationException
     */
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        /*
        System.out.println("userRequest = " + userRequest);
        System.out.println("userRequest.getClientRegistration() = " + userRequest.getClientRegistration());
        System.out.println("userRequest.getAccessToken() = " + userRequest.getAccessToken());
        System.out.println("userRequest.getAccessToken().getTokenValue() = " + userRequest.getAccessToken().getTokenValue());
        System.out.println("super.loadUser(userRequest).getAttributes() = " + super.loadUser(userRequest).getAttributes());
        */

        /*
            [회원가입 정보]

            username = "google_" + super.loadUser(userRequest).getAttributes().get("sub")
            password = 암호화(겟인데어)
            email = super.loadUser(userRequest).getAttributes().get("email")
            role = ROLE_USER
            provider = userRequest.getClientRegistration().get("registrationId")
            providerId = super.loadUser(userRequest).getAttributes().get("sub")
         */

        // 회원등록이 안됐으면 회원가입 진행
        OAuth2User oAuth2User = super.loadUser(userRequest);

        String provider = userRequest.getClientRegistration().getRegistrationId();
        String providerId = oAuth2User.getAttribute("sub");
        String username = provider + "_" + providerId;
        // TODO: OAuth2 회원가입인 경우 패스워드에 특정값을 넣어줘서 노출되는 경우 모든 OAuth 회원이 노출되게 되는 문제가 있음. 해결 필요함
        String password = bCryptPasswordEncoder.encode("특정값");
        String email = oAuth2User.getAttribute("email");
        String role = "ROLE_USER";


        User userEntity = userRepository.findByUsername(username);
        if (null == userEntity) {
            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());
    }
}

 

 

8. 구글 로그인 후처리를 어디서 할 지 SecurityConfig에서 설정

/**
 * OAuth2 과정
 *  1. 토큰 받기 (인증)
 *  2. 토큰으로 access토큰 받기 (정보에 대한 접근 권한)
 *  3. access토큰으로 정보 요청
 *  4. 받아온 정보를 이용해 바로 회원가입을 할 수 있음 or 회원에 추가정보가 필요한 경우 추가정보입력화면을 띄워서 회원가입 진행
 *
 * 그런데, OAuth2 Client 라이브러리 사용하면 바로 access토큰, 사용자프로필정보 를 받아온다.
 */
@Configuration
@EnableWebSecurity  // 스프링 시큐리티 필터가 스프링 필터체인에 등록됩니다.
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) // securedEnabled = true -> @Secured 어노테이션 활성화 // prePostEnabled = true -> @PreAuthorize, @PostAuthorize 어노테이션 활성화
public class SecurityConfig  extends WebSecurityConfigurerAdapter {

    @Autowired
    private PrincipalOauth2UserService principalOauth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()   // crsf 토큰 비활성화 ( 테스트 시 필요)
                .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")   // 이 경로가 호출되면 시큐리티가 낚아채서 대신 로그인을 진행한다.
                .defaultSuccessUrl("/")         // /loginForm 으로 들어와서 정상 로그인 후 이동되는 경로 ( 다른 경로로 들어와서 로그인 한 경우에는 들어온 경로로 callback 한다)
                .and()
                .oauth2Login()                      // OAuth2기반의 로그인인 경우
                .loginPage("/loginForm")            // 인증이 필요한 URL에 접근하면 /loginForm으로 이동
                .userInfoEndpoint()                         // 로그인 성공 후 사용자정보를 가져온다
                .userService(principalOauth2UserService)   // 로그인 후 후처리하는 객체 등록  // 구글서버가 '엑세스 토큰', '사용자 프로필 정보'를 반환
                ;
//                .authorizationEndpoint()
//                .baseUri("/login/oauth2/authorization");
    }
}

 

 

 

 

참고

12. OAuth2 (spring.io)

[Spring Security] OAuth 구글 로그인하기 (tistory.com)

'개발환경, 도구 > 오픈API' 카테고리의 다른 글

springboot OAuth2 Client - facebook, naver login  (0) 2023.01.11
kakao developers - 로그인  (0) 2022.12.29