[인스타그램 클론코딩] 04. JWT 필터 구현(Back-End)

2023. 2. 1. 16:53·Web/인스타 클론 코딩

이전까지 회원가입 및 로그인을 구현하였고, 로그인 성공 시 JWT 토큰까지 발급하였다.

이제부터는 JWT 토큰을 가지고 서버에서는 사용자의 요청에 대한 권한 확인을 하게 될 텐데, 그 기능을 JWT 필터를 통해서 구현해본다.

 

1. UserDetails, UserDetailsService 생성

- Spring security에서 사용하는 인증 객체인 Authentication을 사용하기 위하여 UserDeatils와 UserDetails 서비스를 생성해야한다.

@Data
public class CustomUserDetails implements UserDetails {

    private User user;

    @Builder
    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> collection = new ArrayList<>();
        collection.add(new GrantedAuthority() {
            @Override
            public String getAuthority() {
                return user.getRole().getValue();
            }
        });
        return collection;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getEmail();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

- Spring Security의 UserDetails를 상속받는 CustomUserDetails를 생성 후, 내 프로젝트의 User 객체를 생성자를 통해 주입한 후 남은 메서드들을 오버라이딩해준다.

 

@Service
@RequiredArgsConstructor
@Slf4j
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException(email));

        return CustomUserDetails.builder().user(user).build();
    }
}

 

- UserDetailsService를 상속받는 CustomUserDetailsService는 User DB에서 입력받은 email로 유저를 찾아 Custom한 UserDetails 객체로 반환해주는 loadUserByUsername 메서드를 오버라이딩한다.

 

2. JwtTokenProvider 토큰 변환, 인증 기능 구현

- JwtTokenProvider 에서 입력받은 토큰에서 email 값을 뽑아내는 메서드와, 토큰이 유효한지 검증해주는 메서드를 추가한다.

@Component
@Slf4j
@RequiredArgsConstructor
public class JwtTokenProvider {
    private final CustomUserDetailsService customUserDetailsService;

	...

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token).getBody();
        String email = claims.get("email").toString();

        UserDetails principal = customUserDetailsService.loadUserByUsername(email);

        return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
    }

    public int validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token);
            return 1;
        } catch (Exception e) {
            return -1;
        }
    }
}

 

- getAutentication은 토큰에서 Claim의 이메일 값을 추출하여, 1번에서 생성한 CustomUserDetilasService의 loadUserByname 메서드의 파라미터로 넘겨주고 그 결과를 principal 이름으로 생성한다. 그 후, 스프링에서 사용하는 AuthenticationManager에서 사용하는 Authentication 객체 -> UsernamePasswordAuthenticationToken 객체에 principal과 권한 값을 주입하여 리턴한다.

- validToken은 주어진 token이 유효하면 1을, 그렇지 않다면 -1을 리턴해준다.

 

3. JwtFilter 구현

@Component
@RequiredArgsConstructor
@Slf4j
public class JwtFilter extends OncePerRequestFilter {

    public static final String AUTHORIZATION_HEADER = "Authorization";
    public static final String BEARER_PREFIX = "Bearer ";

    private static final String[] PERMIT_URL_ARRAY = {
            "v2",
            "swagger-resources",
            "configuration",
            "swagger-ui",
            "webjars",
            "v3",
            "users"
    };
    private final JwtTokenProvider jwtTokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 요청 Url의 시작이 PERMIT_URL_ARRAY에 포함되는지 검증하여, 해당되면 다음 필터로 요청을 전달한다.
        String prePath = request.getServletPath().split("/")[1];
        if(Arrays.asList(PERMIT_URL_ARRAY).contains(prePath)) {
            filterChain.doFilter(request, response);
        } else {
            String token = getToken(request);
            log.info("Token : {}", token);

            if(StringUtils.hasText(token)) {
                int flag = jwtTokenProvider.validateToken(token);

                // 토큰이 유효할 때, 인증 객체를 생성하여 SecurityContextHolder에 삽입 후, 다음 필터로 넘어간다.
                if(flag == 1) {
                    Authentication authentication = jwtTokenProvider.getAuthentication(token);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    doFilter(request, response, filterChain);
                } 
                else if (flag == -1) {
                    log.warn("토큰 값이 잘못되었습니다.");

                    response.setContentType("application/json");
                    response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                    response.setCharacterEncoding("UTF-8");
                }
            } else {
                log.warn("토큰 값이 비었습니다.");

                response.setContentType("application/json");
                response.setStatus(HttpServletResponse.SC_FORBIDDEN);
                response.setCharacterEncoding("UTF-8");
            }
        }
    }
    public String getToken(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        log.info("bearerToken : {}", bearerToken);

        if(StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }

        return null;
    }
}

 

