[PreProject] [SpringSecurity], [MalFormedJwtException] ,@Lazy

NtoZ·2023년 8월 14일
0

PreProject

목록 보기
3/12

배경 소스 코드

  • SecurityConfiguration
package com.codestates.stackoverflowbe.global.auth.config;

import com.codestates.stackoverflowbe.domain.account.service.AccountService;
import com.codestates.stackoverflowbe.global.auth.filter.JwtVerificationFilter;
import com.codestates.stackoverflowbe.global.auth.handler.AccountAuthenticationSuccessHandler;
import com.codestates.stackoverflowbe.global.auth.handler.AccountAccessDeniedHandler;
import com.codestates.stackoverflowbe.global.auth.filter.JwtAuthenticationFilter;
import com.codestates.stackoverflowbe.global.auth.handler.AccountAuthenticationEntryPoint;
import com.codestates.stackoverflowbe.global.auth.handler.AccountAuthenticationFailureHandler;
import com.codestates.stackoverflowbe.global.auth.jwt.JwtTokenizer;
//import com.codestates.stackoverflowbe.global.auth.handler.OAuth2UserSuccessHandler;
import com.codestates.stackoverflowbe.global.auth.utils.CustomAuthorityUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

import java.util.Arrays;

@Configuration
public class SecurityConfiguration {

    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;
    private final AccountService accountService;

    private final CorsFilter corsFilter;

    @Lazy // accountService의 순환참조 문제 해결
    public SecurityConfiguration(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils, AccountService accountService,
                                 CorsFilter corsFilter) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
        this.accountService = accountService;
//        this.corsFilter = corsFilter;
        this.corsFilter = corsFilter;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .headers().frameOptions().sameOrigin() // (해당 옵션 유효한 경우 h2사용가능) SOP 정책 유지, 다른 도메인에서 iframe 로드 방지
                .and()
                .csrf().disable()
//                .cors(Customizer.withDefaults()) //CORS 처리하는 가장 쉬운 방법인 CorsFilter 사용, CorsConfigurationSource Bean을 제공
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 정보 저장X
                .and()
                .formLogin().disable() // CSR 방식을 사용하기 때문에 formLogin 방식 사용하지 않음
                .httpBasic().disable() // UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter 등 비활성화
                .exceptionHandling() // 예외처리 기능
                    .authenticationEntryPoint(new AccountAuthenticationEntryPoint()) // 인증 실패시 처리 (UserAuthenticationEntryPoint 동작)
                    .accessDeniedHandler(new AccountAccessDeniedHandler()) //인가 거부시 UserAccessDeniedHandler가 처리되도록 설계
                .and()
                .apply(new CustomFilterConfigurer()) // 커스터마이징한 필터 추가
                .and() // 허용되는 HttpMethod와 역할 설정
                .authorizeHttpRequests( authorize -> authorize
                        .antMatchers(HttpMethod.GET, "/accounts").hasRole("ADMIN")
                        .antMatchers(HttpMethod.POST, "/accounts/**").permitAll()
                        .anyRequest().permitAll()
//                .oauth2Login( oauth2 -> oauth2
//                        //OAuth2 인증이 성공했을 때 핸들러 처리
//                        .successHandler(new OAuth2UserSuccessHandler(jwtTokenizer, authorityUtils, accountService)) //OAuth 2.0 로그인이 성공했을 때의 동작을 정의하는 커스텀 핸들러
                );

        return httpSecurity.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }


    public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {

        @Override
        public void configure(HttpSecurity builder) throws Exception {
            // authenticationManager : 사용자가 로그인 요청시 입력한 아이디와 패스워드를 해당 객체로 전달하여 인증 수행하며, 결과에 따라 로직 처리
            AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class); // AuthenticationManager 객체얻기

            JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, jwtTokenizer); // JwtAuthenticationFilter 객체 생성하며 DI하기

            // AbstractAuthenticationProcessingFilter에서 상속받은 filterProcessurl을 설정 (설정하지 않으면 default 값인 /Login)
            jwtAuthenticationFilter.setFilterProcessesUrl("/accounts/authenticate");
            jwtAuthenticationFilter.setAuthenticationSuccessHandler(new AccountAuthenticationSuccessHandler());
            jwtAuthenticationFilter.setAuthenticationFailureHandler(new AccountAuthenticationFailureHandler());

			//⭐ 인가를 담당하는 JwtVerificationFilter이 정상적으로 동작되지 않는 것으로 보임.
            JwtVerificationFilter jwtVerificationFilter = new JwtVerificationFilter(jwtTokenizer, authorityUtils);

            // Spring Security FilterChain에 추가
            builder
                    .addFilter(corsFilter)
                    .addFilter(jwtAuthenticationFilter)
                    // OAuth2LoginAuthenticationFilter : OAuth2.0 권한 부여 응답 처리 클래스 뒤에 jwtVerificationFilter 추가 (Oauth)
                    .addFilterAfter(jwtVerificationFilter, JwtAuthenticationFilter.class);
        }
    }



}

  • JwtAuthenticationFilter
