MainProject 5

김소희·2023년 7월 19일
1

Day 15

지난 일주일 동안 블로그에 작성할 내용이 떠올랐지만, 그것을 글로 옮기기에는 에너지가 부족했다. 나는 하루 종일 프로젝트에만 집중해야 했고, 6시가 넘어서도 에러를 해결하느라 게임도 많이 못했다. 😓
프론트엔드 팀원 중 한 명은 취업으로 인해 우리 팀을 떠났고, 하나뿐인 백엔드 팀원은 실력이 부진하여 기여를 못하는 터라 나 혼자서 프로젝트를 진행해야 했다. 우리 프로젝트는 실질적으로 3인 프로젝트가 된 셈이다. 그로인해 프로젝트가 실패하지 않을까 하는 걱정과 나의 부족한 실력으로 인한 답답한 마음이 항상 있었다.

코드스테이츠의 운영진들이나 멘토님들과 여러 차례 상담을 받았는데, 내가 해내지 못하면 프로젝트가 실패하는데 내 능력이 모자라서 분하고 속상하다는 마음을 이야기하다가 울곤 했다. 그 말을 들은 운영진들은 멋진 속상함을 느끼고 있는 것 같다고, 소희님은 책임감이 강한 사람이라서 개발자로서 포텐셜이 높은 사람인 것 같으니 믿어주시겠다며 힘든점을 언제든 들어주시겠다며 많은 위로와 격려와 공감과 응원을 해주셨고 정말로 많은 힘이 되었다.
나는 추가적인 도움을 받지않고 혼자서 프로젝트를 해내보겠다고 말씀드렸고, 마감까지 일주일이 남은 오늘, ngrok으로 배포한 후 프론트엔드 쪽에서 테스트를 해 보았다. 잘 작동하는 것을 확인하고서야 가벼운 마음으로 블로그를 작성하러 올 수 있었다.

결국 모든 오류들을 해결하게 되어서 너무너무 기쁘고 스스로가 대견하다. (난 짱이야!!!😊)
프론트와 연결한 이후에는 사소한 요청들을 수정해나가고 있는데 연결이 된 것 만으로도 뿌듯해서 너무나 행복하게 코딩할 수 있다. 이 맛에 프로젝트를 하는 것 같다.

오늘 해결한 오류들

cors에러를 지난번 프로젝트에서 해결했던 터라 이번에는 쉽게 넘어갈 줄 알았는데 또 cors에러가 뜬다고해서 관련된 블로그를 참 많이 살펴보고 코드를 거의 50번이상 수정 했었지만 고쳐지지 않았는데 결국 찾아낸 방법은 프론트에서 요청할 때 헤더에 'ngrok-skip-browser-warning': '69420' 넣어서 해결했다. 무료로 ngrok을 사용하기때문에 설정해주어야 하는 부분인 것 같다.

CORS 설정

@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를 추가하기

이제는 필터가 어떤 역할을 하는지 알아서인지 응답헤더에 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도 최종수정하고, 더미데이터를 많이 만들어두면 오늘의 할일은 끝이 날 것 같다. 그동안 너무 수고가 많았고 남은 일주일은 테스트코드를 열심히 짜야겠다 화이팅 !!!

profile
백엔드 자바 개발자 소희의 노트

1개의 댓글

comment-user-thumbnail
2023년 7월 19일

아주 유용한 정보네요!

답글 달기