슈룹 프로젝트 시큐리티

박진영·2023년 8월 17일
0

시큐리티 구조 요약 작성중 . .

시큐리티는 아래와 같은 구조로 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";
    }
profile
안녕하세요

1개의 댓글

comment-user-thumbnail
2023년 8월 17일

글 재미있게 봤습니다.

답글 달기