[Spring] Spring Security : JWT 로그인

최혜원·2023년 10월 18일
1

Spring

목록 보기
14/19
post-thumbnail

📍Security Filter 순서 확인하기

📍JWT 인증 처리 (Filter)

  • JwtAuthenticationFilter : 로그인 진행 및 JWT 생성
package com.sparta.springauth.jwt;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springauth.dto.LoginRequestDto;
import com.sparta.springauth.entity.UserRoleEnum;
import com.sparta.springauth.security.UserDetailsImpl;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import java.io.IOException;

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
    private final JwtUtil jwtUtil;

    public JwtAuthenticationFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
        setFilterProcessesUrl("/api/user/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        log.info("로그인 시도");
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);
			// 인증 처리를 하는 메서드
            // 여기서 select 이루어짐 → 확인하면서 비밀번호도 확인하고 실패하면 unsuccessfulAuthentication 메서드로
            return getAuthenticationManager().authenticate(
            		// 인증 객체 Token을 넣어줘야 함
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(),
                            requestDto.getPassword(),
                            null
                    )
            );
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new RuntimeException(e.getMessage());
        }
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        log.info("로그인 성공 및 JWT 생성");
        String username = ((UserDetailsImpl) authResult.getPrincipal()).getUsername();
        UserRoleEnum role = ((UserDetailsImpl) authResult.getPrincipal()).getUser().getRole();

        String token = jwtUtil.createToken(username, role);
        jwtUtil.addJwtToCookie(token, response);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        log.info("로그인 실패");
        response.setStatus(401);
    }
}

📌 첫번째 매서드 attemptAuthentication : 로그인 시도
getAuthenticationManager - UsernamePasswordAuthenticationFilter 을 상속받아서 사용할 수 있음
AuthenticationManager 가 .authenticate 라는 메서드를 가지고 있다.

📌 두번째 메서드 successfulAuthentication : 로그인 성공했을 때 수행되는 메서드 + JWT 생성
마지막에 Authentication 인증 객체를 받아온다. Authentication 여기 안에
UserDetails 가 들어있다. (전에 UserDetails와 UserDetailsService 를 AuthenticationManager 가 사용한다고 했었음!)

authResult.getPrincipal() 이렇게 코드로 직접 장성! ← ProductController 의 @AuthenticationPrincipal 사용했었음!

📌 세번째 메서드 unsuccessfulAuthentication : 로그인 실패했을 때

⭐️UsernamePasswordAuthenticationFilter 중요!
여기 들어있는 기능들을 사용하기 위해 상속받아서 JwtAuthenticationFilter

UsernamePasswordAuthenticationFilter 역할?
사용자가 username 이랑 password 를 보내면 인증객체 즉,
UsernamePasswordAuthenticationToken 을 만든 다음에
AuthenticationManager 를 통해서 확인까지!!!

직접 custom 해서 만들어 보자. 직접하는 이유?
우리는 JWT 까지 생성해줘야 하기 때문

UsernamePasswordAuthenticationFilter 이 것을 직접 사용하면
Session 방식이기 때문에!

UsernamePasswordAuthenticationFilter 를 상속받으면?
setFilterProcessesUrl 이라는 메서드를 호출할 수 있다.

 

📍JWT 인가 처리 (Filter)

  • JwtAuthorizationFilter : API에 전달되는 JWT 유효성 검증 및 인가 처리
package com.sparta.springauth.jwt;

import com.sparta.springauth.security.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
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.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;

@Slf4j(topic = "JWT 검증 및 인가")
public class JwtAuthorizationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;

    public JwtAuthorizationFilter(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
    }

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

        String tokenValue = jwtUtil.getTokenFromRequest(req);

        if (StringUtils.hasText(tokenValue)) {
            // JWT 토큰 substring
            tokenValue = jwtUtil.substringToken(tokenValue);
            log.info(tokenValue);

            if (!jwtUtil.validateToken(tokenValue)) {
                log.error("Token Error");
                return;
            }

            Claims info = jwtUtil.getUserInfoFromToken(tokenValue);

            try {
                 setAuthentication(info.getSubject());
            } catch (Exception e) {
                log.error(e.getMessage());
                return;
            }
        }

        filterChain.doFilter(req, res);
    }

    // 인증 처리
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    // 인증 객체 생성
    private Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

