OAuth2 로그인

강한친구·2022년 8월 10일
0

소셜로그인

요즘 사이트중에서 소셜로그인을 지원하지 않는 사이트는 거의 없다. 특히 구글의 경우 구현이 쉬워서인지 범용성이 좋아서인지 없는 사이트를 찾아보는게 더 힘들정도이다.

이런 소셜로그인 서비스를 구현할 때 사용하는 인증규악 프로토콜을 OAuth라 부른다.

OAuth의 간략한 역사

2006년 Twitter와 Google이 정의한 개방형 Authorization 표준이며, API 허가(Authorize)를 목적으로 JSON(JavaScript Object Notation) 형식으로 개발된 인증프로토콜이다.

다른 웹 서비스를 이용할 때 로그인 자격 증명과 개인정보 전송 없이도 접근 또는 권한부여를 하는 프로토콜이다.

기본적인 OAuth의 프름

리소스 서버는 백엔드서버이고, Authentication Server는 구글, 애플, 카카오같은 로그인을 처리해주는 사이트이다.

원리는 다음과 같다.

  1. 프론트에서 Authentication Server로 요청을 보낸다.
  2. 요청후 승인이 되면 AccessToken을 보낸다.
  3. AccessToken을 가진상태로 BackEnd에 보안절차가 필요한 URL에 대한 요청을 보낸다
  4. 백엔드는 이 토큰을 검증하고
    4-1 유효하면 요청을 수행하고
    4-2 유효하지 않으면 에러를 낸다
  5. 로그인 상태를 유지하는 방법에는 여러가지가 있지만 RefreshToken같은 방법이 쓰인다.

물론 이는 가장 기본적인 방법으로 더 다양한 로그인 인증방식이 있다.

백엔드가 로그인과 인증을 전부 전담하는 방식, 둘다 인증을 하는 방식, 프론트가 인증하고 백엔드는 토큰만 발행하는 방식등 다양하다

링크

코드로 보는 OAuth2

가장 기본적인 웹 Oauth2 인증방식을 구현해보았다.
한가지 유의할점은 Google의 경우 자주 사용해서 스프링에 이미 콜백 주소, 인증 주소가 다 저장되어 있어서 간단하게 세팅해도 되지만, 카카오, 네이버같은 경우는 조금 더 복잡하다.

yml

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: 154711632031-e6a1lsonnrt68haimdnlou81n254rdug.apps.googleusercontent.com
            client-secret: 시크릿키가 나옴 
            scope:
              - email
              - profile

이렇게 구글 API에서 받아온 ID와 시크릿을 넣어서 tml 파일을 세팅하면 된다.

클라이언트 ID, Secret 발급 방법은 이 글을 참고하자

Security Config

이전에 하던것처럼, Security를 세팅해주면 끝이다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable(); // csrf 비활성화
//        http
//                .formLogin().disable()
//                .httpBasic().disable();
        http
                .authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/manger/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_MANAGER')")
                .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
                .anyRequest().authenticated()
                .and()
                .logout()
                .clearAuthentication(true)
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .and()
                .sessionManagement()
                .maximumSessions(1);
        http
                .formLogin()
                .loginProcessingUrl("/loginProc")
                .and()
                .oauth2Login()
                .successHandler(new OAuthSuccessHandler(memberRepository))
                .userInfoEndpoint()// 후처리 시작
                .userService(principalOAuth2UserService); // 서비스에서 후처리함


    }

예제코드에서는 폼로그인과 OAuth2가 모두 활성화 되어있지만, 불편하면 form쪽을 disable해도 된다.

OAuth2 Principal Details

SuccessHAndler는 로그인 성공 시 후처리할 핸들러를 담고 있다.

UserInfoEndPoint는 로그인 정보 전송 후 후처리를 시작하는 부분이다. 여기서 UserSerevice메서드를 통해 Principal Service를 지정해주면, 폼 로그인과 동일한 방식으로 흘러가게 된다.

package Focus_Zandi.version1.web.config.auth;

