개발자의 오르막
SpringBoot 에 Firebase Admin Auth SDK 적용하기 본문
이번 프로젝트는 안드로이드 APP 과 API 가 먼저 Beta 버전으로 런칭예정이다. 팀 내에서 안드로이드 개발자만 있었기 때문에 거의 모든 기능을 안드로이드 앱으로 해결을 하려했기 때문에 인증 또한 Firebase Auth 를 기준으로 개발에 착수하였다.
Firebase Admin SDK 받기
- 먼저 firebase 홈페이지에 들어가 프로젝트를 추가한다.
- 왼쪽 상단의 설정 단추 (톱니바퀴) 를 누른 후 프로젝트 설정 화면의 서비스 계정 탭으로 들어갑니다.
Admin SDK 를 활용할 언어에 맞게 비공개 키를 생성합니다.
- Admin SDK 를 활용할 언어에 맞게 비공개 키를 생성합니다.
SpringBoot 프로젝트에 Firebase SDK 적용하기
- Firebase sdk json 파일을 프로젝트에 추가합니다.
- 해당 파일은 service-api 의 resources 디렉터리에 위치하여 .gitignore 에 등록해줍니다.
- 추후 Docker 로 배포시 Volume Mount 로 해당 파일을 ignore 처리 예정입니다.
Firebase 의존성 추가
implementation group: 'com.google.firebase', name: 'firebase-admin', version: '7.1.0'
implementation 'org.springframework.boot:spring-boot-starter-security'
- build.gradle 에 firebase-admin 의존성을 추가해줍니다.
- spring-boot security 의존성도 추가해줍니다.
Spring Configuration Bean 추가
@Configuration
@RequiredArgsConstructor
public class FirebaseConfig {
@Value("${firebase.sdk.path}")
private String sdkPath;
private FirebaseApp firebaseApp;
@PostConstruct
public FirebaseApp initializeFCM() throws IOException {
FirebaseOptions options = new FirebaseOptions.Builder()
.setCredentials(GoogleCredentials.fromStream(getFirebaseInfo()))
.build();
firebaseApp = FirebaseApp.initializeApp(options);
return firebaseApp;
}
@Bean
public FirebaseAuth initFirebaseAuth() {
FirebaseAuth instance = FirebaseAuth.getInstance(firebaseApp);
return instance;
}
private InputStream getFirebaseInfo() throws IOException {
Resource resource = new ClassPathResource(sdkPath);
if (resource.exists()) {
return resource.getInputStream();
}
throw new RuntimeException("firebase 키가 존재하지 않습니다.");
}
}
- FirebaseAuth 객체를 통한 인증을 위해 @Bean 생성을 진행
- @PostConstruct 를 통해 해당 Bean 이 생성되기 전에 firebase.json 파일을 읽고 설정하도록 진행
- sdkPath 는 @Value 를 통해 application.yml 에 지정한 경로에서 가져옵니다.
- Resource 는 해당 모듈의 resources 경로부터 시작하기 때문에, path 를 아래와 같이 설정해주면 된다.
firebase:
sdk:
path: config/chat-app-firebase-adminsdk.json
Filter 에서 인증토큰 검증하기
- 백엔드에서 firebase ID Token 을 인증하는 부분입니다.
- Filter 는 사용자 요청의 전후 처리를 할 수 있는 구성요소입니다. 사용자 요청이 들어오면 Controller 에 접근하기 전 먼저 Request 를 인터셉트 해서 전처리 역할 및 후처리 역할을 할 수 있습니다.
- 또한 Spring Security 설정과 결합하면 특정 Request 와 결합할때만 사용자 요청을 처리할 수 있습니다.
- 토큰을 검증하는 Filter 를 만들고 Security 요청에 따라 검증하도록 처리해보겠습니다.
SecurityConfig
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final FirebaseTokenFilter firebaseTokenFilter;
private final AuthServiceImpl authService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.authorizeRequests()
.antMatchers("/api/health-check", "/api/members/sign-up/**", "/api/email-verify", "/api/nickname-verify")
.permitAll()
.anyRequest().authenticated().and()
.addFilterBefore(firebaseTokenFilter,
UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED));
}
@Bean
public RegistrationBean firebaseAuthTokenRegister(FirebaseTokenFilter filter) {
FilterRegistrationBean registrationBean = new FilterRegistrationBean(filter);
registrationBean.setEnabled(false);
return registrationBean;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(authService);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers("/resources/**");
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- Security 에서 적용되는 필터 전에 적용될 수 있도록 설정한다. .addFilterBefore(firebaseTokenFilter, UsernamePasswordAuthenticationFilter.class)
- http 기본 설정과 csrf 는 사용하지 않도록 disable 처리를 해준다.
FirebaseTokenFilter
@Component
@RequiredArgsConstructor
public class FirebaseTokenFilter extends OncePerRequestFilter {
private final FirebaseAuth firebaseAuth;
private final AuthServiceImpl authService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// get the token from the request
if (ValidateRequestHeader.checkAuthorization(request)) {
filterChain.doFilter(request, response);
return;
}
FirebaseToken decodedToken;
String token;
try {
token = ValidateRequestHeader.getAuthorizationToken(request);
} catch (IllegalArgumentException e) {
setUnauthorizedResponse(response, "INVALID_TOKEN");
return;
}
try {
decodedToken = firebaseAuth.verifyIdToken(token);
} catch (FirebaseAuthException e) {
setUnauthorizedResponse(response, "INVALID_TOKEN");
return;
}
if (decodedToken == null) {
setUnauthorizedResponse(response, "INVALID_TOKEN");
return;
}
// User를 가져와 SecurityContext에 저장한다.
try{
UserDetails user = authService.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) throws IOException {
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\\"code\\":\\"INVALID_TOKEN\\"}");
}
}
- doFilterInternal 을 오버라이드한 상태에서 Request 가 들어오면 해당 로직을 타게 된다.
- 이때 요청의 header 에서 Authentication 의 Bearer 로 Firebase 의 ID Token 이 올때만 해당 로직을 타도록 Validation 을 적용한다.
- firebaseAuth.verifyIdToken(token) 을 통해 firebase sdk 로 적합한 ID Token 이 잇는지 체크한다.
- 이후 authService 로 해당 uuid 를 가진 회원이 있는지 검사하고, 회원의 로그인이 적합한지 loadUserByUsername 으로 객체를 가져온다.
- SecurityContextHolder 에 로그인을 성공한 객체를 Set 해준다.
전체로직
- Authorization Header 에서 Token 을 가져온다.
- FirebaseAuth 를 이용하여 Token 을 검증한다.
- UserDetailsService 에서 사용자 정보를 가져와 SecurityContext 에서 추가해준다.
- id 를 firebase 에서 제공하는 uid 를 사용하였다.
- UserDetails 와 UserDetailsService 는 Interface 를 구현한다.
- Context 에 추가한 User 정보는 Controller 에 Principal principal 을 추가해 받아올 수 있다.
- 인증 실패시 HttpStatus 403과 json 으로 code 를 response 하게 하였다.
Postman ID Token 테스트
- 서버에서 ID Token 을 발급받는 방법은 Postman 으로 할 수 있다.
- firebase 설정 화면에 가면 webKey 를 받을 수 있다.
- 그리고 Postman 으로 회원가입 API 를 등록하면 아래와 같은 Response 를 얻을 수 있다.
Firebase admin sdk Sign-up
- URL : https://identitytoolkit.googleapis.com/v1/accounts:signUp?key={webKey}
- Method : POST
- Request
{
"email": "kjuiop222@naver.com",
"password":"",
"returnSecureToken":true
}
- Response
{
"email": "kjuiop222@naver.com",
"password":"",
"returnSecureToken":true
}
- 웹 API 키는 프로젝트 설정 화면의 일반탭에 명시되어 있다.
- 이후 인가가 필요한 API 요청 Headers 에 idToken 을 기입하면 된다.
Reference
'Toy Project > Tortee' 카테고리의 다른 글
Layer Architecture 에 따른 멀티모듈 구성하기 (0) | 2022.11.12 |
---|---|
Spring Boot Rest Docs 멀티모듈 프로젝트에 적용 (0) | 2022.10.17 |
지속 가능한 개발을 위한 멀티모듈과 SpringBoot (0) | 2022.10.10 |
Comments