package com.codestates.stackoverflowbe.global.auth.filter;

import com.codestates.stackoverflowbe.domain.account.entity.Account;
import com.codestates.stackoverflowbe.global.auth.jwt.JwtTokenizer;
import com.codestates.stackoverflowbe.global.auth.login.dto.LoginDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Slf4j
// 클라이언트 로그인 인증 요청을 처리하는 엔트리포인트 클래스
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final AuthenticationManager authenticationManager;
    private final JwtTokenizer jwtTokenizer;

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager, JwtTokenizer jwtTokenizer) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenizer = jwtTokenizer;
    }
    /*
    *  Spring Security의 인증처리에서 토큰 생성 부분을 가로챈다.
    *  인증 위임을 해당 메서드가 오버라이딩해서 대신 객체를 전달한다.
    * */

    @SneakyThrows // checked 예외를 Runtime 예외로 변경하여 try~catch문을 사용하지 않아도 되게끔 한다.
    @Override
    //인증을 위임하기 위한 메서드
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        ObjectMapper objectMapper = new ObjectMapper(); //역직렬화 위한 ObjectMapper 인스턴스
        LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);

        log.info("# attemptAuthentication : loginDto.getUsername()={}, login.getPassword()={}",loginDto.getUsername(),loginDto.getPassword());

        // Username과 Password 정보를 포함한 미인증 토큰 발행
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginDto.getUsername(), loginDto.getPassword());

        // AuthenticationManager에 인증 시도
        return authenticationManager.authenticate(authenticationToken);
    }

    @Override
    // 인증에 성공할 경우 호출 (AccessToken, RefreshToken 생성하여 응답헤더로 반환)
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                            FilterChain chain, Authentication authResult) throws IOException, ServletException {
        // 인증이 성공되어 Authentication의 principal 필드에 할당된 Account 객체 얻어오기
        Account account = (Account) authResult.getPrincipal();

        String accessToken = delegateAccessToken(account); // AccessToken 생성
        String refreshToken = delegateRefreshToken(account); // RefreshToken 생성

        response.setHeader("Authorization", "Bearer" + accessToken); //응답헤더(Authorization)에 AccessToken을 추가
        response.setHeader("Refresh", refreshToken);

        //AuthenticationSuccessHandler의 onAuthenticationSuccess() 메서드를 호출 -> AccountAuthenticationSuccessHandler의 onAuthenticationSuccess 호출
        this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
    }

    private String delegateAccessToken(Account account) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("username", account.getEmail());
        claims.put("roles", account.getRoles());

        String subject = account.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
        String acceesToken = jwtTokenizer.generateAccessToken(claims, subject, expiration, base64EncodedSecretKey);

        return acceesToken;
    }

    private String delegateRefreshToken(Account account) {
        String subject = account.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        return refreshToken;
    }

}

  • JwtVerificationFilter (인가 필터)
package com.codestates.stackoverflowbe.global.auth.filter;

import com.codestates.stackoverflowbe.global.auth.jwt.JwtTokenizer;
import com.codestates.stackoverflowbe.global.auth.utils.CustomAuthorityUtils;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.security.SignatureException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;

public class JwtVerificationFilter extends OncePerRequestFilter { // request 당 한 번 실행
    private final JwtTokenizer jwtTokenizer;
    private final CustomAuthorityUtils authorityUtils;

    public JwtVerificationFilter(JwtTokenizer jwtTokenizer, CustomAuthorityUtils authorityUtils) {
        this.jwtTokenizer = jwtTokenizer;
        this.authorityUtils = authorityUtils;
    }

