Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Tags more
Archives
Today
Total
관리 메뉴

seaking110 님의 블로그

Spring Security 본문

Today I Learned

Spring Security

seaking110 2025. 3. 12. 16:46

Spring Security

  • 2003년부터 개발된 JWT보다 10년 이상 오래된 도구
  • 따라서 JWT가 없던 시대부터 사용되어와서 JSP 등 SSR 방식과 연관된 스펙이 매우 많음
  • 다시말해 세션 방식과 결합된 JWT가 Spring Security에 결합되어 너무 많이 사용되고 있다.
  • 해당 방식은 잘못된 방식! (Stateless 문제)
  • AbstractAuthenticationToken을 set 해줘야 한다 Security 컨텍스트에다가!!
  • UserDetatilsServiceImpl를 사용하면 Stateless라는 JWT 개념과 매우 강하게 충돌!!
  • UserDetatilsService는 DB에 접근해서 유저 정보를 가져옴 즉 세션을 위한 스펙
  • 따라서 JWT를 쓸 이유가 없어짐!! 이렇게 하면 안됨!!

기존 JWTutil 코드

더보기
@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {

    private static final String BEARER_PREFIX = "Bearer ";
    private static final long TOKEN_TIME = 60 * 60 * 1000L; // 60분

    @Value("${jwt.secret.key}")
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(Long userId, String email, UserRole userRole) {
        Date date = new Date();

        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(String.valueOf(userId))
                        .claim("email", email)
                        .claim("userRole", userRole.getUserRole())
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                        .setIssuedAt(date) // 발급일
                        .signWith(key, signatureAlgorithm) // 암호화 알고리즘
                        .compact();
    }

    public String substringToken(String tokenValue) {
        if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
            return tokenValue.substring(7);
        }
        log.error("Not Found Token");
        throw new NullPointerException("Not Found Token");
    }

    public Claims extractClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}

 

 

  • Spring Security에서는 더이상 사용하지 않음

 

SecurityConfig

더보기

새로운 SecurityConfig로 기존의 FilterConfig와 비슷한 역할

  • SecurityConfig
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthenticationFilter;

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

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(session -> session
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                )
                .addFilterBefore(jwtAuthenticationFilter, SecurityContextHolderAwareRequestFilter.class)
                .formLogin(AbstractHttpConfigurer::disable)
                .anonymous(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .rememberMe(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers(request -> request.getRequestURI().startsWith("/auth")).permitAll()
                        .requestMatchers("/test").hasAuthority(UserRole.Authority.ADMIN)
                        .requestMatchers("/open").permitAll()
                        .anyRequest().authenticated()
                )
                .build();
    }
}

 

  • 기존의 FilterConfig와 비슷
  • Spring Security에서는 BCryptPasswordEncoder를 제공하여 Bean으로 등록하여 사용
  • .csrf 비활성화 : JWT는 세션을 이용하지 않으므로 CSRF(사이트 간 요청 위조) 공격을 방지하는 보안 기능을 비활성화
  • sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) : 세션을 사용하지 않고 서버는 무상태를  유지하겠다고 하는 것
  • .addFilterBefore : JWT 인증 필터를 검사한 후 Spring Security의 보안 컨텍스트를 처리하도록 설정
  • Spring Security의 설정 파일로 세션과 관련된 Filter를 비활성화
    • formLogin : SSR이 아니므로 폼 기반 로그인 기능 필요 X
    • annoymous : 익명 사용자 권한은 필요 X
    • httpBasic : 커스텀 Filter를 사용하므로 비활성화 (Basic 필터도 사용 가능하나 매우 복잡함!)
    • logout : 세션 정보를 지우는 요청이므로 세션이 아니므로 비활성화 + 로그아웃은 JWT에서 사실상 무의미한 개발 
    • rememberMe : Stateless를 유지해야 하므로 기억할 수 없음!
    • .authorizeHttpRequests(auth -> auth.requestMatchers(...)) : 요청에 대한 인가 설정, 특정 URL에 대한 접근 정책을 정의  
  • .anyRequest().authenticated() : SecurityContext에 토큰이 set 되었다면 통과를 시키겠다는 뜻 (즉 나머지 모든 URL에 인증을 요구하는 설정)

 

기존 filterConfig

@Configuration
@RequiredArgsConstructor
public class FilterConfig {

    private final JwtUtil jwtUtil;

