Spring Security session방식 Oauth2로그인- 구글로그인 두개이상의 provider(5)

2025. 2. 28. 17:52Springboot/security

 

yml파일에서 client registration의 google을  추가합니다

참고로 google 같은 경우 provider 값을 security가 갖고있기 때문에  

따로 설정해주지 않아도 됩니다. 

동작 방식은 https://brilliantdevelop.tistory.com/218과 같습니다.

 

dotenv:
  enabled: true  # .env파일설정.    KAKAO_CLIENT_ID 직접 쓰려면 없어도됨.
spring:
  datasource:
    url: jdbc:h2:mem:security
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console: # H2 DB를 웹에서 관리할 수 있는 기능
      enabled: true           # H2 Console 사용 여부
      path: /h2-console       # H2 Console 접속 주소
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        default_batch_fetch_size: 100
    open-in-view: false
  devtools:
    livereload:
      enabled: true
    freemarker:
      cache: false
    restart:
      enabled: true
  thymeleaf:
    cache: false


  security:
    oauth2:
      client:
        registration:
          kakao:
            client-id: ${KAKAO_CLIENT_ID}  # 카카오 REST API 키
            #client-secret: ${KAKAO_CLIENT_SECRET}  # 선택 (일반적으로 필요 없음)
            redirect-uri: http://localhost:8080/login/oauth2/code/kakao # 카카오 로그인 후 리디렉트될 주소
            authorization-grant-type: authorization_code  # OAuth2 인증 방식 (코드 방식)
            scope:
              - profile_nickname
              - account_email  # 요청할 사용자 정보 (이메일, 닉네임)


          google: # ✅ 구글 OAuth2 추가
            client-id: ${GOOGLE_CLIENT_ID}
            client-secret: ${GOOGLE_CLIENT_SECRET}   #구글은 인가코드- access_token 과정에서 내부적으로 secret을 필요로 함.
            scope:
              - profile       #카카오는 profile안의 여러정보 다 따로 지정해줘야하지만 구글은 profile만 지정하면 됨.
              - email
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/google


        provider:
          kakao:
            authorization-uri: https://kauth.kakao.com/oauth/authorize  # 카카오 로그인 URL  (인증서버)
            token-uri: https://kauth.kakao.com/oauth/token  # 액세스 토큰 요청 URL (인증서버)
            user-info-uri: https://kapi.kakao.com/v2/user/me  # 사용자 정보 요청 URL (리소스서버)
            user-name-attribute: id  # OAuth2User에서 사용할 식별 값. 왠만하면 oauth에서 제공하는대로사용하자. 카카오는 id, 구글은 sub



#          google:  #security 가 구글provider에 대한 정보를 갖고 있어서 추가안해도됨.
#            authorization-uri: https://accounts.google.com/o/oauth2/auth  # ✅ 인증 서버 (Authorization Server)
#            token-uri: https://oauth2.googleapis.com/token  # ✅ 액세스 토큰 요청 (Token Server)
#            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo  # ✅ 사용자 정보 요청 (Resource Server)
#            user-name-attribute: sub  # ✅ Google의 고유 사용자 ID (sub)


logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace








#    google:
#      client-id: your-client-id
#      client-secret: your-client-secret
#      # 다른 설정들은  이미 설정되어있음   client-authentication-method: client_secret_basic으로 .
#      # redirect-uri :  http://localhost:8080/login/oauth2/code/google 기본이지만 실제 서비스에서는 변경
#      # provider의 user-name-attribute도 구글에서 사용하는 유저객체 식별 속성이고 정해져있기 때문에 provider쪽
#      scope:
#        - profile
#        - email

 

 

 

구글 OAUTH 설정

 

구글 클라우드- 콘솔

 

 

 

 

새 프로젝트 만들기

 

 

프로젝트 선택

 

 

 

 

사용자 인증 정보 선택   (최종적으로 여기에 보이는 사용자 인증정보+Oauth 동의화면을 설정해야함)

 

 

 

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

 

 

 

 

 

 

 

