OAuth2 session 방식으로 구현 해보기( Google, Naver)

조대훈·2024년 3월 27일
0

스프링 시큐리티

목록 보기
2/4
post-thumbnail

0. OAuth란 ?

Ouath란 Open Authorization 으로 인터넷 사용자들이 비밀번호를 제공하지 않고 다른 웹 사이트 상의 자신들의 정보에 대해 웹 사이트나 애플리케이션의 접근 권한을 부여할 수 있는 공통적인 수단으로서 사용되는 접근 위임을 위한 개방형 표준이다.

개방형 표준 ? 누구나 자유록ㅂ게 수정할 수 있는 기술 표준. 특정 기업이나 단체에 의해 독점되지 않고
공개적으로 문서화 되어있다.

신뢰성 있는 서비스에서 로그인 하여 인증을 받고, 그 권한을 읻가 받아 세션을 생성하는 스프링 시큐리티 OAuth2 소셜 로그인 프로젝트 입니다.

0.1 OAuth2.0 용어

Resource Server

OAuth2.0 서비스를 ㅈ공하고 Resource를 관리하는 서버
클라이언트는 이 서버로 하여급 인증 서버에서 발급받은 Token을 넘겨 개인 정보를 받을 수 있다.

ResourceOwner

어플리케이션을 이용하려는 Resource Server의 계정을 소유하고 있는 사용자

Client

Resource Server API를 사용하여 정보를 가져오려는 애플리케이션 서버

AuthorziationServer

Client 가 Resource Server 서비스를 사용할 수 있게 인증하고, 토큰을 발행 해주는 서버
사용자 : 서버로 ID PW 를 넘기면 Authorization Code 를 발급 받음
클라이언트 : 사용자가 발급 받은 AUthorization Code를 Token 을 발급 받음

AccessToken

JWT 의 AcessToken 해당 토큰으로 Resource Server에 요청해서 개인정보를 받을 수 있다.

Refresh Token

JWT RefreshToken 해당 토큰으로 AcessToken 을 재발급 받을 수 있다.


실습 예제 디렉토리 구조

  • config
    - SecurityConfig
  • controller
    - MainController
    - MyController
  • dto
    - CustomOAuth2User
    - GoogleResponse
    - NaverResponse
  • entity
    - UserEntity
  • Reporsitory
    - UserRepository
  • service
    - CustomOAuth2UserService

1. 실습 목표 및 간단한 동작 원리

실습 목표

OAuth2.0 클라이언트와 스프링 시큐리티6 프레임워크를 활용하여 신뢰할 수 있는 외부 사이트 ( 구글, 네이버 ) 부터 인증을 받고 전달 받은 유저 데이터를 활용하여 세션을 만들고, 인가를 진행하는 방법.
인증 받은 데이터는 MySQL 데이터베이스르 활용하여 저장하고 관리 한다.

구현

  • 인증 : 네이버 소셜 로그인, 구글 소셜 로그인 ( 코드 방식 )
  • 인가 : 세션 방식을 통한 경로별 접근 권한
  • 인증 정보 DB 저장 후 추가 정보 기입

OAuth2.0 인증 방식 시큐리티 동작 원리

  • 인증 서버와 자원 서버 및 우리의 서버(코드 방식)

아래의 네모가 스프링 서버고, 위의 네모가 신뢰할 수 있는 서버 ( 구글, 네이버 ) 서비스다.
1. 처음에 사용자로부터 로그인 요청이 오면 스프링 부트 서버에서 로그인 요청을 진행하게 된다.
2. 로그인 페이지를 리디렉션하는 필터가 요청을 받아 인증 서버 ( 구글, 네이버 ) 리디렉션 창을 띄운다.
3. 로그인이 성공되면, 인증 서버에서 시큐리티 인증 필터 쪽으로 코드를 발급 해준다.
4. 발급받게된 코드를 가지고 시큐리티 인증 필터를 가지고 있고
5. 해당 코드를 다시 네이버 인증 서버로 보내게 되는데 액세스 토큰 을 발급받기 위해서다.
6. 전송을 해서, 코드와 시큐리티 키, 아이디를 모두 검증을 마치면 다시 액세스 토큰 을 시큐리티 인증 필터로 보내준다.
7. 시큐리티 인증 필터는 액세스 토큰을 가지고 서비스 리소스 서버에 액세스 토큰을 보내면, 그것에 대해서 검증을 마친 이후에 다시 로그인한 유저 데이터 를 발급 해준다.
8. 발급받은 유저 데이터를 OAuth2 라는UserDetailService 가 있는데 세세션 및 DB를 저장한다.

