본문 바로가기
[Spring]/Spring Security

당신의 첫 프로젝트를 위한 스프링 시큐리티(2) - 인증 방식 개념과 AuthenticationFilter

by 팡펑퐁 2024. 7. 20.
728x90
📌 이번 글에서는 인증 방식 개념과 AuthenticationFilter 구현에 대해 다룹니다.

🤗 저의 스프링 시큐리티 구현은 아래와 같은 시나리오를 기준으로 합니다.
- 프론트 엔드와 백엔드가 나뉘어 진행되는 프로젝트를 기반으로 하여 스프링 시큐리티 설정에서 로그인 페이지에 대한 설정을 따로 하지 않음
- JWT 토큰 인증 방식을 사용함
- 토큰 관리에 redis를 이용함

 

🙉 이전 글 보기

첫 번째 글부터 정독하시면 보다 쉽게 이해하실 수 있습니다!

https://suzuworld.tistory.com/438 - 당신의 첫 프로젝트를 위한 스프링 시큐리티 톺아보기

이전 글

https://suzuworld.tistory.com/439 - SecurityConfig 구성하기

 

📖 목차

스프링 시큐리티 톺아보기

SecurityConfig 구성하기

인증 방식 개념과 AuthenticationFilter (현재 글)

 

🤔 들어가기 전에

<구글 이미지에서 검색하면 흔히 나오는 스프링 시큐리티 인증 아키텍처>

  • 지난 글에서는 Spring Security를 사용하기 전에 설정해야 할 Configuration에 대해 알아보았습니다.
  • 이제 본격적인 구현을 해보겠습니다.
  • 먼저 우리가 어떤 기능을 구현해야 할지 생각해 봅시다.
    • 로그인, 로그아웃 당연히 필요하겠죠?
    • 그리고 로그인 이후 각 요청에 맞는 인증 및 권한에 대해 검사하여 통과시키거나 입구컷하는 검증 로직이 필요할 겁니다.
  • 첫 글부터 언급했지만 저희는 사용자의 인증 정보를 다룰 방법 중 토큰 관리 방식을 사용할 겁니다.
  • 따라서 이번 글에는 사용자의 인증 정보를 다루는 개념과 위 그림의 1 ~3번에 해당하는 부분을 살펴보겠습니다.

 

👮🏻‍♀️ 사용자 인증 정보 관리 방식

  • 아마 대부분 이해하고 계시겠지만 혹시나 이해보다는 구현에 앞선 분들을 위해 아주 간단히 설명해 보겠습니다.
  • 로그인을 하면 사용자가 서버에 자신의 아이디와 비밀번호를 전달하여 해당 정보가 서버에 저장된 사용자 정보와 일치함을 확인합니다.
  • 그런데, HTTP 통신의 대표적인 특징이 있죠? 바로 무상태성(Stateless)입니다. 
    • 즉, HTTP는 클라이언트와 서버 간의 통신이 독립적이며 각 요청은 이전 요청과 관련이 없습니다.
  • 우리가 로그인을 하고 나서 개인정보가 들어있는 내 프로필 보기 조회를 한다고 가정해 봅시다.
  • 우리는 로그인만 했을 뿐입니다. 자신의 프로필 조회를 확인하기 위해 [내 프로필 보기] 버튼을 누른다면 해당 요청은 이전 로그인과는 전혀 무관한 요청이 됩니다.
  • 해당 요청에 다시 또 로그인 정보를 싣거나 하지 않으면 서버 입장에서는 이 요청이 인가된 요청인지를 확인할 방법이 없는 것이죠.
  • 그렇다고 모든 요청에 매번 로그인 아이디와 비밀번호를 입력한다면 말도 안 되겠죠..
  • 그래서 로그인을 성공하면 사용자 인증을 했다는 일종의 출입증(?)을 서버로부터 발급받아 가지고 있다가 다음 요청이 있을 때마다 서버에게 요청 정보에 넣어 보내면서 추가 인증 없이 원하는 요청을 진행하는 방식이 사용되는 것입니다.

 

🤯 이 방식에는 대표적으로 세션,  쿠키,  토큰 관리 방식이 있습니다.

세션(Session)

  • 서버 측에서 사용자 인증 정보를 저장하는 방식입니다.
  • 무상태 HTTP 통신에서 사용자 인증 정보를 유지하기 위해 서버 측에 세션을 사용합니다.
  • 서버에서 로그인을 성공하면 세션 ID를 생성하여 클라이언트에 전달합니다.
  • 클라이언트는 이후 서버와 통신할 때 이 세션 ID를 사용합니다.
  • 클라이언트에게 줄 정보가 사용자의 정보 대신 세션 ID이므로 정보 보안에 강합니다.
  • 서버에 사용자 인증 정보가 저장되기 때문에 서버 리소스를 사용한다는 단점이 있습니다.

 

쿠키(Cookie)

클라이언트 측에서 사용자 인증 정보를 저장합니다.

서버에서 별도로 사용자 인증 정보를 저장하지 않으므로 서버 리소스 사용이 상대적으로 적다는 장점이 있습니다.

