항해99

[항해99 TIL] Spring Security

Luke_look 2023. 7. 6. 00:27

1. 필터

Untitled_(3)

그림을 보게 되면 클라이언트에서 오는 요청과 응답을 최초와 최종에 있으며 응답의 정보를 바꾸거나 부가적인 기능을 추가할 수 있습니다. 스프링 프레임워크에서는 서블릿 필터를 의미하며 클라이언트로부터의 요청이 서버의 특정 URL 패턴에 매칭될 때마다 자동으로 실행되는 자바 클래스입니다.

사용 예시로는

라이언트 요청에 대한 로깅, 인증 및 권한 확인, 데이터 압축, 문자 인코딩 설정, CSRF 공격 방지 등의 작업을 수행.

1) LoggingFilter

// 'topic' 매개변수로 로깅 주제를 지정할 수 있습니다.
@Slf4j(topic = "LoggingFilter") 
@Component
// 필터의 실행 순서를 지정하는 데 사용
@Order(1) 

// Filter 인터페이스를 구현하는 LoggingFilter 클래스를 선언합니다. 
public class LoggingFilter implements Filter { 

    // ServletRequest와 ServletResponse를 매개변수로 받아 필터링 작업을 수행하는 메소드입니다.
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { 
        // 전처리 부분 시작

        // 들어온 ServletRequest를 HttpServletRequest로 형변환
        // HTTP 요청에 관한 메소드를 사용
        HttpServletRequest httpServletRequest = (HttpServletRequest) request; 
        // HttpServletRequest에서 요청한 URI를 가져옵니다.
        String url = httpServletRequest.getRequestURI(); 
        // 가져온 URI 정보를 로그로 출력합니다.
        log.info(url); 

        // 전처리 부분 끝
        // 현재 필터 다음에 있는 필터로 요청과 응답을 전달
        // 만약 다음 필터가 없다면 실제 요청을 처리하는 서블릿이나 컨트롤러가 호출됩니다.
        chain.doFilter(request, response); 

        // 후처리 부분 시작
        // "비즈니스 로직 완료" 메시지를 로그로 출력합니다.
        log.info("비즈니스 로직 완료"); 
        // 후처리 부분 끝
    }
}

2) AuthFilter

@Slf4j(topic = "AuthFilter")
@Component
@Order(2)

public class AuthFilter implements Filter {

    // UserRepository와 JwtUtil에 대한 참조를 보관하는 인스턴스 변수
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;

    // UserRepository와 JwtUtil을 매개변수로 받는 생성자
    public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;
    }

    // ServletRequest와 ServletResponse를 매개변수로 받아 필터링 작업을 수행하는 메소드
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String url = httpServletRequest.getRequestURI();        // 요청 URL을 가져옵니다.

        // URL이 존재하고, 해당 URL이 "/api/user", "/css", "/js"로 시작하는 경우 
        if (StringUtils.hasText(url) &&
                (url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))    
        ) {
            // 회원가입, 로그인 관련 API 는 인증 필요 없이 요청 진행
            chain.doFilter(request, response); // 다음 Filter로 이동
        } else {
            // 나머지 API 요청은 인증 처리를 진행
            // JWT 토큰을 가져옵니다.
            String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest);

            // 토큰이 존재하는 경우 토큰 검증을 시작합니다.
            if (StringUtils.hasText(tokenValue)) { 
                // JWT 토큰을 substring 합니다.
                String token = jwtUtil.substringToken(tokenValue);

                // 토큰의 유효성을 검증합니다.
                if (!jwtUtil.validateToken(token)) {
                    throw new IllegalArgumentException("Token Error");
                }

                // 토큰에서 사용자 정보를 가져옵니다.
                Claims info = jwtUtil.getUserInfoFromToken(token);

                // 사용자를 찾아 저장합니다.
                User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
                        new NullPointerException("Not Found User")
                );

                // request의 attribute로 사용자 정보를 설정합니다.
                request.setAttribute("user", user);
                chain.doFilter(request, response); // 다음 Filter로 이동
            } else {
                // 토큰이 존재하지 않는 경우 예외를 발생시킵니다.
                throw new IllegalArgumentException("Not Found Token");
            }
        }
    }
}

2. 스프링 시큐리티

'Spring Security' 프레임워크는 Spring 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어 줍니다. 마치 'Spring' 프레임워크가 웹 서버 구현에 편의를 제공해 주는 것과 같습니다.