OAuth 클라이언트 ID 만들기 전에 동의화면 구성을 하라고 한다.

 

 

 

 

 

 

 

 

앱 동의화면 구성 

- 여기서 사용자 지원 이메일은 

구글로그인 실패, API 사용량 등에 관한 안내메일을 받을 개발자메일이다.

 

 

 

 

내부는 내가 따로 등록한 구글계정만 허용

외부는 구글계정가진 누구나 허용.  

 

 

 

 

 

연락처 정보도 앱정보 사용자지원 이메일과 똑같이해주자.

 

 

 

 

다시 사용자 인증 정보의 OAuth 클라이언트 ID 만들기

 

 

 

 

애플리케이션 종류는 웹 애플리케이션

이름은 구글의 프로젝트 내부적으로 OAuth 클라이언트 구별용- 내 맘대로 

승인된 Javascript원본은 프론트엔드에서 직접 OAuth 요청할 때(react 등) 만 설정

승인된 리디렉션 URI는 카카오랑 동일.    application.yml의 값이랑 똑같이. 

이렇게 하고 만들기 누릅니다

 

 

 

 

 

 

저장 후 OAuth 클라이언트를 확인해보면 

클라이언트ID, 클라이언트 보안 비밀버호, redirect-uri가 보입니다. 

 

 

 

각각 yml의 client-id, client-secret, redirect-uri가 됩니다.

google: # ✅ 구글 OAuth2 추가
  client-id: ${GOOGLE_CLIENT_ID}
  client-secret: ${GOOGLE_CLIENT_SECRET}   #구글은 인가코드- access_token 과정에서 내부적으로 secret을 필요로 함.
  scope:
    - profile       #카카오는 profile안의 여러정보 다 따로 지정해줘야하지만 구글은 profile만 지정하면 됨.
    - email
  authorization-grant-type: authorization_code
  redirect-uri: http://localhost:8080/login/oauth2/code/google

 

 

 

 

 

데이터 엑세스로 가서 범위(가져올 유저정보)를 선택합니다. 

scope의 profile, email에 맞게 선택합니다. 

 

 

 

나머지 설정인 authorization : authorization_code는 따로 설정하지는 않습니다.

다만 지금까지 한 구글 OAuth 설정들은  authorization_code방식에 따른 설정방식이었습니다.

 

 

 

 

 

OAuth2로그인처리

기존 OAuth2 로그인처리는 카카오만 처리하도록 되어 있었다.

이제는 Oauth2의 리소스서버로부터 받아온 데이터의 

attributes 형태가 구글,카카오 등에 따라 달라져서 다음과 같이 처리해야한다.

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = false)
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);//  user-info-uri 에서 유저정보를 가져옴.
        String registrationId = userRequest.getClientRegistration().getRegistrationId(); //"google","kakao"
        Map<String, Object> attributes = oAuth2User.getAttributes();

       //registirationId는 "kakao","google" 등
       // attributes가  카카오냐 구글에 따라 형태가 다르다.
       
       if( registrationId.equals("kakao")){
       
       }else if(registrationId.equals("google")){
       }
      

    }
}

 

 구글
{
    "sub": "112233445566778899001",
    "name": "John Doe",
    "given_name": "John",
    "family_name": "Doe",
    "picture": "https://lh3.googleusercontent.com/a-/AOh14Gh...",
    "email": "johndoe@gmail.com",
    "email_verified": true,
    "locale": "en"
}


 카카오
 {
     "id": 1234567890,  문자아님 Long
     "kakao_account": {
         "email": "user@example.com",
         "profile": {
             "nickname": "홍길동"
         }
     }
 }

 

 

 

여기서 각각 구글, 카카오 형태에 맞게 attributes를 처리해도되지만 

카카오 구글 뿐만아니라 여러 OAuth2 가 추가되면  코드가 지저분 해지기 때문에

각 Oauth2에 맞는 ENUM을 만들어 각 ENUM에 처리를 위임한다.

 

 

OAuthProvider

