Spring

Spring Security

장진혁 2023. 3. 15. 01:11
스프링 심화에서 Spring Security 라이브러리를 추가해서
jwt토큰을 이용한 인증/인가 기능을 추가할려고한다.

스프링 시큐리티 config설정과
커스텀한 필터를 위주로 기록했다.
WebSecurityConfig 안에 시큐리티 기능 중에 어떤것을 활성화 할것인지 또한 어떤 API를 인증 없이 통과 할 것인지 명시했다.
코드에 관한 설정은 내가 최대한 이해한 내용을 바탕으로 주석으로 작성했다.

설정에서 핵심은
비밀번호를 암호화 할때 단방향으로 설정하고 평문을 암호화 하고 다시 평문으로 되돌릴 수 없다.

로그인, 회원가입, 조회는 인증이 필요없는 부분이기 때문에 인증필터를 통과한다.
를 제외한 모든 것은 인증을 거처야한다.
@Configuration
@RequiredArgsConstructor
// 스프링 Security 지원을 가능하게 함
@EnableWebSecurity
// 메소드 보안 활성화
// @Secured 어노테이션 활성화
// 메소드 레벨에서 권한(Authority)을 설정할 수 있다.
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 비밀번호 암호화 기능 등록
        // 적응형 단방향 함수를 사용
        // 단방향은 평문에서 암호화가 가능하지만 다시 평문으로 돌리는 것은 불가능하다.
        return new BCryptPasswordEncoder();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // h2-console 사용 및 resources 접근 허용 설정
        // 밑에 있는 SecurityFilterChain 보다 우선적으로 걸리는 설정이다.
        // h2-console 사용 및 resources 접근 허용 설정
        return (web) -> web.ignoring() // ignoring 인증처리를 무시하겠다.
                .requestMatchers(PathRequest.toH2Console())
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        // CSRF(Cross-Site Request Forgery, 사이트 간 요청 위조)
        // 웹 애플리케이션 취약점 중 하나로, 인증된 사용자의 권한을 사용하여 악성 요청을 전송하는 공격
        http.csrf().disable();

        // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        http.authorizeRequests()
                // 로그인, 회원가입 기능 인증없이 허용
                .antMatchers("/post/user/**").permitAll()

                // 게시글 전체, 단건 조회는 인증없이 허용
                .antMatchers("/posts/list/**").permitAll()

                // 이외의 URL요청들은 전부 다 authenticated 인증 처리를 하겠다.
                .anyRequest().authenticated()

                // JWT 인증/인가를 사용하기 위한 설정
                // Custom Filter 등록하기
                // addFilterBefore - (어떠한 Filter 이전에 Filter를 먼저 실행 및 추가하겠다.)
                // 즉 JwtAuthFilter를 UsernamePasswordAuthenticationFilter 실행 전에 사용하도록 설정
                // UsernamePasswordAuthenticationFilter 이것으로 인증처리가 되지만
                // JWT토큰을 사용하는 시큐리티 커스텀 필터를 사용할 것이다.
                .and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
        // UsernamePasswordAuthenticationFilter
        // Form Login 기반이 사용되고 이것은 인증이 필요한 URL요청이 들어왔을 때
        // username 과 password 가 인증이 안되어 있으면 로그인페이지가 반환


        // 내장 기본 로그인 사용
//        http.formLogin();
        // Custom 로그인 페이지 사용
//        http.formLogin().loginPage("/api/user/login-page").permitAll();
        // 접근 제한 페이지 이동 설정
//        http.exceptionHandling().accessDeniedPage("/api/user/forbidden");

        return http.build();
    }
다음 코드는 커스텀한 필터인데 클라이언트에서 보낸 내용이 필터에 먼저 도달하고 
wtAuthFilter(jwtUtil) 필터에 먼저 처리 된다.
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter { // 기본 필터 사용됨

    private final JwtUtil jwtUtil;

    // API요청이 오면 HTTP객체가 Filter를 타고서 controller까지 온다.(HTTP객체를 파라미터로 받는다.)
    // filterChain은 Filter가 Chain형식으로 연결되어 Filter끼리 이동한다.
    // 이 Filter가 끝나고 나면 다음 Filter로 이동을 해야한다.
    // 이 Filter 마지막 줄 doFilter(request,response) 를 통해서 request와 response를 담아서 다음 필더로 이동
    // 이 Filter에서 예외처리가 되면 이전 Filter로 예외가 넘어간다.
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        String token = jwtUtil.resolveToken(request);

        // 로그인 회원가입하는 부분은 인증이 필요없다.
        // 토큰이 헤더에 없기때문에 토큰을 검증하는 부분에서 예외가 터진다.
        // 그래서 if문 분기 처리를 해준다.
        // 인증이 필요없는 것은 다음 필터로 넘어간다.
        if (token != null) {
            if (!jwtUtil.validateToken(token)) {
                // 밑에 있는 jwtExceptionHandler 를 통해서 클라이언트로 반환
                jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
                return;
            }
            Claims info = jwtUtil.getUserInfoFromToken(token);
            setAuthentication(info.getSubject());
        }
        filterChain.doFilter(request, response);
    }

    // 인증 객체 생성
    // 인증이 만들어지면 SecurityContextHolder 에 인증이 된다.
    // SecurityContextHolder 안에 SecurityContext 인증 객체가 들어있다.
    // 다음 필터로 이동했을때 이 요청은 인증 했다고 인식해서 컨트롤러로 넘어간다.
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = jwtUtil.createAuthentication(username);
        context.setAuthentication(authentication);
        SecurityContextHolder.setContext(context);
        // SecurityContextHolder 는 Spring Secutity 로 인증한
        // 사용자의 상세정보를 가지고있는 컨테이너이다.
    }

    // 토큰에 대한 오류가 발생했을때 클라이언트로 예외 처리 값을 알려준다.
    public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
        response.setStatus(statusCode);
        response.setContentType("application/json");
        try {
            String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }
인증 객체 생성
// 인증 객체 생성
    public Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
밑에 있는 코드는 필터를 거치고 인증이 완료되면
컨트롤러에서 @AuthenticationPrincipal 어노테이션을 이용하여 
인증된 사용자의 정보를 주입한다.
/**
     * @AuthenticationPrincipal 컨트롤러 메서드의 파라미터로 현재 인증된 사용자의 정보를 주입
     */

    // 게시글 저장 name content password
    @PostMapping("/create")
    public PostResponseDto createPost(
            @RequestBody PostRequestDto postRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails) {
        return postService.createPost(postRequestDto, userDetails.getUser());
    }
이 코드 이후로 서비스 계층으로 넘어가 비즈니스 로직을 수행한다.