Spring Security

민찬홍·2024년 1월 23일

Spring

목록 보기
16/16
post-thumbnail

이미지 공유 서비스 제작에서 회원 도메인을 맡아 제작하면서 Spring Security에 관해 하나하나 궁금해지기 시작했다. 관련 파일과 설명들을 정리할 생각이다. 사실상 custom하게 만든것도 많아서 이게 정답은 아니다..

나는 스프링 시큐리티 + 인증방식으로 JWT를 선택해서 JWT 와 관련된 파일들도 정리해놓겠다.

SecurityConfig 클래스

SecurityConfig 클래스는 Spring Security를 사용하여 웹 어플리케이션의 보안 구성을 담당하는 설정 클래스이다.

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // SecurityFilterChain을 정의하는 Bean 메서드
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                // CSRF(Cross-Site Request Forgery) 보안 설정을 비활성화
                .csrf(AbstractHttpConfigurer::disable)
                
                // 모든 요청에 대해 권한을 허용
                .authorizeHttpRequests((authorizeHttpRequests) -> authorizeHttpRequests
                        .requestMatchers(new AntPathRequestMatcher("/**")).permitAll())
                
                // "/h2-console/**" 경로의 요청에 대해서는 CSRF 보안을 무시
                .csrf((csrf) -> csrf
                        .ignoringRequestMatchers(new AntPathRequestMatcher("/h2-console/**")))
                
                // X-Frame-Options 헤더 설정을 추가하여 SAMEORIGIN으로 설정
                .headers((headers) -> headers
                        .addHeaderWriter(new XFrameOptionsHeaderWriter(
                                XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)))
                
                // 로그인 페이지 및 성공 시 이동할 기본 URL 설정
                .formLogin((formLogin) -> formLogin
                        .loginPage("/member/login")
                        .defaultSuccessUrl("/"))
                
                // 로그아웃 관련 설정
                .logout((logout) -> logout
                        .logoutRequestMatcher(new AntPathRequestMatcher("/member/logout"))
                        .logoutSuccessUrl("/")
                        .invalidateHttpSession(true))
                
                // 세션 관리 정책 설정 (STATELESS: 세션을 사용하지 않음)
                .sessionManagement((sessionManagement) -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        ;
        return http.build();
    }

    // 비밀번호 인코더 Bean 정의
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // AuthenticationManager Bean 정의
    @Bean
    AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws
            Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    // 현재 사용자가 인증되었는지 확인하는 메서드
    private boolean isAuthenticated() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || authentication instanceof AnonymousAuthenticationToken) {
            return false;
        }
        return authentication.isAuthenticated();
    }
}

위의 코드에 주석을 달아놓았다. 한줄한줄 무슨 의미인지 살펴보자.

CSRF(Cross-Site Request Forgery)

csrf는 악의적인 웹사이트에서 희생자가 이미 인증된 상태로 다른 웹사이트에 요청을 보내도록 속이는 공격이다. 이는 사용자가 의도하지 않은 동작을 수행하게 만든다. 스프링 시큐리티에서 csrf 보안 설정은 이러한 종류의 공격을 방지하는 데 도움을 준다. 스프링 시큐리티에서 csfr 보안 설정은 기본적으로 활성화되어 있다. 그러나 일부 경우에는 보안을 비활성화하고 특정 URL 패턴에서는 csrf 보안을 적용하지 않도록 설정할 수 있다.

X-Frame-Options , SAMEORIGIN

X-Frame-Options는 웹 페이지가 다른 웹 페이지 내의 <frame>, <iframe>,<object> 요소에 의해 로드되는 것을 방지하기 위한 HTTP 응답 헤더이다. 이 헤더는 클릭재킹(clickjacking) 과 같은 공격을 방어하기 위한 것이다. SAMEORIGIN 값은 같은 출처에서만 해당 페이지를 로드할 수 있도록 지정한다. 즉 같은 도메인, 프로토콜, 포트에서만 해당 페이지를 사용할 수 있도록 허용하는 것이다.