📌 첫번째 메서드 doFilterInternal
OncePerRequestFilter 상속 받으면 HttpServletRequest, HttpServletResponse를 받아올 수 있다.

jwtUtil.getTokenFromRequest(req); → Cookie 에서 JWT 를 가지고 있는 Cookie 가지고 오는 코드

setAuthentication() → 인증처리하는 메서드
setAuthentication(info.getSubject()); → 사용자 정보(username) 가져올 수 있음
강사님이 Token 만들 때 Subject에 user 이름을 넣었었다?

📌 두번째 메서드 setAuthentication : 인증처리
원래는 Authentication Maneger 가 Authentication 인증 객체를 만들고
SecurityContextHolder 에 넣어주고 이런 역할들을 내부적으로 원래는 다 해주는데 지금은 우리가 직접 Token 검증하고 그 다음에 인가되었다는 인가처리를 해줘야 한다. → 그래서 우리가 직접 인증 객체 만들고 SecurityContextHolder 에 직접 넣어줘야 한다.

📌 세번째 메서드 createAuthentication : 인증 객체 생성
userDetailsService.loadUserByUsername(username);-> 해당 user 가 있는지 없는지 확인/ 여기에서 사용하려고!

⭐️ 인가 되었어!
→ 너는 인증된 사용자!
다음 Filter로 넘어가도 문제없이 DispatcherServlet 통해서
Controller 까지 넘어갈 수 있어

 

📍WebSecurityConfig 필터 등록

Filter 단에서 JWT 방식으로 인증, 인가를 전부 처리하게끔 구현!!!

package com.sparta.springauth.config;

import com.sparta.springauth.jwt.JwtAuthorizationFilter;
import com.sparta.springauth.jwt.JwtAuthenticationFilter;
import com.sparta.springauth.jwt.JwtUtil;
import com.sparta.springauth.security.UserDetailsServiceImpl;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;

    public WebSecurityConfig(JwtUtil jwtUtil, UserDetailsServiceImpl userDetailsService, AuthenticationConfiguration authenticationConfiguration) {
        this.jwtUtil = jwtUtil;
        this.userDetailsService = userDetailsService;
        this.authenticationConfiguration = authenticationConfiguration;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil);
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration));
        return filter;
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService);
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

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

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 요청 모두 접근 허가
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        http.formLogin((formLogin) ->
                formLogin
                        .loginPage("/api/user/login-page").permitAll()
        );

        // 필터 관리
        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class);
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }
}

⭐️ 로그인 방식에서 JWT 방식으로 바꾸는 방법!
Spring Security 에서 기본적으로 제공하는 Session 방식이 아니라 우리가 배운 JWT 를 어떻게 Security 에 적용할 수 있는지?

이전에는 로그인 처리를 하고 JWT 를 생성해서 반환하는 걸 Controller 단에서 했었음→service→repository

📍Filter 구현
인증, 인가 처리와 비즈니스 로직 처리를 분리한다.

UserController 쪽에서 로그인을 구현하지 않고
Filter 단에서만 처리한다!

 

📍실행해보기

그 다음 메인페이지 호출됨

Token 검증 다 잘 이루어지고
인증처리에 인증 객체 생성돼서 SecurityContextHolder 에 잘 담겨서
HomeController 까지 왔다..
main 페이지에 가기 위해 만들어 놓은 Controller

profile
어제보다 나은 오늘

1개의 댓글

comment-user-thumbnail
2023년 12월 6일

전체 코드 좀 공유해주실 수 있나요

답글 달기