기타

  • OAuth2.0 코드 인증 방식 활용
  • 인증 후 발급된 정보로 세션을 반들고 SSR 방식으로 모든 페이지를 응답한다
  • 소셜 로그인을 통해 인증 받은 데이터는 우리의 서비스 데이터베이스에 저장을 한 뒤 관리를 진행해야 한다. 관리를 하지 않고 인증만 받고 사용할 수 있지만 추가적인 사용자 정보나 어떤한 사용자가 우리의 서비스를 활용하는지 확인하기 위해서는 무조건 관리하는 것을 추천한다.
  • 구현적인 부분과 간단한 동작 로직을 먼저 작성한 뒤 내부 동작 로직을 학습할 예정이다.

2. OAuth2.0 동작 원리

OAuth 동작 모식도

  1. /oauth2/authorization/서비스명 클라이언트가 서버로 로그인 요청
  2. 어플리케이션 서블릿 컨테이너(톰캣)에서 OAuth2AuthorizationRequestRedirectFilter 가 동작
  3. 등록된 외부 소셜 로그인 인증 서버로 전달 (리다이렉션)
  4. 외부 소셜 로그인 인증 서버는 해당 서비스 로그인 페이지로 응답
  5. 사용자는 네이버 , 구글 로그인 진행
  6. 성공시 인증 서버에 등록된 우리 서버의 주소로 (리다이렉션)
  7. 성공 메세지와 함께 서비스 인증 서버에서 로그인 성공 Code를 날려준다 /login/oauth2/code/서비스명
  8. OAuth2LoginAuthenticationFilter 에서 해당 요청을 받게 되는데
  9. OAuth2LoginAuthenticationProvider 로 코드를 넘겨주게 된다.
  10. 해당 프로바이더는 액세스 토큰을 받기 위해 다시 인증 서버로 코드와 특정 정보를 보낸다.
  11. 액세스 토큰을 발급받게 되면, 다시 외부 소셜 로그인 서비스 리소스 서버로 사용자 유저정보를 받기 위해 토큰을 다시 보낸다
  12. 리소스 서버는 해당 토큰을 검증한 뒤, 요청한 유저 정보를 포함해 다시 반환하게 된다.
  13. 커스텀유저디테일 서비스에 해당하는 OUath2UserDetailServiceOAuth2UserDetails 에 보낼 정보를 담아 넘겨준다.
  14. 세션 저장, DB 저장 과 같은 나머지 시큐리티 로직 동작

각 필터가 동작하는 주소

기능 하는 필터에 따라 반드시 관례에 따라 주소를 기입 해야 한다.

OAuth2AuthorizationRequestRedirectFilter
처음 로그인 할때 리디렉션 하는 필터

/oauth2/authorization/서비스명

/oauth2/authorization/naver
/oauth2/authorization/google

OAuth2LoginAuthenticationFilter
로그인 성공 후 코드 및 액세스 토큰 발급해주는 필터

/login/oauth2/code/서비스명

/login/oauth2/code/naver
/login/oauth2/code/goggle

위에 나온 모식도를 전부 직접 구현 해야 할까?

변수 처리만 해주면 OAuth2.0 이 내부적으로 처리를 해준다. 전부 다 직접 구현 할 필요 없다.

OAuth2 인증 및 동작을 위한 변수들

변수 설정만 진행 하면 OAuth2AuthorizationRequestRedirectFilter ->OAuth2LoginAuthenticationFilter -> OAuth2LoginAuthenticationProvider -> 과정을 추가 하지 않아도 자동으로 진행 한다. 따라서 사용자는

