다양한 인증을 어떻게 쉽고 유연하게 제공할 수 있을까?

Sian·2021년 9월 18일
1

SpaceONE FrontEnd 개발

목록 보기
5/6
post-thumbnail

Google Oauth2, Keycloak, ID/PW 등 여러 인증 방식을 제공해야 하는 요구 사항이 들어온다면 어떻게 쉽게 해당 인증을 구현할 수 있을까? 이러한 다양한 인증 방식을 구현했던 경험에 대해 소개해보고자 한다.

인증 플로우

SpaceONE은 현재 도메인 설정에 따라 Google Oauth2, Keycloak, KB SSO 세 가지의 인증 중 하나를 선택하여 제공하고, 인증 플로우는 다음과 같다.

  1. 우선 해당 도메인에서 어떤 Sign-in 방식(ID/PW인지, 외부 인증을 제공하는 지, 제공한다면 어떤 인증방식인지)을 사용하는 지 체크한다.
  2. 그 후, 각각 다른 Sign-in 방식에 맞는 UI를 보여주고, 해당 템플릿에서 Authenticator를 상속받은 커스텀 인증 모듈의 메소드를 호출한다.
  3. 각각의 커스텀 Auth들은 각자 필요한 Sign-in 절차를 수행한 후, 상속받은 Authenticator의 기본 Sign-in 로직을 수행한다.

각 도메인 별로 다양한 SSO를 제공해야 하기도 했으며, 또 오픈소스로 개발하고 있기 때문에 누구나 SSO 플러그인을 쉽게 붙일 수 있는 구조가 필요하였다. 그래서 Authenticator라는 인증 클래스를 이용한 구조를 구현하였다.

이후 이루어지는 인증 플로우는 다음과 같다.

Authenticator

Authenticator는 다음과 같은 구조를 가진다.

최상위 Authenticator는 아주 기본적인 signIn과 signOut 메서드만 가지는 추상 클래스이다.

abstract class Authenticator {
	static async signIn(필요한 매개변수): Promise<void> {
    	// sign in 로직 
    }
    
    static async signOut(): Promise<void> {
    	// sign out 로직
    }
}

export {
	Authenticator,
}

signIn을 위해 백엔드 인증 서버에 보낼 credentials가 필수적이고, userId와 userType(일반 User인지, 관리자인지, API만 사용하는 API user인지)을 부수적으로 받는다.
signOut에서는 vuex에 작성해놓은 signOut 메서드를 호출한다.

커스텀 인증 구조

이제 기본적인 추상 클래스 작성이 끝났으니, 이 클래스를 상속받아 각 SSO에 맞는 auth 클래스들을 작성한다. 위의 구조도를 보면 알 수 있듯이, 한 auth를 구현하는 데에 필요한 것은 두 가지가 있다. 바로 폼 렌더링(SSO 버튼 등)을 위한 템플릿과, 메서드들을 구현한 module(.ts) 파일이다.

그렇게 각각을 구현하면 위와 같은 디렉터리 구조를 갖는다.

커스텀 인증 클래스 개발

// custom-auth.ts

class CustomAuth extends Authenticator {
    const signIn = async () => {
        // 각 SSO, 커스텀 인증에 필요한 Sign in 로직
        const credentials = { // 각 커스텀 인증에서 생성된 credentials }
        super.signIn(credentials);
    }

    const signOut = async () => {
        // 각 SSO, 커스텀 인증에 필요한 Sign Out 로직
        super.signOut();
    }
}

결론적으로, 커스텀 auth class 내부에 상속받은 authenticator(super)의 함수들을 호출하여 spaceONE의 인증을 동시에 수행할 수 있도록 한다.

커스텀 인증 클래스 loader

이제 어떤 인증 클래스를 부를 건지 결정하는 loader를 간단하게 만들어보도록 하자.

export const loadAuth = (authType?) => {
    if (authType === 'CUSTOM_AUTH') return CustomAuth;
    return SpaceAuth;
};

signIn은 개별 템플릿을 가지지만, signOut은 어디에서든 호출될 수 있기 때문에 위와 같은 loader를 사용하여 현재 도메인의 인증 타입을 넣으면 알맞은 인증 로직을 수행할 수 있도록 작성한다.

커스텀 인증 템플릿

위의 작업을 하고난 후 마지막 작업은 폼 렌더링이다.

//Custom Auth의 template(external/custom/template/CUSTOM_AUTH.vue)

<template>
	<div>Custom 로그인을 위한 폼 버튼</div>
</template>

<script lang="ts">
 setup() {
 	//필요한 로직들
 	onMounted(async() => {
    		try {
            	await loadAuth('CUSTOM_AUTH').signIn(); //loader를 사용하여 customAuth 클래스의 signIn 함수 호출
            } catch (e) {
            	//에러 핸들링
            }
        }
    )
 }
</script>

각 커스텀 인증은 저마다의 폼 렌더링이 필요하기 때문에 위와 같이 해당 Auth에 맞는 커스텀 템플릿을 작성해준다.

그리고 마지막으로 커스텀 템플릿을 signIn Page에서 보여준다.
vue에는 라는 문법으로 다이나믹하게 컴포넌트를 불러올 수 있는 문법이 존재한다. 해당 문법을 이용하여

//sign-in page
<component :is="component" class="sign-in-template"
	@sign-in="handleSignIn"
/>

이렇게 템플릿 부분에 작성해주고, 아래 스크립트 부분에서

//sign-in page
  const state = reactive({
  	...
	component: computed(() => {
                let component;
                const auth = state.authType;
                if (auth) {
                    try {
                        component = () => import(`./external/${auth}/template/${auth}.vue`);
                    } catch (e) {
                        //필요한 에러 핸들링. 
                    }
                }
                return component;
            }),
  })

(composition api를 사용하기 때문에 state와 reactive를 사용한다.)
위와 같이 dynamic import 방식을 사용하여 컴포넌트를 갈아끼워준다.

코드는 상황에 따라 굉장히 다르게 구현될 수 있기 때문에 중요한 부분이 아니라고 생각하여 대부분을 생략하였다.

절차 요약

  1. 외부 인증 뿐만 아니라 자체 인증 또한 성공해야 하기 때문에 자체 인증만을 구현한 추상 클래스를 만든다.
  2. 해당 추상 클래스를 상속하여 구현한 커스텀 인증 클래스들을 만들어 해당 클래스 내부에서 커스텀 인증 로직을 처리하고, super class의 인증도 처리한다.
  3. 해당 커스텀 클래스를 불러오기 위한 loader를 추가한다.
  4. 커스텀 클래스의 폼을 렌더링하기 위한 template을 작성해준다.
  5. Sign In 페이지에서 다이나믹하게 해당 template을 불러서 갈아 끼워준다.

위와 같은 방식이 올바른 구현이라고 단정지을 수는 없지만 이 방식을 통해 Keycloak이나 ID/PW 기반 구현, 그리고 내부망에서만 접속이 가능한 외부 회사의 SSO도 쉽게 플러그인처럼 끼워넣어 개발할 수 있었다.

만약 다양한 인증을 제공하여야 하는 요구사항이 있다면 이렇게 class와 dynamic component를 적극적으로 활용하는 방안을 추천하고 싶다.

0개의 댓글