[Spring] Spring Security (6)CustomSecurityFilter 적용해보기

hyewon jeong·2023년 1월 2일
1

Spring

목록 보기
7/59

WebSecurityConfig

package com.sparta.springsecurity.config;


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig {

		private final UserDetailsServiceImpl userDetailsService;

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

		@Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // h2-console 사용 및 resources 접근 허용 설정
        return (web) -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console())
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();
        
        http.authorizeRequests().antMatchers("/api/user/**").permitAll()
																.anyRequest().authenticated();

        // Custom 로그인 페이지 사용
        http.formLogin().loginPage("/api/user/login-page").permitAll();

				// Custom Filter 등록하기
				http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);
        
        return http.build();
    }

}

addFilterBefore

지정된 필터 앞에 커스텀 필터를 추가 (UsernamePasswordAuthenticationFilter 보다 먼저 실행된다)

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
      

        // Custom Filter 등록하기
        http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);
  • UsernamePasswordAuthenticationFilter에서 기본적으로는 username/password방식으로 인증처리가 되지만
    CustomSecurityFilter를 사용할 예정으로
    이제 우리가 적용할 프로젝트에는 JWT토큰을 사용할 것이기 때문에 JWT토큰을 검증할 필터도 만들어서 추가를 할 것이다.

  • UsernamePasswordAuthenticationFilter.class 보다 CustomSecurityFilter를 먼저 실행하게 한다.

일반적인 Spring Security의 인증 과정은 다음과 같습니다:

  1. 클라이언트로부터 인증 요청이 들어옵니다.
  2. UsernamePasswordAuthenticationFilter가 요청을 가로채어 사용자가 입력한 인증 정보를 확인합니다.
  3. AuthenticationManager를 통해 사용자 인증을 수행합니다.
  4. 인증이 성공하면 Authentication 객체가 생성됩니다.
  5. Authentication 객체는 SecurityContextHolder에 저장되어 현재 인증된 사용자 정보를 관리합니다.
  6. 요청은 다음 필터로 계속 전달됩니다.
  http.addFilterBefore(new CustomSecurityFilter(userDetailsService, passwordEncoder()), UsernamePasswordAuthenticationFilter.class);

📌 이렇게 설정을 하면 이 CustomSecurityFilter가 UsernamePasswordAuthenticationFilter를 돌기 전에 먼저 진행이 되어
📌 성공한 경우 Authentication 객체를 생성하여 SecurityContextHolder에 저장한다.
📌 따라서 이후 실행되는 UsernamePasswordAuthenticationFilter는 이미 인증이 완료된 상태이므로, 다음 필터로 계속 전달되면서 컨트롤러까지 요청이 지나갈 수 있다.

📌 즉, CustomSecurityFilter는 사용자 인증을 처리하여 SecurityContextHolder에 인증 정보를 저장하고, 이후의 필터 및 처리 과정에서 이 인증 정보를 활용할 수 있게 된다. 이는 인증이 완료된 상태에서 요청이 계속 처리될 수 있도록 하는 역할을 한다.

UserController.java

package com.sparta.springsecurity.controller;

import com.sparta.springsecurity.dto.SignupRequestDto;
import com.sparta.springsecurity.entity.User;
import com.sparta.springsecurity.entity.UserRoleEnum;
import com.sparta.springsecurity.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.Optional;

@Controller
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {

    private final PasswordEncoder passwordEncoder;
    private final UserRepository userRepository;

    // ADMIN_TOKEN
    private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";

    @GetMapping("/signup")
    public ModelAndView signupPage() {
        return new ModelAndView("signup");
    }

    @GetMapping("/login-page")
    public ModelAndView loginPage() {
        return new ModelAndView("login");
    }

		@PostMapping("/signup")
    public String signup(SignupRequestDto signupRequestDto) {

        String username = signupRequestDto.getUsername();
        String password = passwordEncoder.encode(signupRequestDto.getPassword());

        // 회원 중복 확인
        Optional<User> found = userRepository.findByUsername(username);
        if (found.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // 사용자 ROLE 확인
        UserRoleEnum role = UserRoleEnum.USER;
        if (signupRequestDto.isAdmin()) {
            if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
                throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
            }
            role = UserRoleEnum.ADMIN;
        }

        User user = new User(username, password, role);
        userRepository.save(user);

        return "redirect:/api/user/login-page";
    }
  
}

