2/12(목) Spring Security JWT 로그인 필터 실습

dev_joo·2026년 2월 12일

아침부터 속이 불편해서 깼다.
독감을 옮았는지 몸살기도 있어서 오늘은 캠프에 결석을 하고 휴식을 취하고 저녁에 조금 공부를 했다.

코드 카타 2026.02.12

자연수 뒤집어 배열로 만들기:

class Solution {
    public int[] solution(long n) {
        List<Long> answer = new ArrayList<>();
        while (n > 0) {
            answer.add(n % 10);
            n /= 10;
        }
        return answer.stream().mapToInt(Long::intValue).toArray();
    }
}
class Solution {
    public int[] solution(long n) {
        String str = Long.toString(n);
        int[] answer = new int[str.length()];

        for (int i = 0; i < str.length(); i++) {
            answer[i] = str.charAt(str.length() - 1 - i) - '0';
        }
        return answer;
    }
}

문자열을 정수로 바꾸기

class Solution {
    public int solution(String s) {
        if(s.charAt(0) == '-'){
            return Integer.parseInt(s);
        }else return Integer.parseInt(s);
    }
}
class Solution {
    public int solution(String s) {
        return Integer.parseInt(s);
    }
}

정수 제곱근 판별

class Solution {
    public long solution(long n) {
        for(long x= 0; x*x<=n; x++){ // 시간초과 유의
            if(x*x == n) {
                x++;
                return x*x;
            }
        }
        return -1;
    }
}

정수 내림차순으로 배치하기

import java.util.*;

class Solution {
    public long solution(long n) {
        char[] arr = String.valueOf(n).toCharArray();
        Arrays.sort(arr);
        
        long answer = 0;
        for (int i = arr.length - 1; i >= 0; i--) {
            answer = answer * 10 + (arr[i] - '0');
        }
        return answer;
    }
}
String.valueOf(n)
→ 내부적으로
Long.toString(n)
👉 불필요한 객체 생성 없이 바로 문자열 반환


✅ "" + n

컴파일 시:
new StringBuilder()
    .append("")
    .append(n)
    .toString();


tringBuilder 객체 생성 및
append(), toString() 호출

숙련주차 강의

Spring Security JWT 로그인

인증

Filter로 구현해 인증과 비즈니스 로직을 분리한다.

package com.sparta.springauth.jwt;
...
import com.sparta.springauth.security.UserDetailsImpl;
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;


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

            return getAuthenticationManager().authenticate(
                    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);
    }
}

로그인 경로 설정

기존 Security의 Default login기능을 사용할 때 SecurityConfig 파일에 설정했던 필터가 가로챌 경로를 직접 설정해준다.

setFilterProcessesUrl("/api/user/login");

Object Mapper로 값 불러오기

ObjectMapper.readValue(inputStream, class) JSON을 객체로 가져온다.

LoginRequestDto requestDto = new ObjectMapper().readValue(request.getInputStream(), LoginRequestDto.class);

Manager 객체로 인증 수행

getAuthenticationManager().authenticate(
	new UsernamePasswordAuthenticationToken(
    	requestDto.getUsername(),
        requestDto.getPassword(),
        null
    )
);

로그인 성공시 처리

successfulAuthentication 메서드를 사용한다.
파라미터로 Authentication 객체를 받아 인증 정보를 JWT 생성에 사용한다.

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

로그인 실패시 처리

unsuccessfulAuthentication 메서드를 사용한다.

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

인가

OncePerRequestFilter 를 상속받아 HttpServlet Request/Response 객체를 받을 수 있다.
원래는 AuthenticationManager가 인가 처리를 해주지만 예제에서는
인증 객체 생성과 인증 처리를 직접 구현했다.

package com.sparta.springauth.jwt;
...
import com.sparta.springauth.security.UserDetailsServiceImpl;
import io.jsonwebtoken.Claims;
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());
    }
}

인증 객체 생성

username으로 Security Context에 담을 Authentication 객체를 만들어준다.
Authentication은 username으로 만든 UserDetails를 principal로써 포함한다.

private Authentication createAuthentication(String username) {
	UserDetails userDetails = userDetailsService.loadUserByUsername(username);
    return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}

인증 처리

JWT 토큰의 Claims 에서 Subject에 넣어둔 username을 받아
username으로 생성한 Authentication을 담은 SecurityContext를 만들고 ContextHolder에 저장한다.

 public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        Authentication authentication = createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

필터 등록

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

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final 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 {
    
    // 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정
    	http.sessionManagement((sessionManagement) ->
    	sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    	);
        
        ...
        
		http.formLogin((formLogin) ->
        	formLogin
            .loginPage("/api/user/login-page").permitAll()
        );

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

bean을 해당 필터 이전에 동작하도록 등록

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

코드를 해석하는 방법

예전에는 모르는 코드를 발견하면 겁부터 먹고 흐린 눈으로 보게 되는 느낌이 있었다.
강의를 들으면서 설명하는 모습을 보니 코드를 해석하는 방법을 알게 되었다.
앞으로 코드를 작성할 때에도 이를 유의하면서 작성한다면 다른 사람이 내 코드를 이해하는 데에도 도움이 될 것 같아 적어둔다.

클래스 내부 해석하기

1. 클래스이름을 확인한다. 클래스가 어떤 요소를 가지고 어떤 동작을 할지 유추할 수 있다.
2. 클래스가 상속받은 추상클래스 / 인터페이스를 확인한다. 
3. 클래스가 가지고 있는 필드와 그 필드가 어디에 사용되는지 확인한다.
4. 필드가 사용되는 메서드의 이름을 확인한다.
5. 메서드가 필드를 어떻게 처리하고 반환값을 내는지 확인한다.

객체끼리 상호작용 해석하기

1. 어떤 Bean의 메서드에서 어떤 클래스가 파라미터로 들어오는지 확인한다.
2. 또 어떤 객체가 사용되는지 확인한다.
2. 헤딩 Bean이 어디에서 관리되는지 확인한다. (예: Config 파일에서 Filter로 등록 됨 등)
profile
풀스택 연습생. 끈기있는 삽질로 무대에서 화려하게 데뷔할 예정 ❤️🔥

0개의 댓글