지난 일주일 동안 블로그에 작성할 내용이 떠올랐지만, 그것을 글로 옮기기에는 에너지가 부족했다. 나는 하루 종일 프로젝트에만 집중해야 했고, 6시가 넘어서도 에러를 해결하느라 게임도 많이 못했다. 😓
프론트엔드 팀원 중 한 명은 취업으로 인해 우리 팀을 떠났고, 하나뿐인 백엔드 팀원은 실력이 부진하여 기여를 못하는 터라 나 혼자서 프로젝트를 진행해야 했다. 우리 프로젝트는 실질적으로 3인 프로젝트가 된 셈이다. 그로인해 프로젝트가 실패하지 않을까 하는 걱정과 나의 부족한 실력으로 인한 답답한 마음이 항상 있었다.
코드스테이츠의 운영진들이나 멘토님들과 여러 차례 상담을 받았는데, 내가 해내지 못하면 프로젝트가 실패하는데 내 능력이 모자라서 분하고 속상하다는 마음을 이야기하다가 울곤 했다. 그 말을 들은 운영진들은 멋진 속상함을 느끼고 있는 것 같다고, 소희님은 책임감이 강한 사람이라서 개발자로서 포텐셜이 높은 사람인 것 같으니 믿어주시겠다며 힘든점을 언제든 들어주시겠다며 많은 위로와 격려와 공감과 응원을 해주셨고 정말로 많은 힘이 되었다.
나는 추가적인 도움을 받지않고 혼자서 프로젝트를 해내보겠다고 말씀드렸고, 마감까지 일주일이 남은 오늘, ngrok으로 배포한 후 프론트엔드 쪽에서 테스트를 해 보았다. 잘 작동하는 것을 확인하고서야 가벼운 마음으로 블로그를 작성하러 올 수 있었다.
결국 모든 오류들을 해결하게 되어서 너무너무 기쁘고 스스로가 대견하다. (난 짱이야!!!😊)
프론트와 연결한 이후에는 사소한 요청들을 수정해나가고 있는데 연결이 된 것 만으로도 뿌듯해서 너무나 행복하게 코딩할 수 있다. 이 맛에 프로젝트를 하는 것 같다.
cors에러를 지난번 프로젝트에서 해결했던 터라 이번에는 쉽게 넘어갈 줄 알았는데 또 cors에러가 뜬다고해서 관련된 블로그를 참 많이 살펴보고 코드를 거의 50번이상 수정 했었지만 고쳐지지 않았는데 결국 찾아낸 방법은 프론트에서 요청할 때 헤더에 'ngrok-skip-browser-warning': '69420' 넣어서 해결했다. 무료로 ngrok을 사용하기때문에 설정해주어야 하는 부분인 것 같다.
@Configuration
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {
private final JwtUtils jwtUtils;
private final JwtTokenizer jwtTokenizer;
private final CustomAuthorityUtils authorityUtils;
private final RedisService redisService;
private final MemberRepository memberRepository;
@Value("${spring.security.oauth2.client.registration.google.clientId}")
private String clientId;
@Value("${spring.security.oauth2.client.registration.google.clientSecret}")
private String clientSecret;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.exceptionHandling()
.authenticationEntryPoint(new UserAuthenticationEntryPoint())
.accessDeniedHandler(new UserAccessDeniedHandler())
.and()
.apply(new CustomFilterConfigurer())
.and()
.authorizeHttpRequests(authorize -> authorize
.antMatchers(HttpMethod.POST, "/members/**").permitAll()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.GET, "/members/**").permitAll()
.antMatchers(HttpMethod.PATCH,"/members/**").authenticated()
.antMatchers(HttpMethod.DELETE,"/members/**").authenticated()
.antMatchers("/trades").authenticated()
.antMatchers("/fixeds").authenticated()
.antMatchers("/wishlists").authenticated()
.anyRequest().permitAll()
)
.logout()
.logoutUrl("/logout")
.addLogoutHandler(new UserLogoutHandler(redisService, jwtTokenizer))
.logoutSuccessUrl("/");
return http.build();
}
//우리가 구현한 JwtAuthenticationFilter를 등록하는 역할
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer, redisService);
jwtAuthenticationFilter.setFilterProcessesUrl("/login");
jwtAuthenticationFilter.setAuthenticationSuccessHandler(new UserAuthenticationSuccessHandler());
jwtAuthenticationFilter.setAuthenticationFailureHandler(new UserAuthenticationFailtureHandler());
JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils, redisService, memberRepository);
builder
.addFilter(jwtAuthenticationFilter)
.addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
}
}
@Override //인터셉터와 연결
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new ControllerInterceptor(jwtUtils))
.addPathPatterns("/members/**", "/login", "/trades/**", "/fixed/**", "/wishlists/**", "/totals/**");
}
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET","POST", "PATCH", "DELETE","OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization")
.exposedHeaders("Refresh");
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE","OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.addExposedHeader("Authorization");
configuration.addExposedHeader("Refresh");
configuration.addExposedHeader("MemberId");
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
그리고 컨트롤러에 @CrossOrigin 붙이기
이제는 필터가 어떤 역할을 하는지 알아서인지 응답헤더에 memberId를 추가해달라는 요청을 받았는데 5분만에 뚝딱 해결했다. 시큐리티에 대해 전혀 모르던 내가 이만큼 발전했다는 사실이 놀랍다.
// JwtAuthenticationFilter에서의 수정
@Override // 사용자 인증이 성공했을때 JWT토큰을 생성하고, 응답정보 설정
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException {
System.out.println("JwtAuthenticationFilter.successfulAuthentication");
Member member = (Member) authResult.getPrincipal();
String accessToken = delegateAccessToken(member);
String refreshToken = delegateRefreshToken(member);
Long memberId = member.getMemberId();
// redis -> key(email) : value(refreshToken) 저장, expireDate(refreshToken) 시간 지난 후 삭제됨
redisService.setDataWithExpiration(
member.getEmail(),
refreshToken,
Long.valueOf(jwtTokenizer.getRefreshTokenExpirationMinutes())
);
System.out.println(redisService.getData(member.getEmail()));
response.setHeader("Authorization", "Bearer " + accessToken); // (4-4)
response.setHeader("Refresh", refreshToken);
response.setHeader("MemberId", String.valueOf(memberId));
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
// SecurityConfig에서의 수정
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PATCH", "DELETE","OPTIONS"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.addExposedHeader("Authorization");
configuration.addExposedHeader("Refresh");
configuration.addExposedHeader("MemberId");
source.registerCorsConfiguration("/**", configuration);
return source;
}
기존 코드는 .setSigningKey(getKeyFromBase64EncodedKey(secretKey)) 였었는데
인코딩되지않은 키로 비밀키를 만들어서 발생한 오류라는 점을 멘토님이 발견해주셨다.
public Claims extractAllClaims(String token) {
return Jwts.parserBuilder()
.setSigningKey(getKeyFromBase64EncodedKey(encodeBase64SecretKey(secretKey)))
.build()
.parseClaimsJws(token)
.getBody();
}
이제 남은 오후에는 5페이지나 되는 API명세서와 ERD도 최종수정하고, 더미데이터를 많이 만들어두면 오늘의 할일은 끝이 날 것 같다. 그동안 너무 수고가 많았고 남은 일주일은 테스트코드를 열심히 짜야겠다 화이팅 !!!
아주 유용한 정보네요!