안녕하세요 카우치코딩입니다.

이전 포스팅에 이어 구글 로그인응 이용하기 위하여 Java Backend(SpringBoot)를 통해 Resource Service를 구현하는 것을 배워보겠습니다. 예제는 자바로 진행하나 구조를 익히면 다른 백엔드 프레임워크에서도 사용할 수 있을 것입니다.


위 그림에서 6-9에 해당되는 로직입니다.

1. 프로젝트 셋업하기

Firebase Admin은 Firebase에서 온 인증 토큰을 검증 할 수 있는 기능을 가지고 있는 라이브러리입니다. 라이브러리를 설치하고 Firebase Admin 사용을 위한 sdk키를 프로젝트에 추가하는 작업을 진행하겠습니다.

해당 작업은 이미 Maven이나 Gradle 기반으로 SpringBoot 프로젝트를 시작했다는 가정에서 시작합니다.

Firebase Admin 설치

Firebase Admin은 Maven Central에서 설치하실수 있습니다. 다음 명령어를 dependencies 안에 추가해줍시다.
Spring Security도 같이 추가해줍시다.

// gradle
implementation group: 'com.google.firebase', name: 'firebase-admin', version: '8.0.1'
implementation 'org.springframework.boot:spring-boot-starter-security'
<!-- Maven -->
<!-- https://mvnrepository.com/artifact/com.google.firebase/firebase-admin -->
<dependency>
    <groupId>com.google.firebase</groupId>
    <artifactId>firebase-admin</artifactId>
    <version>8.0.1</version>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.5.4</version>
</dependency>

Firebase SDK key 다운로드

https://console.firebase.google.com/ 에서 자신의 프로젝트를 클릭하여 Firebase Project페이지에 들어갑시다. 여기서 프로젝트 설정에 들어갑시다.

서비스계정 탭에서 Firebase Admin SDK에서 시작하기를 누르고 비공개키를 다운받습니다.

비공개 키의 위치를 환경변수로 등록해줍시다

// mac or linux
export GOOGLE_APPLICATION_CREDENTIALS="{비공개 키의 위치}"

// windows
$env:GOOGLE_APPLICATION_CREDENTIALS="{비공개 키의 위치}"

이제 기본 셋업이 완료되었으니 프로젝트 구현을 진행합시다.

2. Firebase 초기화, 인증토큰 검증

Firebase 초기화

FirebaseInitializer라는 Configuration을 하나 만들어 FirebaseAuth(인증 관련 모듈)을 초기화하도록 하겠습니다.

@Configuration 
public class FirebaseInitializer {

	@Bean
	public FirebaseAuth getFirebaseAuth(){
		FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(getFirebaseApp());
		return firebaseAuth;
	}
}

Spring에서 Bean 초기화시 우선순위를 가지고 먼저 수행하게 하는 어노테이션 입니다. 먼저 FirebaseApp을 초기화하고 FirebaseAuth를 초기화 하도록하였습니다.

혹시 초기화에서 에러가 나시는분은 환경변수 추가가 제대로 되어있는지 확인해보세요

3. Filter에서 인증토큰 검증하기

이제 백엔드에서는 firebase IDToken을 인증하는 부분을 작성하려고 합니다.
Filter는 사용자 요청의 전후 처리를 할 수 있는 구성요소입니다. 사용자 요청이 들어오면 Controller에 접근하기 전에 먼저 Request를 인터셉트 해서 전처리 역할 및 후처리 역할을 할 수 있습니다.

또한 Spring Security 설정과 결합하면 특정 Request와 결합할때만 사용자 요청을 처리할 수 있습니다.

토큰을 검증하는 Filter를 만들고 Security에 요청에따라 검증하도록 처리해봅시다.

(본 예제는 Client 단에서 Header에 Authorization: Bearer {FirebaseIdToken} 형태로 메세지가 온다고 가정합니다.)

FirebaseTokenFilter

public class FirebaseTokenFilter extends OncePerRequestFilter{

    private UserDetailsService userDetailsService;
    private FirebaseAuth firebaseAuth;