- 요청 URL이 Swagger나 로그인, 회원가입에 관련되었다면, JWT 필터를 스킵한다.

- 토큰이 유효하다면, 스프링 시큐리티에서 제공하는 SecurityContextHolder에 JwtTokenProvider에서 생성한 Authentication 객체를 삽입 후 다음 필터로 넘어간다.

 

4. SecurityConfig 수정

- 앞에서는 WebSecurityConfigurerAdapter를 상속받아 사용했지만 이제는 deprecated 되었다고 하여 SecurityFilterChain을 빈으로 등록하여 사용하도록 수정하였다.

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtFilter jwtFilter;
    private static final String[] PERMIT_URL_ARRAY = {
            /* swagger v2 */
            "/v2/api-docs",
            "/swagger-resources",
            "/swagger-resources/**",
            "/configuration/ui",
            "/configuration/security",
            "/swagger-ui.html",
            "/webjars/**",
            /* swagger v3 */
            "/v3/api-docs/**",
            "/swagger-ui/**",
            /* Auth */
            "/users/**"
    };

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity
                .httpBasic().disable()
                .csrf().disable()
                .cors().and()
                .authorizeRequests()
                .antMatchers(PERMIT_URL_ARRAY).permitAll()
                .and()
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class).build();
    }

}

 

- 생성한 jwtFilter는 UsernamePassAuthenticationFilter가 실행 되기 전으로 순서를 위치시킨다.

 

5. @CurrentUser 생성

- 매번 SecurityContextHolder에서 Authentication 객체를 뽑아오는 것은 매우 귀찮기 때문에, AuthenticationPrincipal을 활요하여 CurrentUser 어노테이션을 생성한다.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@AuthenticationPrincipal(expression = "User")
public @interface CurrentUser {
}

 

- 이렇게 생성하면 다음부터는 아래와 같이 어노테이션으로 User 객체를 가져와 사용할 수 있다.

@RestController
@Slf4j
public class FollowController {

    @GetMapping("/follow")
    public void follow(@CurrentUser User user) {
        log.info(user.toString());
    }
}

 

저작자표시 비영리 변경금지 (새창열림)

'Web > 인스타 클론 코딩' 카테고리의 다른 글

[인스타그램 클론코딩] 05. 기본 화면 구성(Front-End)  (0) 2023.02.02
[인스타그램 클론코딩] 04. JWT 필터 구현(Front-End)  (0) 2023.02.01
[인스타그램 클론코딩] 03. 로그인 구현(Front-End)  (0) 2023.02.01
[인스타그램 클론코딩] 03. 로그인 구현(Back-End)  (0) 2023.02.01
[인스타그램 클론코딩] 02. 회원 가입 구현(Front-End)  (0) 2023.01.31
'Web/인스타 클론 코딩' 카테고리의 다른 글
  • [인스타그램 클론코딩] 05. 기본 화면 구성(Front-End)
  • [인스타그램 클론코딩] 04. JWT 필터 구현(Front-End)
  • [인스타그램 클론코딩] 03. 로그인 구현(Front-End)
  • [인스타그램 클론코딩] 03. 로그인 구현(Back-End)
뚝딱뚝딱2
뚝딱뚝딱2
  • 뚝딱뚝딱2
    개발도상국
    뚝딱뚝딱2
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 공부
        • Java
        • Spring Boot
        • LORA
      • Web
        • 인스타 클론 코딩
        • GPT 응답 API 서버
        • Spring Boot 예외 처리
        • 코테 준비용 서비스 만들기
      • DevOps
        • 쿠버네티스
        • 서버 만들기
      • 코딩테스트
        • 알고리즘
      • 교육
        • 스파르타코딩클럽 - 내일배움단
        • 혼자 공부하는 컴퓨터 구조 운영체제
      • 잡다한것
  • 블로그 메뉴

    • 홈
  • 링크

    • GITHUB
  • 공지사항

  • 인기 글

  • 태그

    스프링 부트
    Entity
    Java
    스프링부트
    MSA
    클론코딩
    오블완
    chat GPT
    리액트
    mapstruct
    spring boot
    react
    REST API
    클러스터
    인스타그램
    백준
    예외
    쿠버네티스
    티스토리챌린지
    OpenAI API
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
뚝딱뚝딱2
[인스타그램 클론코딩] 04. JWT 필터 구현(Back-End)
상단으로

티스토리툴바