UserDetailServiceUserDetails 만 구현하면 된다.


3.OAuth2 변수 역할

OAuth2 소셜 로그인을 위한 변수 설정

application.properties

#registration
spring.security.oauth2.client.registration.서비스명.client-name=서비스명
spring.security.oauth2.client.registration.서비스명.client-id=서비스에서 발급 받은 아이디
spring.security.oauth2.client.registration.서비스명.client-secret=서비스에서 발급 받은 비밀번호
spring.security.oauth2.client.registration.서비스명.redirect-uri=서비스에 등록한 우리쪽 로그인 성공 URI
spring.security.oauth2.client.registration.서비스명.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.서비스명.scope=리소스 서버에서 가져올 데이터 범위

#provider
spring.security.oauth2.client.provider.서비스명.authorization-uri=서비스 로그인 창 주소
spring.security.oauth2.client.provider.서비스명.token-uri=토큰 발급 서버 주소
spring.security.oauth2.client.provider.서비스명.user-info-uri=사용자 정보 획득 주소
spring.security.oauth2.client.provider.서비스명.user-name-attribute=응답 데이터 변수

예시


#registration
spring.security.oauth2.client.registration.naver.client-name=naver
spring.security.oauth2.client.registration.naver.client-id=발급아이디
spring.security.oauth2.client.registration.naver.client-secret=발급비밀번호
spring.security.oauth2.client.registration.naver.redirect-uri=http://localhost:8080/login/oauth2/code/naver
spring.security.oauth2.client.registration.naver.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.naver.scope=name,email

#provider
spring.security.oauth2.client.provider.naver.authorization-uri=https://nid.naver.com/oauth2.0/authorize
spring.security.oauth2.client.provider.naver.token-uri=https://nid.naver.com/oauth2.0/token
spring.security.oauth2.client.provider.naver.user-info-uri=https://openapi.naver.com/v1/nid/me
spring.security.oauth2.client.provider.naver.user-name-attribute=response

​```

**registration 과 provider**

>registration 은 외부 서비스에서 우리 서비스를 특정하기 위해 등록하는 정보여서 등록이 필수적이다.
>하지만 provider의 경우 서비스별로 정해진 값이 존재하며 OAuth2 클라이언트 의존성이 유명한 서비스의 경우 내부적으로 데이터를 이미 가지고 있다. (구글, 깃허브, 페이스북 등)
>*네이버 등은 등록을 해주어야 한다는 말*


***

# 4.SecurityConfig 등록


```java

@Configuration  
@EnableWebSecurity  
public class SecurityConfig {  

private final CustomOAuth2UserService customOAuth2UserService;
  
    @Bean  
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {  
  
        http  
                .csrf((csrf) -> csrf.disable())  
                .formLogin((login) -> login.disable())  
                .httpBasic((basic) -> basic.disable())  
                .oauth2Login((oauth2)->oauth2
                .userInfoEndPoint(
                userInfoEndPointConfig -> 
                userInfoEndpointConifg.userService(customOAuth2UserService)
                ))
                 
                 //oauth2Client를 추가 하게 되면 세부 로그인 로직들을 구현 해야 한다.  
                .authorizeHttpRequests((auth) -> auth  
                        .requestMatchers("/").permitAll()  
                        .anyRequest().authenticated());  
        return http.build();  
    }  
  
}

5. 네이버 소셜 로그인 신청

  1. 네이버 개발자 센터에 API 등록 후
  2. 서비스 환경 URL, 콜백 URL 설정
  3. http://localhost:8080/login 에 접속
  4. 시큐리티 자체에서 구현 된 로그인 화면 확인 후 접속
  5. 해당 로그인 화면 확인 가능




UserDetailServiceUserDetail 을 구현해놓지 않아서 이 다음 진행은 불가하다.


6.구글 소셜 로그인 신청

  1. 사용자 인증 정보 탭에 사용자 인증정보 만들기 추가 후
  2. 리디렉션 URL, 가져올 정보 등을 설정한다.
  3. application.properties에 해당 값들을 기입 한다.
  4. 아래 화면을 확인할 수 있다.
  5. 네이버와 마찬가지로 UserDetailServiceUserDetail 을 구현하지 않아서 이 다음의 진행은 불가하다.

