Skip to content

JWT 인증

MoonDooo edited this page Mar 11, 2024 · 2 revisions

JwtAuthenticationFilter

이 필터에서는 authentication_server로부터 발급한 JWT를 검증하는 것으로 서명을 확인하기 위해 restTemplate를 이용하여 해당 JWT를 보낸다. 올바른 JWT라면 해당 서버의 user의 데이터베이스 상의 id를 받아 데이터베이스에 저장한다. 이를 통해 해당 인증 서버와 user를 동기화 한다.

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final static String AUTHENTICATION_PROPERTY = "Authorization";
    private final RestTemplate restTemplate;
    private final UserRepository userRepository;

    @Value("${security.verify-server.uri}")
    private String authenticationServerUri;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String jwt = request.getHeader(AUTHENTICATION_PROPERTY);
        if (jwt != null && jwt.startsWith("bearer ")) {
            jwt =  jwt.substring(7);
        }else{
            filterChain.doFilter(request, response);
            return;
        }
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(AUTHENTICATION_PROPERTY, jwt);

        HttpEntity<?> httpEntity = new HttpEntity<>(httpHeaders);
        try {
            UserIdAndAuthorities userIdAndAuthorities = restTemplate.exchange(authenticationServerUri, HttpMethod.GET, httpEntity, UserIdAndAuthorities.class).getBody();
            Check.notNull(userIdAndAuthorities, StatusCode.Internal_Server_Error);
            Check.notNull(userIdAndAuthorities.getAuthorities(), StatusCode.Internal_Server_Error);
            Check.notNull(userIdAndAuthorities.getUserId(), StatusCode.Internal_Server_Error);
            List<GrantedAuthority> grantedAuthorities = userIdAndAuthorities.getAuthorities().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
            String userId = userIdAndAuthorities.getUserId();
            createUserIfNotFound(userId);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userIdAndAuthorities.getUserId(), null, grantedAuthorities);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            filterChain. doFilter(request, response);
        }catch (HttpClientErrorException e){
            StatusCodeMessageDto dto = e.getResponseBodyAs(StatusCodeMessageDto.class);
            Check.notNull(dto, StatusCode.Internal_Server_Error);
            StatusCode stateCode = StatusCode.getStateCode(dto);
            Check.notNull(stateCode, StatusCode.Internal_Server_Error);
            throw new CustomErrorException(stateCode);
        }
    }

    private void createUserIfNotFound(String userId) {
        if (!userRepository.existsById(Long.valueOf(userId))){
            userRepository.save(new User(Long.valueOf(userId)));
        }
    }
}

설정 값로 부터 지정한 헤더 이름으로 JWT를 가져온다. 만약 존재하지 않다면 다음 필터로 보낸다. 만약 JWT가 없다면 해당 사용자는 ANONYMOUS 권한을 얻는다 authentication_serve와 동일하게 가져온 userId와 권한들은 authentication 에 저장된다. 이후 HandlerMethodArgumentResolver을 이용하여 @AuthenticationUser를 통해 파라미터에 주입된다. 코드는 다음과 같다.

@Component
public class AuthenticationUserResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(AuthenticationUser.class)!=null&& parameter.getParameterType().equals(Long.class);
    }

    @Override
    public Long resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        return Long.parseLong(authentication.getName());
    }
}

optionl 버전

@Component
public class AuthenticationOptionalUserResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(AuthenticationUser.class)!=null&& parameter.getParameterType().equals(Optional.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!authentication.isAuthenticated() || "anonymousUser".equals(authentication.getName())) {
            return Optional.empty();
        }

        try {
            return Optional.of(Long.parseLong(authentication.getName()));
        } catch (NumberFormatException e) {
            return Optional.empty();
        }
    }
}

optional 버전은 익명이어도 접근 가능한 요청에 대해 허용하기 위해서 사용된다.

@DeleteMapping
    public ResponseEntity<Result<Boolean>> deleteComment(@AuthenticationUser Long userId, @RequestParam Long commentId){
        return Result.isSuccess(commentService.deleteComment(userId, commentId));
    }

사용 예는 다음과 같다.

모든 설정
@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .formLogin(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                .rememberMe(AbstractHttpConfigurer::disable)
                .exceptionHandling(ex->ex.authenticationEntryPoint(entryPoint))
                .authorizeHttpRequests(auth-> {
                    auth
                            .requestMatchers(HttpMethod.GET, "/comment").hasAnyRole(Roles.ANONYMOUS.role(), Roles.OAUTH2_USER.role())
                            .requestMatchers(HttpMethod.DELETE, "/comment").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers(HttpMethod.POST, "/comment").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers(HttpMethod.PUT, "/comment").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers(HttpMethod.POST, "/comment/like").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/comment/nestedComment").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/comment/nestedComment/like").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/destination/detail").hasAnyRole(Roles.OAUTH2_USER.role(),Roles.ANONYMOUS.role())
                            .requestMatchers("/destination/search").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/destination/recommend").hasAnyRole(Roles.OAUTH2_USER.role(), Roles.ANONYMOUS.role())
                            .requestMatchers("/destination/scrap/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/destination/route").hasAnyRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/post/user").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/post/likedCommented").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/post").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/post/detail").hasAnyRole(Roles.OAUTH2_USER.role(), Roles.ANONYMOUS.role())
                            .requestMatchers("/post/like/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/posts").hasAnyRole(Roles.OAUTH2_USER.role(), Roles.ANONYMOUS.role())
                            .requestMatchers("/post/scrap/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/post/search").hasAnyRole(Roles.OAUTH2_USER.role(), Roles.ANONYMOUS.role())
                            .requestMatchers("/scrap/delete").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/travelPlan/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/blockUser/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/nickname/**").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/profile/upload").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/profile").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/profile/original").hasAnyRole(Roles.OAUTH2_USER.role(), Roles.ANONYMOUS.role())
                            .requestMatchers("/scrap").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/searchHistory").hasRole(Roles.OAUTH2_USER.role())
                            .requestMatchers("/swagger-ui/**").permitAll()
                            .requestMatchers("/v3/api-docs/**").permitAll()
                            .anyRequest().hasAnyRole("OAUTH2_USER", "ANONYMOUS");
                })
                .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(customExceptionHandlerFilter, JwtAuthenticationFilter.class)
                .sessionManagement(session->session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();
    }