    @Bean
    public FilterRegistrationBean<JwtFilter> jwtFilter() {
        FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter(jwtUtil));
        registrationBean.addUrlPatterns("/*"); // 필터를 적용할 URL 패턴을 지정합니다.

        return registrationBean;
    }
}

 

JwtAuthenticationFilter

더보기

새로운 Filter로 기존의 Filter를 대체

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(
            HttpServletRequest httpRequest,
            @NonNull HttpServletResponse httpResponse,
            @NonNull FilterChain chain
    ) throws ServletException, IOException {
        String authorizationHeader = httpRequest.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String jwt = jwtUtil.substringToken(authorizationHeader);
            try {
                Claims claims = jwtUtil.extractClaims(jwt);

                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    setAuthentication(claims);
                }
            } catch (SecurityException | MalformedJwtException e) {
                log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
            } catch (ExpiredJwtException e) {
                log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
            } catch (UnsupportedJwtException e) {
                log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
            } catch (Exception e) {
                log.error("Internal server error", e);
                httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        chain.doFilter(httpRequest, httpResponse);
    }

    private void setAuthentication(Claims claims) {
        Long userId = Long.valueOf(claims.getSubject());
        String email = claims.get("email", String.class);
        UserRole userRole = UserRole.of(claims.get("userRole", String.class));

        AuthUser authUser = new AuthUser(userId, email, userRole);
        JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
    }
}
  •  기존 필터와 정말 유사
  • setAuthentication 부분만 다름
  • 기존엔 setAttribute로 JWT의 claims 정보를 저장했다면 이제는 JWT에서 추출한 Claims 정보를 기반으로 사용자 인증을 설정 
  • 즉 JWT를 파싱하여 사용자 정보를 가져온 후 이를 Spring Security의 인증 컨텍스트에 등록
  • 해당 필터를 위에 SecurityConfig에 등록

 

기존의 Filter

@Slf4j
@RequiredArgsConstructor
public class JwtFilter implements Filter {

    private final JwtUtil jwtUtil;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        String url = httpRequest.getRequestURI();

        if (url.startsWith("/auth")) {
            chain.doFilter(request, response);
            return;
        }

        String bearerJwt = httpRequest.getHeader("Authorization");

        if (bearerJwt == null) {
            // 토큰이 없는 경우 400을 반환합니다.
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
            return;
        }

        String jwt = jwtUtil.substringToken(bearerJwt);

        try {
            // JWT 유효성 검사와 claims 추출
            Claims claims = jwtUtil.extractClaims(jwt);
            if (claims == null) {
                httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
                return;
            }

            UserRole userRole = UserRole.valueOf(claims.get("userRole", String.class));

            httpRequest.setAttribute("userId", Long.parseLong(claims.getSubject()));
            httpRequest.setAttribute("email", claims.get("email"));
            httpRequest.setAttribute("userRole", claims.get("userRole"));
            httpRequest.setAttribute("nickName", claims.get("nickName"));

            if (url.startsWith("/admin")) {
                // 관리자 권한이 없는 경우 403을 반환합니다.
                if (!UserRole.ADMIN.equals(userRole)) {
                    httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "관리자 권한이 없습니다.");
                    return;
                }
                chain.doFilter(request, response);
                return;
            }

            chain.doFilter(request, response);
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
            httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
        } catch (Exception e) {
            log.error("Internal server error", e);
            httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    @Override
    public void destroy() {
        Filter.super.destroy();
    }
}

 

 

JwtAuthenticationToken

더보기

JwtAuthenticationToken

기존의 AuthUserArgumentResolver를 대체

public class JwtAuthenticationToken extends AbstractAuthenticationToken {

    private final AuthUser authUser;

    public JwtAuthenticationToken(AuthUser authUser) {
        super(authUser.getAuthorities());
        this.authUser = authUser;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @Override
    public Object getPrincipal() {
        return authUser;
    }
}

 

 

 

 

 

  • Spring Security는 JWT 방식에서는 상대적으로 세션보다 사용법이 간단
  • JWT는 MSA 환경에 유리하다.
  • Security Config에 필터를 등록해줘야함 .addFilterBefore
  • 새로운 필터는 기존 필터나 resolver랑 다른점은 @Component로 등록을 해줘야함

 

 

'Today I Learned' 카테고리의 다른 글

AWS 시작하기!  (0) 2025.03.20
Spring Security 와 WAS  (0) 2025.03.12
연관관계와 N+1  (0) 2025.03.11
테스트 코드 remind  (1) 2025.03.10
헷갈리는 개념 정리  (2) 2025.03.05