예제로 나온 부분 부터 쭉 따라가보겠습니다.

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'

추가 되어있지만 이 의존성을 주입하면 사용할 수 있습니다.

1)

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

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

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용
        http.formLogin(Customizer.withDefaults());

        return http.build();
    }
}

2) WebSecurityConfig

@Configuration
@EnableWebSecurity // Spring Security 지원을 활성화합니다.
public class WebSecurityConfig {

    @Bean // Spring의 Bean으로 생성하도록 선언합니다. 
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 
        // CSRF 설정을 비활성화합니다.
        http.csrf((csrf) -> csrf.disable());

        // 요청에 대한 인증 규칙을 설정합니다.
        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        // 공통 위치에 대한 정적 리소스에 대한 요청은 모두 허용합니다.
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() 
                        // 그 외의 모든 요청은 인증을 받도록 설정합니다.
                        .anyRequest().authenticated()
        );

        // 로그인 설정을 기본값으로 사용합니다.
        http.formLogin(Customizer.withDefaults());

        return http.build(); // HttpSecurity 객체를 생성하여 반환합니다.
    }
}
  1. http.authorizeHttpRequests((authorizeHttpRequests) ->는 Spring Security에게 HTTP 요청에 대한 인증 규칙 설정

  2. .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()

    requestMatchers : RequestMatcher 인스턴스들에 대해 설정

    PathRequest.toStaticResources().atCommonLocations() : 정적 리소스(그림, js등)에 대한 요청에 매치 되는 RequestMatcher를 반환. atCommonLocations()는 경로에 대한 요청

    .permitAll() : 사용자가 접근 가능하게끔

  3. .anyRequest().authenticated()

    anyRequest(): 모든 요청에 대해 설정합니다.

    .authenticated() 인증된 유저만 접근 가능

3) LoggingFIlter

@Slf4j(topic = "LoggingFilter") // Lombok의 로깅 유틸리티,"LoggingFilter"
@Order(1) // 필터의 실행 순서를 정의
public class LoggingFilter implements Filter { // Filter 인터페이스를 구현한 LoggingFilter 클래스
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 전처리
        HttpServletRequest httpServletRequest = (HttpServletRequest) request; // ServletRequest를 HttpServletRequest로 형변환
        String url = httpServletRequest.getRequestURI(); // 요청받은 URL을 가져옵니다.
        log.info(url); // 요청받은 URL을 로그로 남깁니다.

        chain.doFilter(request, response); // 다음 Filter로 이동합니다.

        // 후처리
        log.info("비즈니스 로직 완료"); // 비즈니스 로직이 완료되었음을 로그로 남깁니다.
    }
}

4) AuthFilter

@Slf4j(topic = "AuthFilter") // Lombok의 로깅 유틸리티
@Order(2) // 필터의 실행 순서를 정의
public class AuthFilter implements Filter { 
    private final UserRepository userRepository; // UserRepository를 사용
    private final JwtUtil jwtUtil; // JwtUtil을 사용하여 JWT 토큰을 처리합니다.

    // 생성자를 통해 UserRepository와 JwtUtil을 주입받습니다.
    public AuthFilter(UserRepository userRepository, JwtUtil jwtUtil) {
        this.userRepository = userRepository;
        this.jwtUtil = jwtUtil;
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request; // ServletRequest를 HttpServletRequest로 변환
        String url = httpServletRequest.getRequestURI(); // 요청받은 URL을 가져옵니다.

        // URL이 "/api/user", "/css", "/js"로 시작하는 경우, 인증없이 요청을 진행합니다.
        if (StringUtils.hasText(url) && (url.startsWith("/api/user") || url.startsWith("/css") || url.startsWith("/js"))) {
            chain.doFilter(request, response); // 다음 Filter로 이동합니다.
        } else {
            // 그 외의 요청은 JWT 토큰을 확인하여 인증 처리를 합니다.
            String tokenValue = jwtUtil.getTokenFromRequest(httpServletRequest); // 토큰을 가져옵니다.

            // 토큰이 존재하는 경우
            if (StringUtils.hasText(tokenValue)) {
                String token = jwtUtil.substringToken(tokenValue); // 토큰을 추출합니다.

                // 토큰 검증
                if (!jwtUtil.validateToken(token)) {
                    throw new IllegalArgumentException("Token Error"); // 토큰이 유효하지 않으면 예외를 던집니다.
                }

                // 토큰에서 사용자 정보를 가져옵니다.
                Claims info = jwtUtil.getUserInfoFromToken(token);

                // 사용자 정보를 데이터베이스에서 찾습니다.
                User user = userRepository.findByUsername(info.getSubject()).orElseThrow(() ->
                        new NullPointerException("Not Found User")
                );

                request.setAttribute("user", user); // 요청에 사용자 정보를 추가합니다.
                chain.doFilter(request, response); // 다음 Filter로 이동합니다.
            } else {
                throw new IllegalArgumentException("Not Found Token"); // 토큰이 없으면 예외를 던집니다.
            }
        }
    }
}

