/////
Search
🔐

시큐리티와 JWT 를 이용한 기본적인 인증 / 인가 프로세스

태그
Security
JWT
목차

필터의 순서

요청이 들어올때 JWT 필터를 먼저 거칠 것인가 시큐리티 필터를 먼저 거칠 것인가?

시큐리티의 동작 과정

요청이 들어오면 그림과 같이 AuthenticationFilter 를 거쳐 UsernamePasswordAuthenticationToken 을 만들고 이를 이용하여 그 다음 과정을 쭉쭉 거치게 됩니다. 결론적으로는 SecurityContextHolder 에 인증이 완료된 유저네임과 패스워드가 저장되게 되죠. 그렇다면 JWT 필터는 어떤가요?

JWT 필터

JWT 필터도 별반 다르지 않습니다. 요청이 들어오면 요청 헤더에 들어있는 토큰을 꺼내어 토큰 검증을 거칩니다. 유효한 토큰이라면 아래와 같이 시큐리티에 건내줄 수 있습니다.
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, accessToken, new ArrayList<>()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); }
Java
복사

결론은요?

요청이 들어오면 JWT 필터를 거치고 시큐리티 필터를 거치도록 하였습니다. 토큰을 먼저 꺼내보고 유효하지 않은 경우 아예 접근 자체를 못하게 하기 위해서였습니다.

시큐리티 커스텀

시큐리티에서 제공하는 form 로그인을 사용하지 않고, 로그인 리퀘스트로 받은 값을 그대로 사용하려면 시큐리티를 커스텀 해야 합니다. 그래서 시큐리티의 동작과정에서 보이는 클래스들을 아래와 같이 수정하였습니다.

CustomAuthenticationFilter.java

로그인 리퀘스트를 통해 아이디와 비밀번호를 받습니다.
이를 이용하여 UsernamePasswordAuthenticationToken 을 만듭니다.
@Slf4j public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { public CustomAuthenticationFilter(final AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); } @Override public Authentication attemptAuthentication (final HttpServletRequest request, final HttpServletResponse response) throws AuthenticationException { final UsernamePasswordAuthenticationToken authenticationToken; try { final LoginRequest loginRequest = new ObjectMapper().readValue(request.getInputStream(), LoginRequest.class); log.info("===================================================="); log.info("사용자 아이디: " + loginRequest.getEmail() + " 비밀번호: " + loginRequest.getPassword()); log.info("===================================================="); authenticationToken = new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()); } catch (IOException exception) { throw new org.springframework.security.authentication.AuthenticationServiceException("사용자 인증에 실패하였습니다.", exception); } setDetails(request, authenticationToken); return this.getAuthenticationManager().authenticate(authenticationToken); } }
Java
복사

CustomAuthenticationProvider.java

UsernamePasswordAuthenticationToken 으로부터 아이디와 비밀번호를 조회합니다.
CustomUserDetailsService 를 사용하여 아이디로 사용자를 조회하고, 존재한다면 이를 이용하여 새로운 UsernamePasswordAuthenticationToken 을 생성합니다.
@RequiredArgsConstructor @Slf4j public class CustomAuthenticationProvider implements AuthenticationProvider { private final CustomUserDetailsService customUserDetailsService; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Override public Authentication authenticate(Authentication authentication) { final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; // AuthenticationFilter 에서 생성된 토큰으로 부터 아이디와 비밀번호 조회 final String email = token.getName(); final String password = (String) token.getCredentials(); // UserDetailsService 를 통해 DB 에서 아이디로 사용자 조회 final CustomUserDetails userDetails; try { userDetails = (CustomUserDetails) customUserDetailsService.loadUserByUsername(email); } catch (Exception e) { throw new RuntimeException(e); } if (!bCryptPasswordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("비밀번호가 일치하지 않습니다."); } return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return authentication.equals(UsernamePasswordAuthenticationToken.class); } }
Java
복사

왜 UsernamePasswordAuthenticationToken 에 userDetails 을 넣나요?

인텔리제이에서 UsernamePasswordAuthenticationToken 을 한번 찾아가보면, 아래와같이 나옵니다.
파라미터를 보면 principal 은 Object 입니다. 그래서 유저의 아이디나 이메일같이 유저의 특정값이 아닌, 유저를 넣어주었습니다. 이 principal 은 아래와 같이 인증이 완료된 유저의 인증 정보를 꺼내올 때 사용할 수 있습니다.
public Long getLoginUserIndex() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Object principal = authentication.getPrincipal(); Long userId = ((User) principal).getId(); return userId; }
Java
복사
유저의 어떤 정보가 필요할 지 몰라서 User 자체를 넣어준 것이죠

CustomUserDetailsService.java

토큰을 통해 찾아온 이메일을 이용하여 특정 유저가 존재하는지 확인합니다.
만약 유저가 존재한다면, 이를 CustomUserDetails 에 넣어 반환 합니다.
@Service @RequiredArgsConstructor public class CustomUserDetailsService implements UserDetailsService { private final UserRepository userRepository; @Override public UserDetails loadUserByUsername(String email) { Optional<User> user = userRepository.findByEmail(email); if (user.isEmpty()) { throw new UsernameNotFoundException("해당 유저를 찾을 수 없습니다. 유저 이메일: " + email); } return new CustomUserDetails(user.get()); } }
Java
복사

CustomUserDetails.java

사용자를 저장합니다.
public record CustomUserDetails(User user) implements UserDetails { @Override public Collection<? extends GrantedAuthority> getAuthorities() { return Collections.singleton(new SimpleGrantedAuthority("ROLE_MEMBER")); } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getEmail(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
Java
복사