logo
한달 포트폴리오 멘토링
블로그

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

이전 포스팅에 이어 구글 로그인응 이용하기 위하여 **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에서 시작하기를 누르고 비공개키를 다운받습니다.

비공개 키를 프로젝트 최상위 폴더의 firebase.json 이란 이름으로 저장해봅시다. 이제 기본 셋업이 완료되었으니 프로젝트 구현을 진행합시다.

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

Firebase 초기화

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

@Configuration public class FirebaseInitializer { @Bean public FirebaseApp firebaseApp() throws IOException { log.info("Initializing Firebase."); FileInputStream serviceAccount = new FileInputStream("./firebase.json"); FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .setStorageBucket("heroku-sample.appspot.com") .build(); FirebaseApp app = FirebaseApp.initializeApp(options); log.info("FirebaseApp initialized" + app.getName()); return app; } @Bean public FirebaseAuth getFirebaseAuth(){ FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(firebaseApp()); 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에서 토큰검증을 수행할 것입니다.

전체 예제 코드

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

PortfolioAD
관련있는 글

couchcoding

2022-12-07

Spring Security (1) - 구조와 동작 방식

Spring Security는 Spring Application 개발시에 보안을 적용하기 위해 사용하는 보안 프레임워크 입니다. Spring Security는 웹 보안을 위하여 인증 및 보안 관련 로직을 제공합니다. 특히 Spring Security의 가장 중요한 기...

백엔드

spring
spring security
filter
보안
로그인

couchcoding

2022-12-07

[Spring] QueryDSL로 조건검색 API를 만들어보자(동적 쿼리)

조건 검색을 만들기 위해서 QueryDSL이라는 라이브러리를 사용하려고 합니다. QueryDSL은 JPA만으로는 복잡한 쿼리를 만들기 어렵고 JPQL과 같이 직접 SQL을 사용하는 방식은 SQL을 실행 전까지는 SQL을 검증할 수 없어 오류가 생기기 쉽습니다. ...

백엔드

spring
JPA
QueryDSL
조건 검색
동적 쿼리

couchcoding

2022-12-07

[Spring] 개발환경에 따라 Profile 분리하는 방법과 활용 예제를 알아보자

개발 환경과 Product 실행 환경을 분리하기 위해서 사용 ex) 로컬 개발환경에서는 h2 데이터베이스를 사용하고, 실제 배포환경에서는 postgres db를 사용하는 경우 ex) 보안 파일이나 암호를 모든 개발자에게 공개할 수 없는 경우 ex) 로컬 개발환경에서는...

백엔드

spring
배포
spring profile
spring 환경설정

couchcoding

카우치코딩 공식 계정입니다.