아 모르겠다

일단 시큐리티 개념을 보겠습니다.

5) Spring Security

(1) 필터 체인

밑에 그림을 보면 FilterChain 영역이 보이실 것입니다.

스프링의 모든 호출은 Servlet 통과하는데 이때 클라이언트의 요청이 리소스에 도달하기 전에 통과하는 서블릿 필터를 FilterChain입니다.

img

몇가지 필터를 알아보면

  1. SecurityContextPersistenceFilter : SecurityContext를 HTTP 세션에서 로드하고 저장하는 역할
  2. UsernamePasswordAuthenticationFilter : form-based 로그인 인증
  3. CsrfFilter : Cross-Site Request Forgery (CSRF) 공격을 방어
  4. ExceptionTranslationFilter : Spring Security에서 발생하는 예외를 적절한 방식으로 처리
  5. FilterSecurityInterceptor : HTTP 요청에 대한 접근 제어 결정을 수행하는 데 사용

(2) Form Login 기반은 인증

img

Form Login 기반 인증은 인증이 필요한 URL 요청이 들어왔을 때 인증이 되지 않았다면 로그인 페이지를 반환하는 형태입니다. 쉽게 말하면 아이디 비번 치는 거

여기서 순서대로 정리하면

  1. GET /private: 클라이언트(예: 웹 브라우저)는 GET /private와 같은 보호된 리소스에 대한 HTTP 요청을 보냅니다.
  2. AccessDeniedException: 이 요청이 FilterSecurityInterceptor 필터를 통과하려고 시도할 때, 사용자가 아직 인증되지 않았다는 사실을 알게 되면 AccessDeniedException을 발생시킵니다.
  3. ExceptionTranslationFilter: 이 필터는 AccessDeniedException을 인터셉트하고, AuthenticationEntryPoint를 사용하여 인증 프로세스를 시작합니다. 기본적으로 ExceptionTranslationFilterLoginUrlAuthenticationEntryPoint를 사용하며, 이는 로그인 페이지로 리다이렉션하는 역할을 합니다.
  4. Location: /login: LoginUrlAuthenticationEntryPoint/login URL로 리다이렉션하라는 응답을 클라이언트에 보냅니다.
  5. GET /login: 클라이언트는 리다이렉션에 따라 /login URL로 GET 요청을 보냅니다.
  6. LoginController: /login 요청은 LoginController에 의해 처리됩니다. 이 컨트롤러는 보통 로그인 양식을 포함하는 로그인 페이지(login.html)를 반환합니다.

img

여기서 UsernamePasswordAuthenticationFilter라는 개념이 나오는데

Form-Based 로그인을 처리하는데 사용됩니다. 이 필터는 HTTP 요청에서 사용자 이름과 비밀번호를 추출하고, 이를 사용하여 Authentication 객체를 생성합니다. 이 객체는 그런 다음 AuthenticationManager에 의해 처리되어 인증 과정이 진행합니다

(3) SecurityContextHolder

1)SecurityContextHolder

img

SecurityContext는 인증이 완료된 사용자의 상세 정보(Authentication)를 저장

2) Authentication

  • 현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다.
  • principal : 사용자를 식별합니다.
    • Username/Password 방식으로 인증할 때 일반적으로 UserDetails 인스턴스입니다.
  • credentials : 주로 비밀번호, 대부분 사용자 인증에 사용한 후 비웁니다.
  • authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용합니다.

3) UserDetailsService

username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환

4) UserDetails

UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅됩니다. Custom하여 사용가능합니다.

3. Spring Security : 로그인

img

그림 기깔나게 뽑은 스파르타에게 감사.

  • Spring Security 역할
    • 인증/인가
      1. 성공 시: Controller 로 Client 요청 전달
        1. Client 요청 + 사용자 정보 (UserDetails)
      2. 실패 시: Controller 로 Client 요청 전달되지 않음
        1. Client 에게 Error Response 보냄