#registration  
spring.security.oauth2.client.registration.google.client-name=google  
spring.security.oauth2.client.registration.google.client-id= 
spring.security.oauth2.client.registration.google.client-secret=
spring.security.oauth2.client.registration.google.redirect-uri=http://localhost:8080/login/oauth2/code/google  
spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code  
spring.security.oauth2.client.registration.google.scope=profile,email



7.OAuth2UserService 구현

CustomOAuth2UserService


@Service
public CustomOAuth2UserService extends DefaultOAuth2UserService{

	@Override
	public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{

	// OAuth2 제공 업체로부터 사용자 정보를 얻어오는 과정
	OAuth2USer oAuth2User = super.loadUser(userRequest);

	String registrationId = userRequest.getClientRegistration().getRegistrationId();

OAuth2Response oAuth2Response =null;

	if(registrationId.equals("naver")){{
	
		oAuth2Response = new NaverResponse(oAuth2User.getAttribute());
		}
	else if ( registrationId.equals("google")){
			oAuth2Response = 
			new GoogleResponse(oAuth2User.getAttribute());
		}else{
		return null;
		
		}
		return super.loadUser(userRequest);
		}
	
	}
}

OAuth2Response

public interface OAuth2Response{


String getProvider();
String getProviderId();
String getEmail();
String getName();

}

public class GoogleResponse implements OAuth2Response{

	private final Map<String, Object> attribute;

	public GoogleResponse(Map<String,Object>attribute){
		this.attribute=attribute;
	}

	@Override
	public String getProvider(){
		return "google";
		
	}

	@Override
	public String getEmail(){
		return attribute.get("email").toString();
	}

	@Override
	public String getName(){
		return attribute.get("name").toString();
	}
}

public class NaverResponse implemnets OAuth2Response{

	private final Map<String,Object> attribute;

	public NaverResponse(Map<String,Object> attribute){
		this.attribute=attribute;
	}

	@Override  
	public String getProviderId() {  
	    return attribute.get("id").toString();  
	}  
	  
	@Override  
	public String getEmail() {  
	    return attribute.get("email").toString();  
	}  
	  
	@Override  
	public String getName() {  
	    return attribute.get("name").toString();  
	}
}

8. 응답 데이터로 로그인 완료

CustomOAuth2User

@RequiredArgsConstructor  
public class CustomOAuth2User implements OAuth2User {  
  
  
    private final OAuth2Response oAuth2Response;  
    private final String role;  
  
  
  
    @Override  
    public <A> A getAttribute(String name) {  
        return OAuth2User.super.getAttribute(name);  
    }  
  
    @Override  
    public Map<String, Object> getAttributes() {  
        return null;  
    }  
  
    @Override  
    public Collection<? extends GrantedAuthority> getAuthorities() {  
  
        Collection<GrantedAuthority> collection = new ArrayList<>();  
        collection.add(new GrantedAuthority() {  
            @Override  
            public String getAuthority() {  
                return role;  
            }  
        });  
        return collection;  
    }  
  
    @Override  
    public String getName() {  
        return oAuth2Response.getName();  
    }  
  
    public String username() {  
        return oAuth2Response.getProvider() + " " + oAuth2Response.getProviderId();  
    }  
}

권한을 가져오는 부분은 GrantedAuthority 를 컬렉션 리스트로 만들어서 add하면 되는데
GrantedAuthority 를 새로 초기화 하면서 Override 해줘야 한다. return 값은 role 로 준다

username 메서드는 OAuth2User 인터페이스에서 기본 제공 하는 메서드가 아닌데 이유는 프로바이더와 프로바이더 아이디를 구분하기 위함이다. 가령 네이버 + 아이디, 구글 + 아이디 이런 식으로.


9.유저 정보 DB 저장

어떤 사람이 로그인 했는지, 저장하기 위해 DB 사용이 필수적이다.

