ํ๋ก์ ํธ์ ์ด์ ๋ฒ์ ์๋ ๋ฐ๋ก ๋ฐฑ์๋ ์๋ฒ๊ฐ ์์๊ณ , React Native์์ Firebase๋ฅผ ์ด์ฉํด ์์ ๋ก๊ทธ์ธ์ ์งํํ๊ณ ์์๋ค. ๋๋ ์ดํ ํ๋ก์ ํธ์ ํฉ๋ฅํด Spring์ผ๋ก ๋ค๋ฅธ ์๋ฒ๋ฅผ ๊ฐ๋ฐํ๊ณ ์๋ค. ๋ฐฑ์๋์์๋ ์ฌ์ฉ์๋ฅผ ์๋ณํ๊ธฐ ์ํด Firebase๋ก ๋ฐ๊ธ๋ ํ ํฐ์ ์ฌ์ฉํด์ผ ํ๋ค.
๊ตฌํ์ ์ํด Firebase Token์ ๋ํด ์ดํดํ๋ ๊ณผ์ ์ด ํ์ํ๊ณ , ํ ํฐ์ ๋ค๋ฃจ๊ธฐ ์ํด Spring Security๋ ๊ณต๋ถํด์ผ ๋๋ค. ๊ทธ๋ฆฌ๊ณ ์ต์ข ์ ์ผ๋ก firebase๊ฐ ํฌํจ๋ ์๋ฒ๋ฅผ ๋ฐฐํฌํ๊ธฐ๊น์ง ๋ง์ ์ฝ์ง์ ํ๊ฒ ๋์๋ค. ๊ตฌํํ๋ฉด์ ๊ณต๋ถํ๋ ๋ด์ฉ์ ์ ๋ฆฌํ๊ณ , ๊ตฌํ ๊ณผ์ ์ ๊ณต์ ํ๊ณ ์ ํ๋ค!
Firebase Token?
์ฐ์ Firebase Token์ ๋ํด ์์์ผ ํ๋ค. Firebase Authentication์ ํตํด ๋ก๊ทธ์ธ์ ํ๋ฉด ๋ฐ๊ธ๋๋ ํ ํฐ์ผ๋ก JWT(JSON Web Token) ํ์์ด๋ค. Firebase SDK์์ ๋ก๊ทธ์ธ๊ณผ ์ธ์ ์ ์ง ๋ฟ๋ง ์๋๋ผ, ID Token(Access Token)๊ณผ Refresh Token์ ์๋ช ์ ๊ด๋ฆฌํ๊ณ , ์๋์ผ๋ก ๊ฐฑ์ ํด์ฃผ๋ ์ญํ ์ ํ๋ค.
ํด๋ผ์ด์ธํธ์ ์๋ฒ์์ ์ฌ์ฉ์ ์ธ์ฆ์ ์ํด ๋ฐ๊ธ๋๊ณ ์ฌ์ฉ๋๋ ์ ๋ณด์๋ `UID`, `ID Token`, `Refresh Token`์ด ์๋ค.
UID
- Firebase Authentication์์ ์ธ์ฆ๋ ์ฌ์ฉ์์๊ฒ ๋ฐ๊ธํ ๊ณ ์ ํ ID
- ๋ฐฑ์๋์์ Firebase Admin SDK๋ก ํ ํฐ ๊ฒ์ฆ ๋ฐ UID ์ถ์ถ์ ํ ์ ์๋ค.
ID Token(Access Token)
- 1์๊ฐ ๋์ ์ ํจํ๋ฏ๋ก ๋ง๋ฃ๋๋ฉด ์๋ก ๊ฐฑ์ ํด์ผ ํ๋ค.
- ์๋ฒ์ ์ธ์ฆ ์์ฒญ์ ๋ณด๋ผ ๋ ์ฌ์ฉ๋๋ค.
- ๋ฐฑ์๋์์ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ์์ฒญ ํค๋๋ก ์ ๋ฌ๋ฐ์ ID Token์ ์ ํจ์ฑ์ ๊ฒ์ฆํ ์ ์๋ค.
Refresh Token
- ID Token์ด ๋ง๋ฃ๋์์ ๋, Firebase์์ ์๋ก์ด ID Token์ ๋ฐ๊ธ๋ฐ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
- Firebase SDK๊ฐ ์๋์ผ๋ก ๊ด๋ฆฌํด์ค๋ค.(๊ฐ๋ฐ์๊ฐ ์ง์ Refresh Token ๊ด๋ฆฌํ ํ์ ์์)
์ธ์ฆ ๊ณผ์
ํด๋ผ์ด์ธํธ์ ์์ ๋ก๊ทธ์ธ ์ ๊ณต์, Firebase ์ฌ์ด์์ ์ด๋ฃจ์ด์ง๋ ์์ ๋ก๊ทธ์ธ์ ์์ธํ ๊ณผ์ ์ ์๋ตํ๊ณ , ๋ก๊ทธ์ธ ์ดํ ํด๋ผ์ด์ธํธ, Firebase, ์๋ฒ์์ ์ํธ์์ฉ์ ์ค์ฌ์ผ๋ก ์ค๋ช ํ๊ฒ ๋ค. ๋ค์๊ณผ ๊ฐ์ ๊ณผ์ ์ผ๋ก ์ธ์ฆ ๋ฐ ์์ฒญ์ด ์ด๋ฃจ์ด ์ง๋ค.
1. ํด๋ผ์ด์ธํธ์์ ์์ ๋ก๊ทธ์ธ์ ์์ฒญํ๋ฉด Firebase๋ ์์ ๋ก๊ทธ์ธ ์ ๊ณต์๋ก๋ถํฐ ๋ฐ์ ํ ํฐ์ ๊ฒ์ฆํ๋ค.
2. Firebase๋ ํด๋ผ์ด์ธํธ์๊ฒ ID Token๊ณผ Refresh Token์ ๋ฐํํ๋ค.
3. ํด๋ผ์ด์ธํธ๋ ID Token์ ์ธ์ฆ ํค๋๋ก ๋ฐฑ์๋ ์๋ฒ์๊ฒ ์ ๋ฌํ๋ค.
4. ๋ฐฑ์๋๋ Firebase Admin SDK๋ฅผ ์ฌ์ฉํด ํด๋น ํ ํฐ์ ๊ฒ์ฆํ๋ค.
5. Firebases๋ ๊ฒ์ฆ์ ์ฑ๊ณตํ๋ฉด UID๋ฅผ ๋ฐํํ๋ค.
6. ์๋ฒ๋ UID๋ฅผ ์ฌ์ฉํ์ฌ ์ฌ์ฉ์๋ฅผ ์๋ณํ๊ณ ๋น์ฆ๋์ค ๋ก์ง์ ์ฒ๋ฆฌํ๋ค.
Spring Security
์๋ฒ๋ก ๋ค์ด์ค๋ ๋ชจ๋ ์์ฒญ์ Firebase ํ ํฐ์ผ๋ก ๊ฒ์ฆํ๊ณ , ์ ๊ทผ์ ์ ์ดํ๋ ์์คํ ์ ๊ตฌ์ถํด์ผ ํ๋ค. Spring์์ ์ธ์ฆ, ์ธ๊ฐ ๊ธฐ๋ฅ์ ๊ฐ๋ฐํ๋ ๊ธฐ๋ฅ์ ์ ๊ณตํด์ฃผ๋ Spring Security๋ฅผ ์ฌ์ฉํ๊ฒ ๋์๋ค.
Spring Security๋ Filter๋ฅผ ํตํด ์์ฒญ์ด Controller์ ๋๋ฌํ๊ธฐ ์ ์ Firebase ํ ํฐ์ ๊ฒ์ฆํ๊ฒ ๋๋ค. ๊ทธ๋ฆฌ๊ณ ๊ฒ์ฆ์ด ๋๋ฉด, SecurityContextHolder์ UID๋ ์ด๋ฉ์ผ ๋ฑ์ ์ ๋ณด๊ฐ ํฌํจ๋ ์ธ์ฆ ๊ฐ์ฒด(Authentification)์ ์ ์ฅํ๋ค. ๊ทธ๋์ ์๋ฒ์ ์ ์ญ์์ ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ์ฐธ์กฐํด ๋ก์ง์ ์ํํ ์ ์๊ฒ ๋๋ค.
๊ตฌํ
๊ฐ๋ฐ ํ๊ฒฝ
์๋ฒ
- Spring Boot `3.2.4`, Amazon corretto `17.0.10`
- Firebase
- Spring Security
- Lombok
์ธํ๋ผ
- GCP, Ubuntu
- Docker
1. ์์กด์ฑ ์ถ๊ฐ
implementation 'com.google.firebase:firebase-admin:9.2.0'
implementation 'org.springframework.boot:spring-boot-starter-security'
- `build.gradle`์ Firebase Admin ์ฌ์ฉ์ ์ํ ์์กด์ฑ์ ์ถ๊ฐํด์ค๋ค.
- Spring Security ์ฌ์ฉ์ ์ํ ์์กด์ฑ๋ ์ถ๊ฐํด์ค๋ค.
2. Firebase key ์์ฑ
Firebase ํ๋ก์ ํธ์์ ๋ค์ด๊ฐ์ 'ํ๋ก์ ํธ ์ค์ ' > '์๋น์ค ๊ณ์ ' > 'Firebase Admin SDK' > '์ ๋น๊ณต๊ฐ ํค ์์ฑ' ์ ํด๋ฆญํ๋ฉด, json์ผ๋ก ๋ key๋ฅผ ์์ฑํ ์ ์๋ค.
3. Firebase ์ค์ ํด๋์ค ์ถ๊ฐ
@Configuration
public class FirebaseConfig {
@Bean
public FirebaseApp initializeFirebase() throws IOException {
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(new ClassPathResource("serviceAccountKey.json").getInputStream()))
.build();
if (FirebaseApp.getApps().isEmpty()) {
return FirebaseApp.initializeApp(options);
} else {
return FirebaseApp.getInstance();
}
}
}
key์ ํ์ผ ๊ฒฝ๋ก์ json ํ์ผ์ด ์๋ ๊ฒฝ๋ก๋ฅผ ๋ฃ์ด์ฃผ๋ฉด ๋๋ค.
๊ฐ๋ฐํ๋ ๋ก์ปฌ ํ๊ฒฝ์์๋ `src/main/resources/serviceAccountKey.json`์๋ค. ํ์ง๋ง ํด๋ผ์ฐ๋์ docker๋ฅผ ์ด์ฉํ์ฌ ๋ฐฐํฌํ๋ฉด ์๋ฌ๊ฐ ๋ ๊ฒ์ด๋ค.
Failed to instantiate [com.google.firebase.FirebaseApp]: Factory method 'initializeFirebase' threw exception with message: src/main/resources/serviceAccountKey.json (No such file or directory)
๋ฐฐํฌ ํ๊ฒฝ์์ ์๋ฌ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ๋ค์๊ณผ ๊ฐ์ด ์ฝ๋๋ฅผ ์์ ํ๋ค.
//AS-IS
FileInputStream serviceAccount = new FileInputStream("src/main/resources/serviceAccountKey.json");
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(serviceAccount))
.build();
//TO-BE
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(new ClassPathResource("serviceAccountKey.json").getInputStream()))
.build();
4. Firebase ์ธ์ฆ ํํฐ ํด๋์ค
Firebase ์ธ์ฆ ํ ํฐ์ ์ฒ๋ฆฌํ๊ธฐ ์ํ Spring Security์ Custom Filter์ด๋ค. Servlet์ผ๋ก ๋ค์ด์ค๋ ์์ฒญ์ ๊ฐ๋ก์ฑ ์ธ์ฆ ๋ก์ง์ ์ํํ๊ฒ ๋๋ค.
@Component
public class FirebaseAuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
String authHeader = httpRequest.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.replace("Bearer ", ""); // "Bearer " ์ดํ์ ํ ํฐ ๊ฐ๋ง ์ถ์ถ
// Firebase Token์ ๊ฒ์ฆํ๊ณ UID๋ฅผ ์ถ์ถ
FirebaseToken decodedToken = FirebaseAuth.getInstance().verifyIdToken(token);
String uid = decodedToken.getUid();
// UID๋ฅผ ์ด์ฉํ์ฌ ์ฌ์ฉ์ ์ธ์ฆ ๊ฐ์ฒด ์์ฑ
User principal = new User(uid, "", Collections.emptyList());
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(principal, null, Collections.emptyList());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpRequest));
// Spring Security์ SecurityContext์ ์ค์
SecurityContextHolder.getContext().setAuthentication(authentication);
}
// ๋ค์ ํํฐ๋ก ์์ฒญ์ ๋๊น
filterChain.doFilter(servletRequest, servletResponse);
}
}
1. `doFilter` ๋ฉ์๋๋ ํํฐ๊ฐ ์คํ๋ ๋ ํธ์ถ๋๋ค.
2. ์์ฒญ ํด๋์์ Firebase์ JWT๋ฅผ ๊ฐ์ ธ์จ๋ค. ํด๋ผ์ด์ธํธ์์ `Bearer [ํ ํฐ]` ํ์์ผ๋ก ์ ๋ฌ์ด ๋๋ค.
3. ํ ํฐ ๊ฐ(ID Token)์ ์ถ์ถํ๊ณ , Firebase Admin SDK๋ก ํ ํฐ์ ๊ฒ์ฆํ๋ค. ์ฌ๊ธฐ์ ํ ํฐ์ด ์ ํจํ์ง ์๊ฑฐ๋ ๋ง๋ฃ๋์๋ค๋ฉด ์์ธ๊ฐ ๋ฐ์ํ๋ค.
4. ์ธ์ฆ๋์์ผ๋ฉด, UID๋ฅผ ์ถ์ถํ๋ค.
5. UID๋ฅผ ์ฌ์ฉ์ id๋ก ์ค์ ํด Spring Security์ Authentification ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
6. ํํฐ ์ฒด์ธ์์ ๋ค์ ํํฐ๋ก ์์ฒญ์ ๋๊ธฐ๊ฑฐ๋ ์ปจํธ๋กค๋ฌ๋ก ์ ๋ฌํ๋ค.
5. Security ์ค์ ํด๋์ค ์ถ๊ฐ
Spring Security ์ค์ ํด๋์ค์์ Firebase ์ธ์ฆ ํํฐ๋ฅผ ์ ์ํ๊ณ , ์๋ฒ์ ๋ณด์ ๊ท์น์ ์ค์ ํ๋ค.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final FirebaseAuthenticationFilter firebaseAuthenticationFilter;
public SecurityConfig(FirebaseAuthenticationFilter firebaseAuthenticationFilter) {
this.firebaseAuthenticationFilter = firebaseAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf.disable()) // CSRF ๋นํ์ฑํ
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/swagger", "/swagger-ui.html", "/swagger-ui/**", "/api-docs", "/api-docs/**", "/v3/api-docs/**")
.permitAll()
.requestMatchers("/test/**")
.permitAll()
.anyRequest().authenticated() // ๊ทธ ์ธ ์์ฒญ์ ์ธ์ฆ ํ์
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // ์ธ์
์ ์ฌ์ฉํ์ง ์์
)
.addFilterBefore(firebaseAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) // Firebase ์ธ์ฆ ํํฐ ์ถ๊ฐ
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // ํจ์ค์๋ ์ธ์ฝ๋ ์ค์
}
}
authorizeHttpRequests
- ์์ฒญ URL์ ๋ํด ์ ๊ทผ ์ ์ด๋ฅผ ์ค์ ํ ์ ์๋ค. ํน์ ๊ฒฝ๋ก๋ `permitAll()`์ ํตํด ์ธ์ฆ ์์ด ์ ๊ทผ ๊ฐ๋ฅํ๋๋ก ํ ์ ์๋ค.
- ๋ง์ฝ Swagger๋ฅผ ์ด๋ค๋ฉด, ๊ด๋ จ๋ ๊ฒฝ๋ก๋ค์ ํ์ฉํด์ค์ผ ํ๋ค. ์ ํด์ฃผ๋ฉด Swagger์ ์ ์ํ ์ ์๋ค.
์ธ์ ๊ด๋ฆฌ ์ ์ฑ ์ ๋ํด์๋ ๋๋ Firebase๋ฅผ ์ฌ์ฉํ์ฌ ํ ํฐ ๊ธฐ๋ฐ์ ์ธ์ฆ์ ํ๋ฏ๋ก, `STATELESS`๋ก ์ค์ ํ๋ค.
Spring Security ํํฐ ์ฒด์ธ์ ์ปค์คํ ํํฐ(firebaseAuthenticationFilter)๋ฅผ ์ถ๊ฐํด์ฃผ๋ฉด ๋๋ค.