.sessionManagement

JWT 는 세션기반이 아니라 토큰 기반의 인증 방식이다. 따라서 JWT를 사용할 때는 세션을 사용하지 않도록 설정하는 것이 일반적이다. 따라서 STATELESS 정책을 사용하는 것이다.

또한 JWT 를 사용하는 경우에는 사용자 인증 정보를 토큰에 담아 클라이언트에 전달하고, 서버는 이 토큰을 검증하여 사용자를 인증한다. 토큰에는 사용자의 권한, 만료 시간 등이 포함되어 있기 때문에 별도의 세션을 유지할 필요가 없다. 세션을 사용하지 않는 것에는 다음과 같은 장점이 있다.

  • Stateless 서버 구현: 서버는 클라이언트의 상태를 유지할 필요 없이 요청을 처리할 수 있다. 각각의 요청은 독립적이며, 서버는 토큰을 통해 필요한 정보를 추출하여 사용자를 인증한다.
  • 수평 확장 용이성: 서버가 상태를 유지하지 않기 때문에 여러 서버 간의 사용자 인증 정보를 동기화할 필요가 없다. 이는 서버를 수평적으로 확장하는 데 용이하다.
  • 토큰 기반 인증의 간결함: 토큰은 사용자 정보와 함께 서명되어 있어서 검증이 쉽다. 토큰 자체가 사용자의 인증 정보를 담고 있어서 별도의 데이터베이스 조회가 필요 없다.

AuthenticationManager

인증을 수행하는 주요 인터페이스이다. 이 인터페이스는 authenticate 메서드를 가지고 있다. 주로 로그인 시에 사용자가 제공한 자격증명(아이디 패스워드) 를 검증하여 사용자를 인증하는 역할을 한다.
AuthenticationManager는 보통 여러 AuthenticationProvider를 포함하고 있다. 각 프로바이더는 특정 인증 방식(아이디/비밀번호, OAuth 토큰 등)을 처리한다. AuthenticationManager는 주어진 Authentication 객체를 전달하면 각 AuthenticationProvider에게 전달하여 실제 인증을 수행한다.

SecurityUser 클래스

SecurityUser 클래스는 Spring Security의 User클래스를 상속받아 사용자 정보를 표현하고, genAuthentication 메서드는 해당 사용자 정보를 기반으로 Spring Security의 인증 토큰을 생성한다. 이러한 객체는 사용자 인증 및 권한 부여를 위해 활용된다.

public class SecurityUser extends User {

    @Getter
    private long id;

    // 생성자: id, username, password, 권한을 받아서 User클래스 생성자 호출
    public SecurityUser(long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, authorities);
        this.id = id;
    }


    // 생성자: username, password, 활성 여부, 계정 만료여부, 자격증명만료여부, 계정 잠금 여부, 권한을 받아서 User 클래스의 생성자 호출
    public SecurityUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
        super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
        this.id = id;
    }

    public Authentication genAuthentication() {
        Authentication auth = new UsernamePasswordAuthenticationToken(
                this,  // 사용자 정보를 나타내는 객체
                this.getPassword(),  // 패스워드
                this.getAuthorities()  // 사용자 권한
        );
        return auth;
    }

UsernamePasswordAuthentication

Spring Security에서 사용되는 토큰 중 하나로, 사용자의 인증 정보를 나타낸다.

JwtAuthenticationFilter 클래스