    @Override // 다음 필터 사이에 동작할 로직으로 JWT 검증 및 인증컨텍스트 저장을 수행한다.
    protected void doFilterInternal(HttpServletRequest request
            , HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            Map<String, Object> claims = verifyJws(request); // JWT 검증
            setAuthenticationToContext(claims);
        //jwt 검증에 실패할 경우 발생하는 예외를 HttpServletRequest의 속성(Attribute)으로 추가
        } catch (SignatureException se) {
            request.setAttribute("exception", se);
        } catch (ExpiredJwtException ee) {
            request.setAttribute("exception", ee);
        } catch (Exception e) {
            request.setAttribute("exception", e);
        }

        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String authorization = request.getHeader("Authorization");

        // authorization이 null이거나 Bearer로 시작하지 않으면 이 필터를 실행하지 않는다.(shouldNotFilter)
        return authorization == null || !authorization.startsWith("Bearer");
    }

    // JWT 검증
    private Map<String, Object> verifyJws(HttpServletRequest request) {
        //request의 header에서 JWT 얻기
        String jws = request.getHeader("Authorization").replace("Bearer ", "");
        //서버에 저장된 비밀키 호출
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());
        //Claims (JWT의 Payload, 사용자 정보인 username, roles 얻기) < - 내부적으로 서명(Signature) 검증에 성공한 상태
        Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody();

        return claims;
    }

    // SecurityContextHolder에 인증 정보 저장
    private void setAuthenticationToContext(Map<String, Object> claims) {
        String username = (String) claims.get("username");
        //authorityUtils의 메서드로 claims에 담긴 roles를 기반으로한 List<GrantedAuthority> 만들기
        List<GrantedAuthority> authorities = authorityUtils.createAuthorities((List)claims.get("roles"));
        // 인증 토큰을 만들어 authentication으로 어퍼 캐스팅하여 SecurityContextHolder에 저장한다.
        Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }
}

문제 상황

  • SpringSecurity를 통해 인증과 인가를 구현하던 중 문제가 발생했다.

  • 인증(JwtAuthenticationFilter) 이후 인가 단계(JwtVerificationFilter)에서 문제가 발생한다.

  • 더 정확히 말하자면, 인증 필터 (JwtAuthenticationFilter)는 액세스 토큰과 리프레쉬 토큰을 명백하게 반환한다.


    인증에 성공하지 않으면 액세스 토큰과 리프레쉬 토큰 모두 발행되지 않고 예외가 발생하므로,
    응답 헤더로 토큰을 받았다는 것은 로그인 정보돠 DB 정보가 일치해 UserDetails를 바탕으로 성공적으로 Authentication 객체가 생성되어 SpringContext에 저장되었음을 의미한다.

    • 갑작스럽게 의문이 든다. SpringContext에 정말 저장된 것이 맞을까?
  • 그런데 일반 유저 계정이나 관리자 계정 모두, 'ADMIN'이 요구되는 멤버 조회(http://{{host}}/accounts)에게 값을 요청하게 되면 UnAuthorized 예외가 발생한다. (물론 인증으로 받은 토큰을 등록하고 난 이후의 상황)

  • 그런데 401에러라고 하니, 시큐리티 예외처리에서 내가 SecurityFilterChain filterChain(HttpSecurity httpSecurity)에 등록해 놓은 .authenticationEntryPoint(new AccountAuthenticationEntryPoint())가 기억났다.

@Slf4j
public class AccountAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override // 인증 실패시 (AuthenticationException 발생) commence 메서드 동작
    public void commence(HttpServletRequest request, HttpServletResponse response,
                         AuthenticationException authException) throws IOException, ServletException {
        Exception exception = (Exception) request.getAttribute("exception");
        ErrorResponder.sendErrorResponse(response, HttpStatus.UNAUTHORIZED); // 인증 실패를 나타내는 FORBIDDEN

        logExceptionMessage(authException, exception);

//        response.sendRedirect("/accounts/login"); // 다시 로그인 폼으로 리다이렉션
    }

    private void logExceptionMessage(AuthenticationException authException, Exception exception) {
        String message = exception != null ? exception.getMessage() : authException.getMessage();
        log.warn("Unauthorized error happend : {}", message); //⭐
    }
}
  • 출력되는 로그 :
  • 아무래도 '인가 실패'라고 생각했지만 '인증이 실패'한건가? 의문이 든다. 아.. 그런데 이상하다. 분명히 인증이 성공했을 때 동작하게 만들어둔 AccountAuthenticationSuccessHandler의 동작에 따른 로그가 출력되었는데...


    인증이 문제인가, 인가가 문제인가..? 둘 다 문제인가? 미궁을 헤매는 것 같다. 그래도 내 생각은 '인가'가 문제인 것 같다. 왜냐하면 인증 실패시 동작하는 핸들러인 AccountAuthenticationFailureHandler가 동작하지 않기 때문이다. (게다가 애초에, 로그인 인증부터 실패했으면 액세스 토큰이 발급될 일이 없잖은가!)ㅡ
    안가가 문제인 것 같으므로 JwtVerificationFilter Map<String, Object> claims = verifyJws(request)에 디버깅을 찍어본다.

  • ⭐디버깅을 해본다면 이 과정에서 malformedjwtexception이 발생함을 확인할 수 있다.

    🔥malformedjwtexception은 전달되는 토큰의 값이 유효하지 않을 때 발생하는 예외이다. 실제로 삽입된 jwttokenizer가 아무런 정보도 담고 있지 않은 '맹짜'임을 확인할 수 있다.

    실제로 malformedjwtexception은 토큰에 들어온 토큰 값의 형식이 올바르지 않을 때 발생하는 예외라고 한다. 토큰에 만료시간과 비밀키가 없는데 동작하는 것이 더 신기할 것이다.

    어쨌거나, 그렇다면 💡'jwttokenizer의 DI에 문제가 생긴게 아닐까?'라는 합리적 추론에 도달하게 된다.

    (참고) 정상적으로 찍혀야 하는 JWT토크나이저 디버깅


