공부기록/Spring Security

[실습] 스프링 시큐리티 OAuth2 Login 1

jhs0129 2023. 2. 21. 12:00
320x100
반응형

목차

사전작업

Dependency 추가

dependencies {
    // 의존성 추가
    implementation 'org.springframework.security:spring-security-oauth2-client'
}

OAuth 서비스 등록

google, naver, kakao, facebook 등 여러개가 있지만 naver를 기준으로 설명을 할 예정이다

네이버 서비스 등록

위 링크에서 사용할 애플리케이션 등록을 해야한다

  1. 애플리케이션 이름, 필요정보 등록

제공정보 선택에서는 DB에 저장하기 원하는 정보를 선택하면 된다 지금은 이름과 이메일만 저장을 할 예정이기에 두개만 선택을 하였다

  1. 서비스 주소 추가

첫번째 서비스 URL은 내가 운영중인 서비스 정보이다 현재는 local에서 돌리기에 위와 같은 주소지만 AWS와 같이 서버를 띄운다면 해당 주소를 작성하면 된다

두번째는 로그인 후 인가 코드를 받아올 URL이다 우선은 이렇게 설정 후 추후 자세히 알아보자

  1. Client Id, Client Secret

등록을 마무리하게 되면 두가지 정보를 알 수 있는데 뒤에서 사용해야 하기에 따로 적어두기로 하자

application-auth.yml

네이버 로그인 Docs

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 
            client-secret: 
            scope: email, profile
          naver:
            client-id: 
            client-secret: 
            client-name: naver
            authorization-grant-type: authorization_code
            redirect-uri: http://localhost:8080/login/oauth2/code/naver
        provider:
          naver:
            authorization-uri: https://nid.naver.com/oauth2.0/authorize
            token-uri: https://nid.naver.com/oauth2.0/token
            user-info-uri: https://openapi.naver.com/v1/nid/me
            user-name-attribute: response 
            # 출력결과를 보면 정보는 response하위로 나타나는 것을 볼수 있다

docs를 보게되면 4개의 링크를 볼수 있게 된다 위 yml 파일과 비교해서 보면 아래와 같다

  • 네이버 로그인 연동 URL 생성하기 (authorization-uri)
  • 네이버 로그인 연동 결과 Callback 정보 (redirect-uri)
  • 접근 토큰 발급 요청 (token-uri)
  • 접근 토큰을 이용하여 프로필 API 호출하기 (user-info-uri)

client-id/secret은 등록 시 발급받은 정보를 넣으면 된다

나머지 정보는 동일하게 작성하자

위 파일에 구글관련해서도 작성을 해두었는데 구글은 네이버와 달리 Spring Security에서 Provider를 제공하기 때문에 registration만 작성을 해주면 된다

Spring Security는 구글 외에도 깃헙, 페이스북, 옥타를 추가로 제공한다 - 이 부분에 대해서는 추후 자세히 알아볼 예정이다

(org.springframework.security.config.oauth2.client#CommonOAuth2Provider)

  • 추가 구글로그인여기서 구글에 대한 정보를 등록하면 된다 네이버와 같이 애플리케이션 등록 및 정보를 작성하면 된다

이로써 사전 작업은 모두 끝났다

설정작업을 하고 로그인한 사용자에 대해서 DB에 저장하는 로직을 작성해보자

Security Config

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    //추가
    http.oauth2Login() //OAuth2 Login Config 시작
            .userInfoEndpoint() // OAuth2 Login 성공시 사용자 정보 가져올 설정
            .userService(customOauth2UserService); // OAuth2 Login 성공시 후 작업
    return http.build();
}

OAuthService

우선 우리가 만든 CustomUser에 OAuth2User도 관리할 수 있게 추가를 해주자

public class CustomUser implements UserDetails, OAuth2User {

    private final User user;
    //추가
    private OAuth2UserInfo oauth2Oauth2UserInfo;

    public CustomUser(User user) {
        this.user = user;
    }

    //추가
    public CustomUser(User user, OAuth2UserInfo oauth2Oauth2UserInfo) {
        this.user = user;
        this.oauth2Oauth2UserInfo = oauth2Oauth2UserInfo;
    }

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

    @Override
    public String getName() {
        return oauth2Oauth2UserInfo.getName();
    }
}
@Component
@RequiredArgsConstructor
public class CustomOauth2UserService extends DefaultOAuth2UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private static final String PASSWORD = "password";

    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        String registrationId = userRequest.getClientRegistration().getRegistrationId();

        // userRequest를 통해 사용자 정보 가져오기
        OAuth2UserInfo oAuth2UserInfo = OAuthAttributes.of(registrationId, super.loadUser(userRequest).getAttributes());
        User user = saveUser(oAuth2UserInfo);
        return new CustomUser(user, oAuth2UserInfo);
    }

    private User saveUser(OAuth2UserInfo oauth2Oauth2UserInfo) {
        String email = oauth2Oauth2UserInfo.getEmail();
        String name = oauth2Oauth2UserInfo.getName();
        String password = passwordEncoder.encode(PASSWORD + UUID.randomUUID().toString().substring(0, 8));

        User user = new User(email, name, password);
        // 유저정보가 없으면 저장 있으면 혹시라도 변경된 정보 업데이트
        userRepository.findUserByEmail(email)
                .ifPresentOrElse(
                        entity -> entity.update(user.getEmail(), user.getName()),
                        () -> userRepository.save(user)
                );
        return user;
    }
}

아래 코드들은 로그인 한 유저 정보 매핑을 위한 클래스들이다

@Getter
@RequiredArgsConstructor
public class OAuth2UserInfo {
    private final Map<String, Object> attributes;
    private final String providerId;
    private final String name;
    private final String email;
}
public enum OAuthAttributes {
    GOOGLE("google", attributes -> new OAuth2UserInfo(
            attributes,
            attributes.get("sub").toString(),
            attributes.get("name").toString(),
            attributes.get("email").toString()
    )),

    NAVER("naver", attributes -> {
        Map<String, Object> response = (Map<String, Object>) attributes.get("response");
        return new OAuth2UserInfo(
                response,
                response.get("id").toString(),
                response.get("name").toString(),
                response.get("email").toString()
        );
    }),
    ;

    private final String registrationId;
    private final Function<Map<String, Object>, OAuth2UserInfo> of;

    OAuthAttributes(String registrationId, Function<Map<String, Object>, OAuth2UserInfo> of) {
        this.registrationId = registrationId;
        this.of = of;
    }

    public static OAuth2UserInfo of(String providerId, Map<String, Object> attributes) {
        return Arrays.stream(values())
                .filter(provider -> provider.registrationId.equals(providerId))
                .findFirst()
                .orElseThrow(IllegalArgumentException::new)
                .of.apply(attributes);
    }
}

이것으로 OAuth2로그인 까지 알아보았다

하지만 이것만 하기에는 앞서 JWT 발행 후 Front로 전달하는 과정이 OAuth Login에는 없어 Front와 통신간에 문제가 생긴다

다음 포스트에서는 OAuth Login 후 JWT 발행에 대해서 알아보자

320x100
반응형