스프링부트와 AWS로 혼자 구현하는 웹서비스

16. 구글 API OAuth - 프로젝트 서버 설정

Mary's log 2024. 10. 20. 22:57

 

 

src/main/resources  -  application-oauth.properties   생성   - 이 파일은 .gitignore에 파일명 추가해서 형상관리에 올리지 않음.

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

 

src/main/resources  -  application.properties   수정.

# jpa sql show setting
spring.jpa.show_sql=true

# mustache korean encoding
server.servlet.encoding.force-response=true

# h2 : create table - id setting
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL

spring.h2.console.enabled=true
spring.profiles.include=oauth

 

 

내부적으로 로그인 관련해서 다루기 위한 '사용자' 객체, '권한Role'  준비.

 

src/main/java/com/jojoldu/springboot/domain/userUser.java  -  Role 빨강은 그 다음에 만들거라 import 없이 일단 ㄱㄱ.

package com.jojoldu.springboot.domain.user;

import com.jojoldu.springboot.domain.posts.BaseTimeEntity;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;

@Getter
@NoArgsConstructor
@Entity
public class User extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String email;

    @Column
    private String picture;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private Role role;

    @Builder
    public User(String name, String email, String picture, Role role) {
        this.name = name;
        this.email = email;
        this.picture = picture;
        this.role = role;
    }

    public User update(String name, String picture) {
        this.name = name;
        this.picture = picture;
        return this;
    }

    public String getRoleKey() {
        return this.role.getKey();
    }
}

 

src/main/java/com/jojoldu/springboot/domain/user - enum  Role.java

Enum 값이 String 타입으로 저장하게 함. 기본은 int로 저장되지만 그냥 숫자로 저장되면 무슨 코드를 의미하는지 알 수 없음.

package com.jojoldu.springboot.domain.user;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Role {

    GUEST("ROLE_GURES", "손님"),
    USER("ROLE_USER", "일반 사용자");

    private final String key;
    private final String title;
}

 

src/main/java/com/jojoldu/springboot/domain/user -  interface UserRepository.java  

이미 생성된 사용자인지, 처음 가입하는 사용자인지 확인하기 위한 findByEmail 함수

package com.jojoldu.springboot.domain.user;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

 

 

 


 

 

build.gradle.kts에 스프링 시큐리티 추가

oauth2-client 추가 > 소셜로그인할 때 꼭 필요.

spring-security-oauth2-client,  spring-security-oauth2-jose 기본으로 관리해줌.

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("junit:junit:4.13.1")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("com.h2database:h2")
    implementation("org.springframework.boot:spring-boot-starter-mustache")
    implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
    compileOnly("org.projectlombok:lombok")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation(platform("org.junit:junit-bom:5.10.0"))
    testImplementation("org.junit.jupiter:junit-jupiter")
}

 

 

 

OAuth 관련해서 총 4개의 파일을 만들거임.

되도록이면, dto 먼저 만들고 Custion...과 Security 생성.

 - OAuthAttributes =  소셜 로그인 인증 속성 객체. OAuth2UserService를 통해 가져온 OAuth2User의 attributes 속성을 담음. 

                                     naver, kakao, apple도 인증되고 나면 이 객체 사용.

 - SessionUser = 세션에 담을 "인증된" 사용자 정보 객체 (그냥 User는 "인증된"게 아니고 @Entity? 라서? 별도로 사용)

 - CustomOAuth2UserService =  delegateloadUser해서 인증 속성을 가져옴.

                                                         클라이언트등록•등록ID 등을 saveOrUpdate(속성)함.

                                                          현재 세션에("user"에, (속성 담음))

 - SecurityConfig = 요청url에 따라서 접근 & 권한 설정.

config.auth
├── dto
│   ├── OAuthAttributes
│   └── SessionUser 
│
├── CustomOAuth2UserService
└── SecurityConfig

 

 

src/main/java/com/jojoldu/springboot/config/auth  - 경로 생성. 


src/main/java/com/jojoldu/springboot/config/auth/dto -  class OAuthAttributes.java    생성

package com.jojoldu.springboot.config.auth;

import com.jojoldu.springboot.config.auth.dto.OAuthAttributes;
import com.jojoldu.springboot.config.auth.dto.SessionUser;
import com.jojoldu.springboot.domain.user.User;
import com.jojoldu.springboot.domain.user.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpSession;
import java.util.Collections;

@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
    private final UserRepository userRepository;
    private final HttpSession httpSession;

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
        OAuth2User oAuth2User = delegate.loadUser(userRequest);

        String registrationId = userRequest.getClientRegistration().getRegistrationId(); // registrationId=google,apple,naver
        String userNameAttributeName = userRequest.getClientRegistration()
                                                  .getProviderDetails()
                                                  .getUserInfoEndpoint()
                                                  .getUserNameAttributeName(); // userNameAttributeName=pk같은 필드값.
                                                                               // google : sub (naver,kakao 없음)
        OAuthAttributes attributes = OAuthAttributes.of(registrationId,
                                                    userNameAttributeName,
                                                    oAuth2User.getAttributes());
        User user = saveOrUpdate(attributes);

        httpSession.setAttribute("user", new SessionUser(user));

        return new DefaultOAuth2User(Collections.singleton(new SimpleGrantedAuthority(user.getRoleKey())),
                                                                                attributes.getAttributes(),
                                                                                attributes.getNameAttributeKey());

    }

    private User saveOrUpdate(OAuthAttributes attributes) {
        User user = userRepository.findByEmail(attributes.getEmail())
                .map(entity -> entity.update(attributes.getName(), attributes.getPicture()))
                .orElse(attributes.toEntity());
        return userRepository.save(user);
    }

}

 