이 클래스는 스프링 시큐리티에서 사용되는 JWT(JSON Web Token)를 기반으로 사용자의 인증을 처리하는 클래스이다. 해당 클래스는 OncePerRequestFilter 를 상속하고, 요청마다 한 번만 실행되도록 구성되어 있다.

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final MemberService memberService;

    @Override
    @SneakyThrows
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
        System.out.println("JwtAuthenticationFilter 실행");
        // TODO: access, refresh token 갱신 자동화 과정 추가

        // 클라이언트로부터 전달받은 AccessToken 추출
        String apiKey = null;
        Cookie[] cookies = request.getCookies();
        if (cookies != null) {
            Optional<Cookie> rqCookie = Arrays.stream(cookies)
                    .filter(cookie -> cookie.getName().equals("accessToken"))
                    .findFirst();
            if (rqCookie.isPresent()) {
                apiKey = rqCookie.get().getValue();
            }
        }

        // AccessToken이 존재하는 경우, 해당 토큰을 사용하여 사용자 정보 조회
        if (apiKey != null) {
            SecurityUser user = memberService.getMemberFromApiKey(apiKey);
            setAuthentication(user);
        }

        // 다음 필터 체인으로 전달
        filterChain.doFilter(request, response);
    }

    // SecurityContextHolder에 인증 정보를 설정하는 메서드
    public void setAuthentication(SecurityUser member) {
        Authentication auth = new UsernamePasswordAuthenticationToken(
                member,  // 사용자 정보를 나타내는 객체
                member.getPassword(),  // 패스워드
                member.getAuthorities()  // 사용자 권한
        );
        SecurityContextHolder.getContext().setAuthentication(auth);
    }
}

doFilterInternal

실제 필터링을 수행하는 메서드로, 각 요청에 대해 한 번 호출된다.

Optional<Cookie> rqCookie = Arrays.stream(cookies) ...

쿠키 배열에서 accessToken 이름을 가진 쿠키를 찾는다.

setAuthentication

받아온 사용자 정보를 이용하여 UsernamePasswordAuthenticationToken 을 생성하고 SecurityContextHolder 에 설정하는 역할을 한다. 이로써 현재 사용자의 인증 정보를 알 수 있게 한다.

JwtProperties 클래스

이 클래스는 @ConfigurationProperties 클래스를 사용하여, application.yml 파일에 정의된 프로퍼티 값을 자동으로 매핑하는 역할을 한다. 보통 JWT의 Secret Key를 매핑하는 역할이다.

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

    private String SECRET_KEY;

}

JwtUtil 클래스

JWT를 생성하고 검증하는 유틸 클래스이다. JWT는 클레임(Claim) 기반의 토큰으로, 보통 사용자의 인증 및 권한 부여를 위해 사용된다. 이 클래스는 encode와 decode로 이루어져서 생성과 검증에 사용된다.

public class JwtUtil {
    // JWT 생성
    public static String encode(long expirationSeconds, Map<String, Object> data, String secretKey) {
        // JWT의 클레임(정보)을 설정합니다.
        Claims claims = Jwts
                .claims()
                .setSubject("sb-23-11-30 jwt")  // JWT의 주제(Subject)를 설정합니다.
                .add("data", data)  // JWT에 추가로 담을 데이터를 설정합니다.
                .build();

        Date now = new Date();
        Date expiration = new Date(now.getTime() + 1000 * expirationSeconds);  // JWT의 만료 시간을 설정합니다.

        // JWT를 빌드하여 문자열로 반환합니다.
        return Jwts.builder()
                .setClaims(claims)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, secretKey)  // JWT를 서명하여 보안을 강화합니다.
                .compact();
    }

    // JWT 검증
    public static Claims decode(String token, String secretKey) {
        // 주어진 토큰을 파싱하고 검증하여 클레임을 추출합니다.
        return Jwts
                .parser()
                .setSigningKey(secretKey)
                .build()
                .parseClaimsJws(token)  // 서명이 유효한지 확인하며, 유효하다면 클레임을 반환합니다.
                .getPayload();
    }
}

결론

간략하게 적어봤는데 이해가 되지 않는 부분들은 추후에 다시 봐야될 것 같다. 스프링 시큐리티 관련 흐름이 잘 나와있는 포스트가 있어서 이것도 같이 올린다.

https://velog.io/@kwj1830/codestates29

https://aonee.tistory.com/72

profile
백엔드 개발자를 꿈꿉니다

0개의 댓글