import Focus_Zandi.version1.domain.Member;
import Focus_Zandi.version1.domain.MemberDetails;
import Focus_Zandi.version1.web.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
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
@RequiredArgsConstructor
public class PrincipalOAuth2UserService extends DefaultOAuth2UserService {

    //후처리함수
    //구글로부터 받은 userRequest에 대한 후처리

    private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    private final MemberRepository memberRepository;

    //구글로그인 버튼 클릭하면 -> 구글로그인 창 -> 로그인 완료 -> code return (OAuth 라이브러리가 받음) -> AccessToken 요청
    //userRequest wjdqh -> 회원프로필을 받음(loadUser함수) -> 구글로부터 회원 프로필을 받아옴
    @Override
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
        OAuth2User oAuth2User = super.loadUser(userRequest);

        //회원가입용 정보
        Member userEntity = createUser(userRequest, oAuth2User);

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

    //자동 회원가입 진행 로직
    private Member createUser(OAuth2UserRequest userRequest, OAuth2User oAuth2User) {
        String provider = userRequest.getClientRegistration().getClientId();
        String providerId = oAuth2User.getAttribute("sub");
        String name = oAuth2User.getAttribute("name");
        String username = provider + "_" + providerId;
        String password = passwordEncoder.encode("CommonPassword");
        String email = oAuth2User.getAttribute("email");
        MemberDetails memberDetails = new MemberDetails();

        Member memberEntity = memberRepository.findByUsername(username);
        if(memberEntity == null) {
            System.out.println("최초 로그인");
            memberEntity = Member.builder()
                    .username(username)
                    .userToken(providerId)
                    .password(password)
                    .name(name)
                    .email(email)
                    .memberDetails(memberDetails)
                    .build();
            memberRepository.save(memberEntity);
        } else {
            System.out.println("이미 가입된 사용자");
        }
        return memberEntity;
    }
}

흐름

  1. 구글로그인 버튼 클릭

  2. 구글로그인 창 나오고 로그인 실행

  3. 구글 Authorization Server에서 Code를 반환

  4. Oauth 라이브러리가 이를 받아서 AccessToken을 요청

  5. 회원 프로필을 받아옴 (미리 지정한 범위내의 프로필을 받음)

  6. loadUser가 호출되면서 유저를 찾아오고 회원가입 혹은 가입 생략을 진행

  7. 가입 혹은 로그인 완료 후 memberEntity를 PrincipalDetails에 담아서 반환함

  8. 이를 세션에 담아서 사용자 인증 완료

하지만 이건 안된다

API로 SPA랑 통신할떄, 즉 안드로이드, IOS, REACT 같은 어플리케이션 일단 프론트와 백이 분리된 상태로 작동한다. 따라서 이렇게 백에서 모든걸 전담하는 방식으로는 제대로 작동하지 않는다.

위에서 설명한것처럼 OAuth 처리방식은 여러가지가 있는데
첫번째 서버쪽에서 처리하는 방식은 Code 방식이라고 하고

두번째 클라이언트쪽에서 처리하는 방식을 Credential 방식이라고 한다. 따라서 이럴때는 클라이언트 처리방식은 Credential 방식을 사용해야한다.

즉, 클라이언트가 인증서버에서 인증까지 다 받아오고 백엔드는 그 정보를 받아서 jwt만 발급해주면 된다.

참고1
참고2

이렇게 해야하는걸 모르고 단순히 IOS에서 Web으로 리다이렉트해서 거기서 하면 안되는건가? 라고 생각하고 그쪽으로만 계속 접근을 해서 프로젝트에서 제대로 사용하지 못했었다.

그래서 어떤 방식인가

간단하게 정리하자면
프론트에서 인증, 인가를 받아 사용자 정보를 받아오고,

백엔드에서는 단순히 /join 을 통해 프론트에서 넘겨준 데이터를 저장하고 JWT 발급기로서의 역할만 하면 되는것이다.

따라서 JWT 사용법을 학습해야한다.

0개의 댓글