모식도를 간단히 살펴 보면 리디렉션 후 로그인에 성공하면 로그인 필터를 거쳐 OAuth2LoginAuthenticationProvider 에 도달 하면 다시 인증 서버에 먼저 발급받은 코드 및 등록 정보를 전달하고 인증 서버는 엑세스 토큰을 발급 해준다. 이 액세스 토큰을 리소스 서버에 전달 하면 리소스 서버는 유저 정보를 전달 해주는데 OAuth2UserDetails 에 담아 OAuth2UserDetailService 에 전달한다. 이제 이 서비스에서 세션 저장과 같은 나머지 시큐리티 로직이 동작한다.

이제 세션,DB 저장 로직을 구현해볼 차례이다.

먼저 소셜 로그인 서비스에서 보내준 Provider를 가지고 이미 DB에 저장된 회원인지 조회를 해야 한다.

  1. 없으면 신규 저장
  2. 있으면 업데이트

DB 저장을 위해 유저 래퍼지 토리, 유저 엔티티, 커스텀 오쓰 유저서비스 단 하단에 저장하는 로직 추가가 필요하다.

유저 레퍼지토리와, 유저 엔티티는 생략

CustomOAuth2UserService


@Service  
@RequiredArgsConstructor  
public class CustomOAuth2UserService extends DefaultOAuth2UserService{  
  
//    타 블로그 글을 보면 OAuth2UserService를 상속 받거나 직접 구현 하는 경우가 있는데  
//    DefaultOAuth2UserService는 구현체이기에 이대로 진행 해도무관하다  
  
    //DB 저장을 진행 하기 위해 유저 래퍼지토리 주입  
    private final UserRepository userRepository;  
  
    @Override  
    public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {  
  
        //부모 클래스 loadUser로 부터 유저 정보를 가지고 오는 메서드 ( OAuth2 공급업체로 부터 사용자 정보를 가져오는 것 )        OAuth2User oAuth2User = super.loadUser(userRequest);  
        System.out.println(oAuth2User.getAttributes());  
  
        String registrationId = userRequest.getClientRegistration().getRegistrationId();  
  
  
        OAuth2Response oAuth2Response = null;  
  
        if (registrationId.equals("naver")) {  
  
            oAuth2Response = new NaverResponse(oAuth2User.getAttributes());  
  
        } else if (registrationId.equals("google")) {  
            oAuth2Response = new GoggleResponse(oAuth2User.getAttributes());  
  
        }else{  
            return null;  
        }
       
// 구글과 네이버 서비스마다 인증 규격이 상이하기 때문에 서로 다른 DTO로 담아야 한다.  
// 따라서 OAuth2 DTO 객체 격인 OAuth2Response 객체를 인터페이스로 만든다.  
// 네이버로 인터페이스를 구현, 구글 타입으로 인터페이스를 구현하는 식으로 진행한다.

	String username = oAuth2Response.getProvider()+ " "+oAuth2Response.getProviderId();

	UserEntity existData = userRepository.findByUsername(username);

	String role =null;

	if(existData ==null){

		UserEntity userEntity = new UserEntity();
		userEntity.setUsername(username);
		userEntity.setRole(oAuth2Response.getEmail());
		userEntity.setEmail("ROLE_USER");

		userRepository.save(userEneity);
		
	}else{
	
	role = existData.getRole();
	existData.setEmail(oAuth2Response.getEmail());
	userRepository.save(existData);
	}

	reutrn new CustomerOAuth2User(oAuth2Response, role);
	}
}

총평

인증 코드 발급, 액세스 코드 발급, JWT에 비해 JWTUtil이나 TokenUtils 등을 구현하지 않고 변수 설정만 해주면 알아서 데이터를 넘겨 주고 받아오는데 생소한 메서드 들이 많아 더 연습이 필요해 보인다.. 서버사이드 렌더링을 써서 세션 기반을 썼지만 리액트를 쓸 경우 클라이언트 사이드 렌더링을 쓸 경우 토큰 기반의 로그인 방법도 추가 학습이 필요 해보인다.

profile
백엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글