문제 해결 과정


가설 1. SecurityConfiguration.java 의 생성자에 순환참조를 방지하기 위한 @Lazy가 문제를 야기한다.

  • 순환참조 오류가 발생하여 SecurityConfiguration 클래스의 생성자에 @Lazy 애너테이션을 붙였었다. 해당 Bean은 실제로 사용될 때까지 초기화 되지 않으므로 JwtTokenizer가 비밀키 등의 정보를 호출하지 못해 문제가 발생했다는 가설이다.
    • 그런데 애초에 인증 필터에서 Access토큰과 Refresh토큰이 만들어질 때는 비밀키를 가지고 JWT 토큰을 정상적으로 제작하다가,
      갑자기 인가 필터에서 JWT토크나이저의 의존성 주입이 제대로 되지 않는 오류가 발생하는 것을 이해하기 어렵다.

  • 그런데 말입니다. CustomFilterConfigurer에서 구현된 JwtAuthenticationFilter의 DI부분에 디버깅을 찍어보니 놀라운 일이 일어났다.


    이미 필터를 만드는 순간에서부터 토큰이 제대로 DI되지 않은 것이다.

  • (참고) 정상적인 상황 :
    이처럼, 환경변수로 세팅되어 있는 변수들 역시 디버깅할 때는 출력이 되어야 하는데... 뭘까?
    역시, @Lazy 때문인가?

  • @Lazy를 지워보자.
    주석을 해제했더니 역시나 순환참조 예외가 발생한다.

***************************
APPLICATION FAILED TO START
***************************

Description:

The dependencies of some of the beans in the application context form a cycle:

   accountController defined in file [C:\Users\LG\Desktop\Pre_Project\seb45_pre_031\be\stackoverflow-be\build\classes\java\main\com\codestates\stackoverflowbe\domain\account\controller\AccountController.class]
┌─────┐
|  accountService defined in file [C:\Users\LG\Desktop\Pre_Project\seb45_pre_031\be\stackoverflow-be\build\classes\java\main\com\codestates\stackoverflowbe\domain\account\service\AccountService.class]
↑     ↓
|  securityConfiguration defined in file [C:\Users\LG\Desktop\Pre_Project\seb45_pre_031\be\stackoverflow-be\build\classes\java\main\com\codestates\stackoverflowbe\global\auth\config\SecurityConfiguration.class]
└─────┘
  • OAuth에서 사용하고자 했던 AccountService 빈을 일단 주입받지 말자.

    🔥🔥🔥 너무 잘된다. 필터 생성자에 @Lazy를 붙였던 것이 문제였던 것이다!

  • 그러나 결과는 여전히 같은 상황이다.