img

로그인 처리까지 그렇다면 코드로 보겠습니다.

1) WebSecurityConfig

@Configuration // 이 클래스를 Spring의 설정 클래스로 선언합니다.
@EnableWebSecurity // Spring Security 지원을 활성화합니다.
public class WebSecurityConfig {

    @Bean // Spring의 Bean으로 생성하도록 선언합니다. 
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 
        // HttpSecurity를 객체로 받음.
        // CSRF 설정을 비활성화합니다.
        http.csrf((csrf) -> csrf.disable());

        // 요청에 대한 인증 규칙을 설정합니다.
        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        // 공통 위치에 대한 정적 리소스에 대한 요청은 모두 허용합니다.
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() 
                        // 그 외의 모든 요청은 인증을 받도록 설정합니다.
                        .anyRequest().authenticated()
        );

        // 로그인 설정을 기본값으로 사용합니다.
        http.formLogin(Customizer.withDefaults());

        return http.build(); // HttpSecurity 객체를 생성하여 반환합니다.
    }
}
  1. http.authorizeHttpRequests((authorizeHttpRequests) ->는 Spring Security에게 HTTP 요청에 대한 인증 규칙 설정

  2. .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()

    requestMatchers : RequestMatcher 인스턴스들에 대해 설정

    PathRequest.toStaticResources().atCommonLocations() : 정적 리소스(그림, js등)에 대한 요청에 매치 되는 RequestMatcher를 반환. atCommonLocations()는 경로에 대한 요청

    .permitAll() : 사용자가 접근 가능하게끔

  3. .anyRequest().authenticated()

    anyRequest(): 모든 요청에 대해 설정합니다.

    .authenticated() 인증된 유저만 접근 가능

2) UserDetailsServiceImpl

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;

    // UserRepository를 의존성 주입을 통해 받아옵니다.
    public UserDetailsServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // UserRepository를 사용해 DB에서 username에 해당하는 User 객체를 찾습니다.
        // User 객체가 존재하지 않을 경우 UsernameNotFoundException을 발생시킵니다.
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        // 찾아진 User 객체를 사용해 UserDetailsImpl 객체를 생성하고 반환합니다.
        // UserDetailsImpl 객체는 Spring Security에서 사용하는 인증 정보를 담고 있습니다.
        return new UserDetailsImpl(user);
    }
}

UserDetailsServiceImpl 클래스는 UserDetailsService 인터페이스를 구현하고 있습니다. UserDetailsService는 Spring Security에서 사용자의 정보를 가져오는 방법을 정의한 인터페이스

3) UserDetailsImpl

public class UserDetailsImpl implements UserDetails {

    private final User user;

    // User 객체를 받아 생성합니다.
    public UserDetailsImpl(User user) {
        this.user = user;
    }

    public User getUser() {
        return user;
    }

    // User 객체의 password를 반환합니다.
    @Override
    public String getPassword() {
        return user.getPassword();
    }

    // User 객체의 username을 반환합니다.
    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // User 객체의 권한 정보를 반환합니다.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        UserRoleEnum role = user.getRole();
        String authority = role.getAuthority();

        SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);        //권한 문자열
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        authorities.add(simpleGrantedAuthority);

        return authorities;
    }

    // 계정이 만료되지 않았음을 반환합니다. 
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    // 계정이 잠기지 않았음을 반환합니다.
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    // 인증 정보가 만료되지 않았음을 반환합니다.
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // 계정이 활성화 상태임을 반환합니다.
    @Override
    public boolean isEnabled() {
        return true;
    }
}

4. Spring Security : JWT 로그인

1) JwtAuthenticationFilter