클라이언트에 사용자 인증 정보가 저장되어 있어 정보 보안이 상대적으로 약하다는 단점이 있습니다.

 

JWT(JSON Web Token)

서버가 사용자 인증 정보를 토큰에 암호화하고 보내면 클라이언트가 이를 저장합니다.

이후 클라이언트가 토큰을 가지고 서버와 통신합니다.

클라이언트가 토큰을 사용하여 서버와 통신하므로 서버 리소스 사용이 적고, 확장성이 높습니다.

 

🏃🏻🏃🏻🏃🏻🏃🏻🏃🏻🏃🏻🏃🏻🏃🏻🏃🏻

보다 자세한 차이는 검색을 통해 알아봅시다!

 

⚙️ Member 관련 세팅

  • 본격적인 구현에 앞서 Member 관련 기본 세팅을 하겠습니다.
  • 여러분이 무엇을 구현하든 분명 회원가입과 로그인, 로그아웃 구현이 들어갈 텐데요.
  • 제각각 다른 방식으로 구현할 테니 여기서는 관련된 내용을 매우 간략하게만 보여드리겠습니다. 설명도 생략합니다.

 

bulid.gradle

// JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
// H2
runtimeOnly 'com.h2database:h2'
  • 간편한 테스트를 위해 JPA를 사용합니다.
  • 지난 글에서 프로젝트 생성 시 h2는 따로 의존성을 추가해 줬습니다.

 

MemberRepository

public interface MemberRepository extends JpaRepository<Member, Long> {
}
  • JPA의 기본 메서드를 사용합니다.

 

MemberDto 

public class MemberDto {

    @Getter
    public static class Enroll {
        private String memberId;
        private String password;
        private String username;
    }

    @Getter
    public static class Login {
        private String memberId;
        private String password;
    }
}
  • 회원 정보로 가질 필드는 아이디, 비밀번호, 회원 이름(닉네임) 세 개입니다.
  • 회원 가입은 회원 정보를 모두 입력받아야 하니 필드 세 개를 모두 가집니다.
  • 로그인은 회원 아이디와 비밀번호가 필요하니 두 개의 필드를 가집니다.

 

Member

@Entity
@Getter
@Setter
@NoArgsConstructor
public class Member {

    @Id
    private String memberId;
    private String password;
    private String username;
}
  • 엔티티 클래스입니다.
  • 간단한 테스트를 위해 @Setter를 사용하였지만, 프로젝트에서는 엔티티에서의 사용을 지양해야 합니다.

 

MemberController

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @PostMapping("/members")
    public ResponseEntity<?> addMember(@RequestBody MemberDto.Enroll enroll) {

        Member member = new Member();
        member.setMemberId(enroll.getMemberId());
        member.setPassword(enroll.getPassword());
        member.setUsername(enroll.getUsername());

        memberRepository.save(member);

        return new ResponseEntity<>(member, HttpStatus.CREATED);
    }
}
  • 간단한 테스트를 위해 컨트롤러에 전부 몰아넣었습니다.
  • DTO로 받은 필드를 엔티티로 옮기고 데이터베이스에 저장하는 간단한 회원가입 로직입니다.

 

h2-conole & application.yml

spring:
  h2:
    console:
      enabled: true # H2 Console을 사용 여부 (H2 Console은 H2 Database를 UI)
      path: /h2-console # H2 Console의 경로
  datasource:
    # H2 접속 정보
    url: jdbc:h2:mem:security # In-Memory Mode로 사용함
    username: sa  # H2 접속 시 username 정보 (자유 입력)
    password:     # H2 접속 시 password 정보 (자유 입력)
    driver-class-name: org.h2.Driver # Database를 H2로 사용함을 명시
  • h2 설정 정보입니다. 참고하시면 될 것 같습니다.

 

SecurityConfig

  • Spring security Config에 약간 변경사항이 있습니다.

 

// h2 사용시 붙여야함
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))

 

  • 현재 테스트는 데이터베이스로 h2를 사용하고 있습니다.
  • h2의 웹 콘솔은 iframe을 통해 화면을 구성한다고 하는데요. 만약 iframe에 대한 모든 요청을 허용하면 디도스 공격, 클릭재킹 공격에 취약해질 수 있으므로 기본 설정은 iframe을 사용하지 못하도록 DENY로 설정하고 있다고 합니다. 이를 허용하는 설정입니다.
    • 이 설정을 하지 않으면 h2 웹 콘솔 사용이 불가능합니다. 
    • 이 설정은 테스트를 위한 설정이므로 h2를 사용하지 않는다면 불필요한 설정입니다.
  • 아래는 관련 설명 링크입니다.

https://dukcode.github.io/spring/h2-console-with-spring-security/

 

Spring Security에서 H2 Console 사용하기

Spring Security에서 H2 Console 사용하기

dukcode.github.io

 

 

// 시큐리티 인증을 하지 않는 요청에 엔드포인트 추가
.requestMatchers("/login", "/enroll", "/h2-console/**").permitAll()
  • 또한, 기존 로그인 api 엔드포인트 외에 추가로 회원가입과 h2-console과 관련된 요청도 전부 인증하지 않게 변경합니다.
    • 회원가입과 로그인, h2-console 모두 인증 후 접근 방식이 불필요하기 때문에 설정한 것입니다.

 