LoginRequestDto.java

package com.sparta.springsecurity.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class LoginRequestDto {
    private String username;
    private String password;
}

SignupRequestDto.java

package com.sparta.springsecurity.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class SignupRequestDto {
    private String username;
    private String password;
    private boolean admin = false;
    private String adminToken = "";
}

시큐리티 커스텀하여 사용하기

CustomSecurityFilter.java

package com.sparta.springsecurity.security;

import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
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;

@RequiredArgsConstructor
public class CustomSecurityFilter extends OncePerRequestFilter {

    private final UserDetailsServiceImpl userDetailsService;
    private final PasswordEncoder passwordEncoder;


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

        String username = request.getParameter("username");
        String password = request.getParameter("password");

        System.out.println("username = " + username);
        System.out.println("password = " + password);
        System.out.println("request.getRequestURI() = " + request.getRequestURI());


        if(username != null && password  != null && (request.getRequestURI().equals("/api/user/login") || request.getRequestURI().equals("/api/test-secured"))){
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            // 비밀번호 확인
            if(!passwordEncoder.matches(password, userDetails.getPassword())) {
                throw new IllegalAccessError("비밀번호가 일치하지 않습니다.");
            }

            // 인증 객체 생성 및 등록
            SecurityContext context = SecurityContextHolder.createEmptyContext();
            Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            context.setAuthentication(authentication);

            SecurityContextHolder.setContext(context);
        }

        filterChain.doFilter(request,response);
    }
}




doFilterInternal()

기존에 있는 필터를 상속받아서 필터를 만들면 doFilterInternal함수를 @Override재정의해서 사용하게 된다.

@RequiredArgsConstructor
public class CustomSecurityFilter extends OncePerRequestFilter {
private final UserDetailsServiceImpl userDetailsService;
private final PasswordEncoder passwordEncoder;

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

🔔 파라미터 3개 : 🔑request, 🔒response, ⛓filterChain 있는데
주의 깊게 볼 점은
우리가 API요청이 오면 HTTP객체가 필터를 타고서 controller까지 들어온다라고 했으므로 파라미터로 🔑request를 받는 것이고

filterChain 은 필터가 체인형식으로 연결되어 있는데 이것을 사용해서 필터끼리 이동을 한다.
이 필터가 끝나고 나면 다음 필터로 넘어가게 된다.

filterChain.doFilter()

filterChain.doFilter를 사용해서 여기에 request, response 를 담아서 다음 필터로 이동 시켜준다.

🎈만약에 여기서 예외처리가 발생이 되면 이 doFilter를 타고 다음 filter가 아니라 그 이전에 filter로 예외가 넘어간다.

credential쪽에는 위에서 검증이 끝났기 때문에 넣어줄 필요가 없어 null값으로 넣어준다.

  • CustomSecurityFilter와 앞으로 MySelectShop에서 적용할 JwtAuthFilter의 차이점 미리 읽어보기

🔥 우리의 프로젝트에서는 토큰을 사용해서 인증/인가를 구현했습니다. 따라서 지금 실습환경과는 다르게 Filter 에서 ID, PWD가 아니라 토큰을 검증해야 하는데 로그인 요청에는 당연히 토큰이 없죠? 따라서 우리의 프로젝트에서는 로그인 검증은 서비스에서 진행이 되고 로그인 성공 후 반환된 토큰이 인증이 필요한 API 요청과 같이 들어왔을 때 토큰을 검증하여 사용자를 인증처리 해주는 JwtAuthFilter로 적용 할 예정입니다.
즉, 정리를 하자면 실습에서는 토큰방식이 적용되어 있지 않기 때문에 Filter 에서 사용자가 요청한 로그인을 검증하고 인증하고 있지만 토큰방식을 적용하게 되면 사용자의 로그인,회원가입과 같은 요청은 Filter에서 인증되지 않게 permitAll 처리하여 실제 검증 및 인증 처리는 service 에서 수행하고 그외의 인증이 필요한 요청 에서는 로그인을 통해 발급받은 토큰을 같이 보내 Filter에서 토큰을 검증하고 인증처리를 하게 됩니다.

-참고 : 스파르타코딩클럽 강의

profile
개발자꿈나무

0개의 댓글