public enum OAuthProvider {
    GOOGLE("google") {
        @Override
        public UserEntity toUserEntity(Map<String, Object> attributes) {
            UserEntity user = new UserEntity();
            user.setProvider(this.getRegistrationId());
            user.setUsername(this.getRegistrationId()+(String) attributes.get("sub"));  //google15432323
            user.setPassword("{noop}oauth2user");
            user.setEmail((String) attributes.get("email"));
            user.setNickname((String) attributes.get("name"));
            user.getRoles().add("USER");
            return user;
        }
    },
    KAKAO("kakao") {
        @Override
        public UserEntity toUserEntity(Map<String, Object> attributes) {
            Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
            Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");

            Long id= (Long) attributes.get("id");  //카카오는 id가 Long.

            UserEntity user = new UserEntity();
            user.setProvider(this.getRegistrationId());  //kakao
            user.setUsername(this.getRegistrationId()+id);  //kokao15432323
            user.setPassword("{noop}oauth2user");
            user.setEmail((String) kakaoAccount.get("email"));
            user.setNickname((String) properties.get("nickname"));
            user.getRoles().add("USER");
            return user;
        }
    };



    private final String registrationId;
    OAuthProvider(String registrationId) {
        this.registrationId = registrationId;
    }

    public String getRegistrationId() {
        return registrationId;
    }

    public static OAuthProvider from(String registrationId) {
        for (OAuthProvider provider : OAuthProvider.values()) {
            if (provider.getRegistrationId().equalsIgnoreCase(registrationId)) { //대소문자구분없이.
                return provider;
            }
        }
        throw new IllegalArgumentException("Unknown provider: " + registrationId);
    }
    public abstract UserEntity toUserEntity(Map<String, Object> attributes);

}

 

 

 

CustomOAuth2UserService

@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

    @Autowired
    private UserRepository userRepository;

    @Transactional(readOnly = false)
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);//  user-info-uri 에서 유저정보를 가져옴.
        String registrationId = userRequest.getClientRegistration().getRegistrationId(); //"google","kakao"
        Map<String, Object> attributes = oAuth2User.getAttributes();

        OAuthProvider currentLoginProvider=OAuthProvider.from(registrationId); //GOOGLE,KAKAO   ENUM
        UserEntity userEntity = currentLoginProvider.toUserEntity(attributes);  //oauth2사용자의 user정보

        userRepository.save(userEntity);  //유저정보 저장


        return new CustomUserAccount(userEntity, attributes);

    }
}

 

 

이러면 이제 각 provider에서 제공해주는 형태에 맞춰 userEntity를 생성해준다. 

우리는 이 userEntity를 사용자정보에 사용할 CustomUserAccount로 return 해주기만 하면 된다.

 

 

 

login.html

참고로 구글로그인버튼 이미지는 

https://developers.google.com/identity/branding-guidelines#custom-button에서 다운.

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

로그인페이지입니다.

<form action="/loginProc" method="post" name="loginForm">              <!--  securityConfig의  loginProcessingUrl("/loginProc")    -->
    <input id="username" type="text" name="username" placeholder="id(username)"/>   <!-- name이 반드시 username이어야한다.  -->
    <input id="password" type="password" name="password" placeholder="password"/>  <!-- name이 반드시 password이어야한다.  -->

    <input id="nickname" type="text" name="username" placeholder="nickname"/>
    <input id="email" type="password" name="password" placeholder="email"/>

    <button type="submit" >로그인</button>
</form>

<br>
<!-- 카카오 로그인 버튼 -->
<a href="/oauth2/authorization/kakao">
    <img src="/images/kakao_login.png" alt="카카오 로그인">
</a>

<br>
<a href="/oauth2/authorization/google">
    <img src="/images/google_login.png" alt="구글 로그인">
</a>






</body>
</html>

 

 

 

 

 

결과

 

 

 

 

 

 

구글로 로그인도 해보고 카카오로도 로그인 해보면 다음과같이

DB에 각 로그인에 맞는 회원정보가 저장된다.