목차
개요
웹 애플리케이션에서 사용자 인증은 필수적인 요소 중 하나이고 회원 기능이 있다면 회원 식별을 위한 인증절차는 필수적이다. 따라서 이번 블로그 포스팅에서는 Spring Security에서 사용자 인증 정보를 얻어오는 두 가지 주요 방법, `Principal`과 `SecurityContextHolder`의 차이점에 대해 살펴보겠다.
SecurityContextHolder 개요
Spring Security는 웹 애플리케이션에서 보안(인증 및 권한 관리)을 다루는 데 사용되는 프레임워크이다.
보안 컨텍스트는 `SecurityContextHolder`를 통해 관리되며, 현재 인증된 사용자는 `Principal` 또는 `Authentication` 객체를 통해 나타낼 수 있다.
SecurityContextHolder
SecurityContextHolder는 Spring Security에서 현재 보안 컨텍스트를 저장하고 액세스하는 데 사용되는 클래스로 `SecurityContext` 객체를 저장하며, 이 `SecurityContext` 객체에는 현재 인증된 사용자를 나타내는 `Authentication` 객체가 포함되어 있습니다.
SecurityContextHolder는 스레드 로컬 저장소를 사용하여 보안 컨텍스트를 유지한다.
이렇게 함으로써 각 HTTP 요청이 처리되는 동안에만 보안 컨텍스트가 유지되며, 요청이 완료되면 해당 컨텍스트는 자동으로 제거됩니다.
Authentication 객체는 다음과 같은 정보를 포함한다.
principal
사용자를 식별합니다. 사용자 이름과 비밀번호를 사용하여 인증하는 경우, UserDetails의 인스턴스가 된다.
credentials
대부분 비밀번호로 사용하고 많은 경우에 인증 후에 이 정보는 지워집니다. 이는 자격 증명 정보가 유출되는 것을 방지하기 위함이다.
authorities
사용자에게 부여된 고수준 권한을 나타내는 GrantedAuthority 인스턴스이다. 이는 일반적으로 사용자의 역할(Role)이나 범위(권한)를 나타낸다.
SecurityContextHolder 세팅 하는법
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
1. SecurityContextHolder에서 createEmptyContext 메서드를 통해 SecurityContext 인스턴스를 생성한다.
- ContextHolder은 공유자원 이기에 race condition을 피하기 위해서는SecurityContextHolder. getContext(). setAuthentication (authentication) 방식이 아닌 새 인스턴스를 만들고 거기에 데이터를 추가하는 방식으로 구현해야지 thread-safe 프로그래밍을 진행할 수있다.
2. 이후 Authentication 객체를 만들어주는데 이때 자신이 쓸 토큰 메서드를 사용해서 authentication에 할당해준다.
3. context에 setAuthentication 메서드를 이용하여 만들었던 authentication를 할당한다.
4. 마지막으로 ContextHolder에 setContext를 이용하여 context를 할당한다.
SecurityContextHolder를 이용해 인증된 회원 정보에 접근하는법
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
1. 역으로 SecurityContextHolder에서 getContext를 하여 얻어낸 context에서 또다시 Authentication을 추출한다.
2. 이후엔 getName으로 username을 Principal로 principal 객체를 가져올수 있다.
3. Spring Security에서 `GrantedAuthority` 인터페이스는 사용자가 가진 권한(역할)을 표현하는데 사용되는데 Spring Security에서는 사용자의 권한을 기반으로 접근 제어 및 권한 부여를 수행한다.
- 제네릭 와일드카드를 이용해서 GrantedAuthority 를 상속받은 타입이라는 형태이다. authority 저장에 있어서 다형성을 부여해준다.
- 결론적으로 GrantedAuthority 인터페이스를 상속받은 객체들을 담을수 있는 Collection 인스턴스 를 선언한 것이다.
## 결론
`Principal`과 `SecurityContextHolder`는 Spring Security에서 현재 인증된 사용자의 정보를 얻는 데 사용되는 두 가지 방법입니다. 필요한 정보와 상황에 따라 적절한 방법을 선택하여 사용할 수 있습니다.
Principal
security 내의 principal 인터페이스를 뜯어보자면
equals
Principal 객체와 입력받은 다른 객체를 비교해서 입력받은 객체가 현재 Principal 객체와 동일하다면 true를 반환하고, 그렇지 않다면 false를 반환한다.
toString
객체의 문자열 표현을 반환한다.
hashCode
객체의 해시코드를 반환한다(고유한 숫자)
getName
대부분 UserDetails의 username을 가져온다.
implies
뒤에 주어진 Subject가 현재 Principal에 implicated 되는지 여부를 반환한다.
Principal과 getAuthentication() 장단점
SecurityContextHolder 통해 얻은 Authentication 객체는 보안 컨텍스트의 전체 인증 정보를 제공한다.
여기에는 `Principal`(사용자 ID), `authorities`(사용자 권한), `credentials`(사용자 비밀번호) 등의 훨씬 다양한 정보가 들어가있다. 만약 사용자 권한이 필요한 경우에는 Authentication 객체를 불러와야만 한다.
반면에 Principal 은 Authentication 객체의 일부로, 일반적으로 사용자의 식별 정보만을 제공한다.
따라서, 사용자 이름만 필요한 경우 Principal을 사용하면 간편하게 사용자의 이름을 얻을 수 있다. 즉 이 방식을 소개한 이유는 회원별로 권한도 체크해서 특정 권한만 접근 가능한 api를 설계할때에는
마무리
즉 이 방식을 소개한 이유는 회원별로 권한도 체크해서 특정 권한만 접근 가능한 api를 설계할때에는 당연히 Authentication 객체를 불러와야겠지만 Principal을 사용하게 된다면 컨트롤러 단에 있어서 아래 예시 코드를 보게 된다면
@DeleteMapping("/{eventId}")
public ResponseEntity<?> deleteEvent(@PathVariable Long eventId, Principal principal) {
eventService.deleteEvent(principal.getName(), eventId);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
1. 딱보면 이 엔드포인트는 유저의 auth가 필요하구나를 확인하는 코드직관성 이 좋아진다.
2. 테스트 코드 작성시 Principal 대신에 mock 객체를 보내 테스트 코드 작성이 편해진다.
3. ContextHolder를 거쳐서 객체를 불러오고 다시 principal 이나 getName을 하는 것보다 단순하게 구현이 가능하다.
결론적으로는 차이와 특징을 잘 이해하고 적절히 사용하여 더 나은 코드를 짜보자
'Spring' 카테고리의 다른 글
JPA관계매핑시 @JoinColumn 옵션 그리고 JPA명명전략(naming strategy) (0) | 2023.06.25 |
---|---|
FetchType.EAGER vs FetchType.LAZY in JPA (0) | 2023.06.23 |
SpringBoot+MySQL(JPA) 회원가입,JWT 로그 구현하기 - (2) (1) | 2023.06.23 |
스프링 JPA: 엔티티 필드의 기본값에 대한 고찰 (0) | 2023.06.21 |
@Transient 어노테이션과 DTO를 사용한 레이어 분리 (0) | 2023.04.22 |