@Slf4j(topic = "로그인 및 JWT 생성")
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter { // UsernamePasswordAuthenticationFilter를 상속 받은 JwtAuthenticationFilter 클래스를 정의
    private final JwtUtil jwtUtil; // JWT를 관리하는 JwtUtil 클래스의 객체를 선언

    public JwtAuthenticationFilter(JwtUtil jwtUtil) { // 생성자를 통해 JwtUtil 객체를 주입받습니다.
        this.jwtUtil = jwtUtil; 
        setFilterProcessesUrl("/api/user/login"); // 로그인 요청을 처리하는 URL을 "/api/user/login"으로 설정합니다.
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { 
        // 로그인 요청이 들어올 때 실행되는 메서드입니다. HttpServletRequest와 HttpServletResponse 객체를 인자로 받습니다.
        log.info("로그인 시도"); // 로그인 시도에 대한 로그를 생성
        try {
            LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class); 
            // 클라이언트에서 보낸 로그인 요청의 데이터를 LoginRequestDto 클래스에 매핑하고, 이를 requestDto 객체에 저장합니다.

            return getAuthenticationManager().authenticate(
                    new UsernamePasswordAuthenticationToken(
                            requestDto.getUsername(), // 사용자의 아이디를 가져옵니다.
                            requestDto.getPassword(), // 사용자의 패스워드를 가져옵니다.
                            null
                    )
            ); // AuthenticationManager에게 사용자 인증을 요청하고 인증 결과를 반환합니다.
        } 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); // 사용자 아이디와 역할로 JWT 토큰을 생성합니다.
        jwtUtil.addJwtToCookie(token, response); // 생성한 JWT 토큰을 응답에 담습니다.
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        // 인증에 실패했을 때 실행되는 메서드입니다.
        log.info("로그인 실패"); // 로그인 실패에 대한 로그를 생성합니다.
        response.setStatus(401); // 응답의 상태 코드를 401(Unauthorized)로 설정합니다.
    }
}

JwtAuthenticationFilter 클래스가 작동하는 순서와 방법에 대한 자세한 설명

  1. 클라이언트로부터 로그인 요청이 들어오면 /api/user/login URL에 해당하는 JwtAuthenticationFilter가 이 요청을 처리합니다. 이 때 요청 정보는 HttpServletRequest 객체로, 응답 정보는 HttpServletResponse 객체로 전달됩니다.

  2. attemptAuthentication 메서드에서 클라이언트로부터 받은 로그인 데이터를 처리합니다. 이 데이터는 요청의 입력 스트림을 통해 전달되며, 이를 LoginRequestDto 클래스에 매핑합니다. 이 객체는 사용자 이름과 비밀번호를 포함합니다.

  3. UsernamePasswordAuthenticationToken 객체를 생성합니다. 이 객체는 스프링 시큐리티에서 사용자의 인증 정보를 담는 데 사용되며, 사용자 이름과 비밀번호를 포함합니다.

  4. AuthenticationManager에게 인증을 요청합니다. getAuthenticationManager().authenticate() 메서드를 호출하여 이루어집니다. AuthenticationManager는 사용자 이름과 비밀번호가 유효한지 검증하고, 그 결과를 Authentication 객체로 반환합니다.

  5. 만약 사용자 이름과 비밀번호가 유효하다면 successfulAuthentication 메서드가 호출됩니다. 이 메서드에서는 로그인에 성공한 사용자에 대한 JWT 토큰을 생성하고 이를 응답에 담아 보냅니다. JWT 토큰 생성은 JwtUtilcreateToken 메서드를 사용하여 이루어집니다. 이 때 토큰에는 사용자의 이름과 권한이 포함됩니다.

  6. 만약 사용자 이름과 비밀번호가 유효하지 않다면 unsuccessfulAuthentication 메서드가 호출됩니다. 이 메서드에서는 로그인 실패에 대한 정보를 로그에 남기고 응답의 상태 코드를 401(Unauthorized)로 설정하여 클라이언트에게 보냅니다. 이는 사용자에게 인증에 실패했음을 알리는 역할을 합니다.

이렇게 JwtAuthenticationFilter 클래스는 클라이언트의 로그인 요청을 받아 사용자의 인증을 처리하고 그 결과를 JWT 토큰으로 변환하여 응답하는 역할을 합니다.

2) JwtAuthorizationFilter

@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;
    }

JwtAuthorizationFilter 클래스의 선언

이 클래스는 클라이언트로부터의 모든 요청이 처리되기 전에 한번씩 실행되는 OncePerRequestFilter를 상속

WT 토큰의 유효성 검증과 사용자 인증을 수행합니다. 필요한 유틸리티와 서비스들(JwtUtil, UserDetailsServiceImpl)은 생성자를 통해 주입받습니다.

@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);
}

doFilterInternal는 요청으로부터 JWT 토큰을 추출하고 토큰이 유효한지 확인

토큰이 유효하면 토큰으로부터 사용자 정보를 추출하고 인증 처리를 수행

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

    SecurityContextHolder.setContext(context);
}

setAuthentication 메소드는 주어진 사용자 이름을 가진 사용자를 인증