회원 가입 테스트

  • PostMan으로 테스트했습니다.
  • 회원가입을 하는 과정입니다.
  • h2에 접속하면 성공적으로 저장됨을 확인할 수 있습니다.

 

🎉 본격적인 구현 시작

  • 아키텍처에서 이번 글에서 구현할 부분만 가져왔습니다.
  • 위의 설명이 길었으니 조금 정리해 봅시다.
  • 우리는 위에서 회원가입을 성공했습니다.
  • 회원 아이디는 "aaa", 비밀번호는 "bbb", 사용자 이름은 "김개똥"입니다.
  • 이제 우리는 로그인을 구현해 볼 겁니다.
  • 이하는 이 글에서 다룰 구현 부분입니다.
    • Http Request로 사용자의 아이디와 비밀번호를 입력받습니다. (1)
    • 인증 필터가 해당 값을 UsernamePasswordAuthenticationToken 객체에 싣습니다. (2)
    • 이후 AuthenticationManager에게 인증 처리 권한을 위임하여 이후 처리를 진행하도록 지시합니다. (3)

 

JwtAuthenticationFilter(Http Request -> AuthenticationFilter)

  • 1번에 해당하는 부분입니다. "/login" 엔드포인트로 요청이 들어오면 컨트롤러에 가기 전 해당 필터를 거치게 될 것입니다.
    • 필터 적용은 다음 글에서 다룹니다. 
    • 이 클래스는 UsernamePasswordAuthenticationFilter이란 필터를 상속받도록 했습니다. 그 이유는?
      • 스프링이 로그인 처리에 대한 필터를 이미 잘 만들어두었기 때문입니다. 우리가 처음부터 만드는 것보다 이미 잘 만들어진 코드를 이용하는 것이 Spring Security를 쓰는 이유니까요!
    • 이름으로 알 수 있듯이 Username(Id)와 Password로 로그인 인증을 하는 필터인 것 같습니다.
    • 참고로 제가 만든 entity의 username과 이 username은 다릅니다. 엔티티의 username은 단순한 닉네임을 의미하고, 여기서의 username은 회원 아이디를 의미합니다.
  • attemptAuthentication() 메서드를 오버라이딩해 줍니다.

 

로직 구현(AuthenticationFilter -> UsernamePasswordAuthenticationToken)

  • 그림의 2, 3번에 해당하는 부분입니다.

 

private final AuthenticationManager authenticationManager;
  • 먼저 authenticationManager를 의존성 주입받습니다.

 

@SneakyThrows

  • Lombok 라이브러리에서 제공하는 어노테이션입니다. 이 어노테이션은 메서드에 사용되며 메서드 내부에서 발생할 수 있는 예외를 자동으로 처리합니다.

 

ObjectMapper objectMapper = new ObjectMapper();
MemberDto.Login loginDto = objectMapper.readValue(request.getInputStream(), MemberDto.Login.class);
  • 이곳은 필터단이므로 지금껏 스프링이 대신해준 작업을 직접해야 합니다.
    • 요청으로 들어온 JSON 데이터를 DTO에 매핑하여 넣어주는 등의 역할을 말합니다.
    • objectMapper는 이러한 매핑 역할을 해주는 객체입니다.
  • new 키워드로 objectMapper 인스턴스를 하나 생성하고 request.getInputStream()를 통해 요청 데이터에 들어있는 json을 MemberDto의 Login에 맞춰 매핑하여 loginDto를 완성합니다. 
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());

return authenticationManager.authenticate(authenticationToken);
  • 그다음 loginDto에서 memberId와 password를 꺼내 UsernamePasswordAuthenticationToken 객체를 생성하여 authenticationManager에게 인증 처리를 부탁합니다.(authenticationManager.authenticate(authenticationToken);)

 

🧑🏻‍🏫  정리 & 다음 글에서 다룰 내용

  • 첫 번째 글에서 AuthenticationManager가 매니저의 역할을 부여받아 일을 처리한다고 했습니다.
  • 그러나, AuthenticationManager는 interface이므로 이를 구현할 구현체가 필요합니다.
    • 해당 구현체가 매니저의 역할을 부여받을 겁니다.
    • 사용자 인증 정보(id, password)를 데이터베이스에서 가져온 정보와 비교하도록 다른 객체에게 오더를 내릴 겁니다.
    • 이후 결괏값을 리턴 받아 성공과 실패 시 각각 응답에 대한 결과를 처리할 겁니다.
    • 이 과정에서 로그인에 성공한다면 JWT 토큰을 발급하고 유저에게 응답 메시지와 함께 보낼 것입니다.
  • 이를 위해 우리가 만든 AuthenticationFilter를 Config에 등록하여 스프링 시큐리티가 이 필터를 사용하도록 해야 합니다.
  • AuthenticationManager의 구현체를 만들어 Config에 등록해야 합니다.

 

 

 

참고

뤼튼

https://dukcode.github.io/spring/h2-console-with-spring-security/

 

 

728x90