    public JwtFilter(UserDetailsService userDetailsService, FirebaseAuth firebaseAuth) {
        this.userDetailsService = userDetailsService;
        this.firebaseAuth = firebaseAuth;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        // get the token from the request
        FirebaseToken decodedToken;
				String header = request.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
						setUnauthorizedResponse(response, "INVALID_HEADER");
						return;
        }
				String token = header.substring(7);

			  // verify IdToken
        try{
            decodedToken = firebaseAuth.verifyIdToken(token);
        } catch (FirebaseAuthException e) {
						setUnauthorizedResponse(response, "INVALID_TOKEN");
            return;
        }

        // User를 가져와 SecurityContext에 저장한다.
        try{
            UserDetails user = userDetailsService.loadUserByUsername(decodedToken.getUid());
            UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                    user, null, user.getAuthorities());        
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } catch(NoSuchElementException e){
						setUnauthorizedResponse(response, "USER_NOT_FOUND");
            return;
        }
        filterChain.doFilter(request, response);
    }

		private void setUnauthorizedResponse(HttpServletResponse response, String code) {
          response.setStatus(HttpStatus.SC_UNAUTHORIZED);
          response.setContentType("application/json");
          response.getWriter().write("{\"code\":\""+code+"\"}");
		}
}

doFilterInternal를 오버라이드 하였는데 Request가 들어오면 해당 로직을 타게된다.

전체로직은

  1. Authorization Header에서 Token을 가져온다.
  2. FirebaseAuth를 이용하여 Token을 검증한다.
  3. UserDetailsService에서 사용자 정보를 가져와 SecuriyContext에 추가해준다.
    1. 현재 예제에서는 id를 firebase에서 제공하는 uid를 사용하였다
      Firebase에서 제공하는 사용자별 유니크 id다.
      →userDetailsService.loadUserByUsername(uid)
    2. UserDetails와 UserDetailsService는 Interface를 구현해 사용해주자.
    3. Context에 추가한 User정보는 Controller에 Principal principal 를 추가해 받아올 수 있다.
      1. https://www.baeldung.com/get-user-in-spring-security 참고!
  4. 인증 실패시 HttpStatus 403과 json으로 code를 response하게 하였다.

이제 해당 Filter를 Security에 적용시켜보자.

만약 '@component'로 Filter를 만든다면 모든 요청에 Filter가 추가된다.

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private FirebaseAuth firebaseAuth;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .anyRequest().authenticated().and()
                .addFilterBefore(new FirebaseTokenFilter(userDetailsService, firebaseAuth),
                     UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
    }
    
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 회원가입, 메인페이지, 리소스
				web.ignoring().antMatchers(HttpMethod.POST, "/users")
            .antMatchers("/")
            .antMatchers("/resources/**");
    }
}

configure는 HttpSecurity를 받는 부분과 모든 WebSecurity을 받는 부분이 있는데 HttpReuqest를 받는 부분에 filter를 적용(addFilterBefore)하고 WebSecurity를 받는 부분에서 Filter를 적용하지 않을 요청을 추가하였습니다. (ignoring)

ignoring 하지 않은 모든 요청은 FirebaseTokenFilter에서 토큰검증을 수행할 것입니다.

포트폴리오 가이드를 받고싶다면

카우치코딩에서는 6주 포트폴리오 수업을 통해 기획 ~ 배포까지의 과정을 멘토링하고 있습니다. 기획 ~ 배포까지의 과정을 전부 경험하고 싶으면 방문해주세요~

전체 예제 코드

https://github.com/Quickeely/OauthSample 에서 회원가입이 포함된 전체 코드를 확인하실 수 있습니다.
JPA, H2등의 추가적인 라이브러리를 활용하였습니다.

이전글

* Firebase로 Google 로그인 구현하기 (Spring + React)

다음글

* Firebase로 Google 로그인 구현하기 (React 구현)

profile
포트폴리오 수업 & 코딩 멘토링 서비스 카우치코딩입니다.

0개의 댓글