SecurityContextHolder로부터 SecurityContext를 생성하고, createAuthentication 메소드를 호출하여 인증 객체를 생성합니다.

인증 객체는 SecurityContext에 설정되며, 이 SecurityContext는 다시 SecurityContextHolder에 설정됩니다. 이렇게 함으로써 사용자가 인증되고 보안 컨텍스트가 설정됩니다.

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

createAuthentication 메소드는 주어진 사용자 이름에 해당하는 사용자를 찾아 인증 객체를 생성

UserDetailsServiceImpl로부터 사용자 정보를 불러온 후, 이 정보와 사용자의 권한을 가진 UsernamePasswordAuthenticationToken을 생성하여 반환

JwtAuthorizationFilter

  1. 클라이언트로부터 요청이 들어오면, doFilterInternal 메소드가 호출됩니다.
  2. 요청 헤더에서 JWT 토큰을 추출합니다.
  3. 토큰이 있고 유효한지 확인합니다. 유효하지 않으면 에러를 로그에 기록하고 처리를 종료합니다.
  4. 토큰이 유효하면 토큰으로부터 사용자 정보를 추출하고 setAuthentication 메소드를 호출하여 사용자를 인증합니다.
  5. 다음 필터로 요청과 응답을 전달합니다.

3) WebSecurityConfig

@Configuration // 이 클래스를 Spring configuration 클래스로 선언
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    private final JwtUtil jwtUtil; // JWT 관련 작업을 처리하는 유틸리티 클래스
    private final UserDetailsServiceImpl userDetailsService; // 사용자의 세부 정보를 로드하는 서비스
    private final AuthenticationConfiguration authenticationConfiguration; // 인증 구성을 담당하는 클래스

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

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

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
        JwtAuthenticationFilter filter = new JwtAuthenticationFilter(jwtUtil); // JwtAuthenticationFilter 인스턴스 생성
        filter.setAuthenticationManager(authenticationManager(authenticationConfiguration)); // AuthenticationManager 설정
        return filter; // 생성된 JwtAuthenticationFilter 반환
    }

    @Bean
    public JwtAuthorizationFilter jwtAuthorizationFilter() {
        return new JwtAuthorizationFilter(jwtUtil, userDetailsService); // JwtAuthorizationFilter 빈 생성 및 반환
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf((csrf) -> csrf.disable()); // CSRF 공격 방지 기능 비활성화

        http.sessionManagement((sessionManagement) -> // 세션 관리 설정
                sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션을 생성하지 않도록 설정 (JWT를 사용하기 때문)
        );

        http.authorizeHttpRequests((authorizeHttpRequests) -> // 요청에 대한 인가 설정
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // 정적 자원에 대한 요청을 허용
                        .requestMatchers("/api/user/**").permitAll() // '/api/user/'로 시작하는 모든 요청을 허용
                        .anyRequest().authenticated() // 그 외의 모든 요청에 대해 인증이 필요함을 선언
        );

        http.formLogin((formLogin) -> // 로그인 폼에 대한 설정
                formLogin
                        .loginPage("/api/user/login-page").permitAll() // 로그인 페이지 경로 설정 및 모든 요청 허용
        );

        http.addFilterBefore(jwtAuthorizationFilter(), JwtAuthenticationFilter.class); // JwtAuthorizationFilter를 JwtAuthenticationFilter 이전에 실행하도록 설정
        http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); // JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 이전에 실행하도록 설정

        return http.build(); // HttpSecurity 설정 빌드
    }
}

6. 람다 1번 문 - 람다로 바꿔!

int max(int a, int b) {
return a > b ? a : b;
}
//바꾸면

(int a, int b) -> a > b ? a : b

1.

int printVar(String name, int i) {
System.out.println(name+"="+i );
}
//바꾸면
(String name, int i)->
    {System.out.println(name+"="+i);}

2.

int square(int x) {
return x * x;
}
//바꾸면
x -> x*x;

3.

int roll() {
return (int)(Math.random() * 6);
}
//바꾸면
()-> {return (int)(Math.random()*6);)}

4.

int sumArr(int[] arr) {
int sum = 0;
for(int i : arr)
sum += i;
return sum;
}

//바꾸면
(int[] arr) -> {
int sum = 0;
for(int i : arr)
sum += i;
return sum;
}

5.

int[] emptyArr() {
return new int[]{};
}
//바꾸면
()-> new int[]{};            //return이 포함이면