시큐리티 구조 요약 작성중 . .
시큐리티는 아래와 같은 구조로 Authentication 이라는 객체(인터페이스)가 존재합니다.
그리고 Authentication 을 통해 회원의 정보를 저장하고 있으며,
회원의 정보는 Principal 과 GrantAuthority 라는 것으로 나뉘게 됩니다.
Principal 은 회원이 "누구"인지를 나타내며 회원의 이름,비밀번호 등을 가지는 회원 그 자체에 대한 정보라고 보면 되고, 또 Principal은 User 또는 UserDetails 의 타입으로 되어 있습니다. (뒤에서 추가 설명)
GrantAuthority 는 그 회원에 대한 권한을 가지고 있는데 인증이 완료되고 나서 해당 정보를 참조하여 접근 권한을 지정합니다.
그리고 Principal 은 "누구"에 대한 정보라고 하였는데, 우리가 직접 만든 회원에 대한 엔티티 Member나 Account와 같은 객체를 저장하면 편하겠지만 스프링 시큐리티는 Principal 에User 나 UserDetails 타입만 올 수 있습니다. (User는 구현체, UserDetails 는 인터페이스)
그래서 User를 상속하여 Member extands User 와 같이 엔티티를 만들던가
아니면 아래와 같이 Member를 품고 있는 User 구현체를 만들게 됩니다.
public class SecurityMember extends User {
@Getter
private final Member member;
public SecurityMember(Member member) {
super(member.getLoginId(), member.getPassword(), List.of(new SimpleGrantedAuthority(member.getRole().name())));
this.member = member;
}
}
위와 같이 만들면 SecurityMember 는 User 타입이니깐 Authentication에 저장을 할 수 있고, 또한 member의 정보를 가질 수 있습니다.
아래와 같이 User 타입인 SecurityMember 를 저장하여 시큐리티가 인가 처리를 할 수 있게 해주고, 로그인된 유저 정보가 필요할 땐 SecurityMember.getMember()를 하여 회원 엔티티를 가져올 수 있습니다.
마지막으로 우리가 만든건 회원 엔티티인데 어떻게 User 타입으로 변환하여 Authentication에 전달할 수 있을까요 ?
바로 UserDetailsService 를 구현해야 합니다.
@Service
@RequiredArgsConstructor
public class SecurityMemberService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String loginId) {
Member member = memberRepository.findByLoginId(loginId)
.orElseThrow(() -> new MemberNotExistException());
return new SecurityMember(member);
}
}
해당 UserDetailsService 를 구현하게 되면 폼 로그인 방식으로 진행했을 때 시큐리티 필터에서 해당 객체를 통해 우리가 만들어둔 MemberSecurity를 만들어 Authentication에 자동으로 넣어줍니다.
물론 JWT 토큰 방식으로 구현할 경우에는 폼로그인이 아니기에 직접 loadUserByUsername() 를 호출해서 UserDetails를 가져와야해서 굳이 UserDetailsService 안해도 되겠지만, 추후 세션 인증 방식으로 변경될 수 있기에 구현은 해두었습니다.
또한 중요한 점은 시큐리티는 다음과 같이 여러개의 필터를 두고 여러 작업을 수행합니다.
그럼 이제 로그인을 완료하고 JWT(토큰) 를 발급받았다고 가정하겠습니다.
그리고 클라이언트가 서버로 무언가 요청을 할 때 마다 Token 과 함께 요청을 보내게 됩니다.
그럼 다음과 같이 필터에서 토큰을 검증하는 단계를 거치게 됩니다
위와 같은 과정이 JwtAuthFilter 클래스에서 진행된다고 보면됩니다
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws
IOException,
ServletException {
String token = jwtTokenProvider.resolveToken((HttpServletRequest)request);
if (token != null && jwtTokenProvider.isValidToken(token)) {
Authentication auth = jwtTokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
만약 위의 코드에서 token 이 null이 아니고, isValidToken() 도 true를 반환되어 if문 안으로 들어갔다고 해봅시다.
그럼 jwtTokenProvider를 통해 token에 있는 정보를 가지고 Authentication을 얻어낼 수 있습니다.
그리고 마지막으로 setAuthentication() 을 통해 다음 그림과 같이 Authentication을 저장하는 것이죠
SecurityContextHolder 는 스레드 로컬을 사용하여 정보를 제공하기 때문에 다음 그림처럼 요청 시작부터 응답이 나가기 전까지만 Authentication 을 참조할 수 있습니다.
요청이 왔을 때 토큰을 받았다면 어떤 컨트롤러에서든 Authentication 을 가져올 수 있게 되고, 로그인된 사용자가 누구인지 알 수 있게 됩니다.
간단하게 @AuthenticationPrincipal 을 사용하여 Authentication 의 UserDetails를 가져올 수도 있고 또한 커스텀 애노테이션을 만들면 다음과 같이 편하게 사용자 및 사용자 아이디를 가져올 수 있습니다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member.getId()")
public @interface LoginMemberId {
}
@PostMapping(value = "/test")
public String test(@LoginMemberId Long memberId, @LoginMember Member member){
log.info("member Id = ",memberId); // member 의 id값 노출
log.info("member = ",member); // member 노출
return "ok";
}
글 재미있게 봤습니다.