src/main/java/com/jojoldu/springboot/config/auth/dto -  class SessionUser.java    생성

package com.jojoldu.springboot.config.auth.dto;

import com.jojoldu.springboot.domain.user.User;
import lombok.Getter;

import java.io.Serializable;

@Getter
public class SessionUser implements Serializable {
    private String name;
    private String email;
    private String picture;

    public SessionUser(User user) {
        this.name = user.getName();
        this.email = user.getEmail();
        this.picture = user.getPicture();
    }
}

 

src/main/java/com/jojoldu/springboot/config/auth -  CustomOAuth2UserService.java

package com.jojoldu.springboot.config.auth.dto;

import com.jojoldu.springboot.domain.user.Role;
import com.jojoldu.springboot.domain.user.User;
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@Getter
public class OAuthAttributes {
    /** OAuth2UserService를 통해 가져온 OAuth2User의 attributes 속성을 담음. naver, kakao, apple도 사용  */
    
    private Map<String, Object> attributes;
    private String nameAttributeKey;
    private String name;
    private String email;
    private String picture;

    @Builder
    public OAuthAttributes(Map<String, Object> attributes, String nameAttributeKey, String name, String email, String picture) {
        this.attributes = attributes;
        this.nameAttributeKey = nameAttributeKey;
        this.name = name;
        this.email = email;
        this.picture = picture;
    }

    /** OAuth2User 리턴타입이 Map이라서 하나하나 변환. 다른 로그인이면 ofNaver와 같이 추가 */
    public static OAuthAttributes of(String registrationId, String userNameAttributeName, Map<String, Object> attributes) {
        return ofGoogle(userNameAttributeName, attributes);
    }

    private static OAuthAttributes ofGoogle(String userNameAttributeName, Map<String, Object> attributes) {
        return OAuthAttributes.builder()
                .name((String) attributes.get("name"))
                .email((String) attributes.get("email"))
                .picture((String) attributes.get("picture"))
                .attributes(attributes)
                .nameAttributeKey(userNameAttributeName)
                .build();
    }

    public User toEntity() {
        return User.builder()
                .name(name)
                .email(email)
                .picture(picture)
                .role(Role.GUEST)
                .build();
    }

}

 

src/main/java/com/jojoldu/springboot/config/auth  - class SecurityConfig  생성

package com.jojoldu.springboot.config.auth;

import com.jojoldu.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@RequiredArgsConstructor
@EnableWebSecurity // build.gradle에서 추가함으로서, Spring Security 라이브러리 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final CustomOAuth2UserService customOAuth2UserService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable().headers().frameOptions().disable() // h2-console 접근 못하게 막기
                .and().authorizeRequests() // URL별 권한 관리 설정 옵션 시작점. 이게 있어야 antMatchers 메소드 체이닝 가능.
                // .antMatchers = 권한 관리 대상 지정 옵션.
                // perimitAll = 전체 열람 권한, hasRole = 특정 권한 부여
                .antMatchers("/","/css/**","/images/**","/js/**","/h2-console/**").permitAll()
                .antMatchers("/api/vi/**").hasRole(Role.USER.name())
                .anyRequest().authenticated() // 그 외 나머지 요청URL 이라면 전부 인증된 사용자만 허용=로그인한 사용자.
                .and().logout().logoutSuccessUrl("/") // 로그아웃 시 이동할 주소
                .and().oauth2Login().userInfoEndpoint().userService(customOAuth2UserService);
    }
}

 

 

 

*** SecurityConfig.java의  WebSecurityConfigurerAdapter   < deprecated라서  오류인가 싶었음.

>> 나중에 구글 로그인, 개인정보제공 동의 까지 하고 나면 오류남. ㅠ 

>>>> 어..?  로그인하고나서 'http://localhost:8080/oauth/authorization/google' 여기로 가는건 문젠데

>>>> 그 상태에서 다시 'localhost:8080'로 가면 로그인 되어있음.. 뭐지? <  화면단 주소가 /oauth2/여야하는데 /oauth/ 오타였음.

아래 소스 말고 위 소스 deprecated 소스 그대로 써도 동작함.

package com.jojoldu.springboot.config.auth;

import com.jojoldu.springboot.domain.user.Role;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final CustomOAuth2UserService customOAuth2UserService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .headers().frameOptions().disable()
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers("/", "/css/**", "/images/**", "/js/**", "/h2-console/**", "/profile").permitAll()
                        .antMatchers("/api/vi/**").hasRole(Role.USER.name())
                        .anyRequest().authenticated())
                .logout(logout -> logout.logoutSuccessUrl("/"))
                .oauth2Login(oauth2Login -> oauth2Login.userInfoEndpoint().userService(customOAuth2UserService));

        return http.build();
    }
}

 

 


 

* 개인 메모

 

<오류나서 순서 어떻게 돌아가는지 디버깅으로 정리...>

클라 localhost:8080 요청

 

IndexController ("/")  - user = null

그냥 index.mustache 보여줌 'Google Login' 클릭

         - "oauth/authorization/google" 주소 호출(Google 쪽)

         - 로긴 & 개인정보제공 동의 

         - return 해줌.

 

CustomOAuth2UserService.loadUser 발동

     delegate.loadUser 가 구글에서 attribute 가져옴.