가설 2. 인증 성공시, SpringContext에 Authentication이 저장되지 않았다.

  • 시큐리티 설정파일의 시큐리티필터에서 httpSecurity..exceptionHandling() .authenticationEntryPoint(new AccountAuthenticationEntryPoint()) .accessDeniedHandler(new AccountAccessDeniedHandler())로 구현된 new AccountAuthenticationEntryPoint()가 작동했던 것을 기억한다.


  • 인증은 문제가 아닐 것으로 생각된다.

    실제로 회원가입 후 로그인 인증 요청을 시도하면 인증 성공 메서드인 successfulAuthenticate()에 브레이킹 포인트가 걸린다. 토큰과 Principal 모두 디버깅에서 확인할 수 있었다. (사진 누락)

  • 역시나 인가 부분에서 malformedjwtexception 예외를 확인할 수 있다.

  • ❗❗충격적이게도 Header를 세팅해줄 때 코드가 잘못되어 있었다.

    • 인가 과정에서 verifyJws(HttpServletRequest request) 는 jwt토큰임을 알려주는 접두사인 "Bearer "를 제거하여 실제 jws를 가져오고, 해당 jws를 Map<String, Object> claims = jwtTokenizer.getClaims(jws, base64EncodedSecretKey).getBody(); 를 통해 파싱하는 과정을 담고 있다.

    • 자, 그러나 인증 과정에서 헤더에 전해준 Jws키를 확인해보자.

      여기서는 "Bearer"에 공백없이 액세스 토큰을 이어붙여서 헤더로 전달해주게 되었다.
      따라서 액세스 토큰이 이미 SecurityContextHolder에 저장된 상태이기 때문에 인증 과정은 성공적으로 완수 된 것이다.
      그러나 마지막에 Bearer를 전달해 줄때 공백 없이 액세스 토큰을 바로 이어붙여서 헤더로 전달했기 때문에 추후,
      인가 필터에서 Bearer로 시작하는 jwt토큰은 캐치해서 JwtVerificationFilter를 동작시키지만 jws 객체를 얻어서 파싱하는 과정에서,
      (String jws = request.getHeader("Authorization").replace("Bearer ", ""); )
      Bearer+(공백)인 접두사를 제거하지 못하고 그대로 다시 전달했기 때문에
      MalFormedJwtException(손상된 형태의 토큰 예외)가 발생하여 인가 과정을 실패하게 된 것이다.


소회

  • try catch문으로 catch된 예외였기 때문에 Exception이 발견되지 않았다. Exception이 아닌 논리적 에러인 줄로만 알았었는데, 디버깅을 다 각도에서 시도하다 malformedjwtexception이 디버깅 화면에 출력됨을 확인할 수 있었다.
    콘솔에 출력되지 않았다고 무작정 논리적 에러라고 볼 수 없다는 사실을 깨달았다.

  • 핸들러와 로그, 디버깅의 중요성을 깨달았다. 인증 성공, 실패 시 발생하는 로그가 아니었다면 디버깅과 추론에 한참 애를 먹고 있었을 것이다.


  • 짧은 지식으로 순환참조와 같은 문제를 해결하기 위해 함부로 @Lazy 를 붙이는 것은 위험하다. 장기적으로는 클래스 자체를 제대로 설계하는 방법을 공부해야겠다.
    💡만약 사용하더라도 필터 단계에서 @Lazy를 사용하는 것이 아닌 필터 이후 클래스 생성자 객체에서 사용을 해야할 것으로 보인다.

  • 💡하드 코딩할 때 오타를 매우 각별하게 신경써야 한다.
    하드코딩이 잘못되면 문제점을 곧바로 파악하기 힘들다.
    특히, 공백이나 순서의 바뀜, 대소문자 등 모든 케이스에 유의해서 코드를 작성해야한다.

의문점

  • 그런데 @Lazy를 시큐리티필터에 달아서 비밀키가 null이었을 때는 대체 왜 AccessToken을 생성하게 되었을까? 이유를 모르겠다.
profile
9에서 0으로, 백엔드 개발블로그

0개의 댓글