
본격적인 코드 구현 전에 Spring Security의 인증 절차에 대해 알아보자.

- 웹 요청(로그인 요청)이 들어오면 AuthenticationFilter에서 로그인 정보(이름, 비밀번호)를 통해
UsernamePasswordAuthenticationToken을 생성AuthenticationManager는 검증 단계를 총괄하는 클래스인AuthenticationProvider에게 인증 처리를 위임(토큰 전달).AuthenticationProvider는 토큰 정보와 일치하는 사용자를 DB에서 조회(userDetailsService의loadUserByUsername메서드 이용)PasswordEncoder의 matches()를 통해 비밀번호 일치 여부 확인. 일치 시에UserDetails를 포함한Authentication객체 반환.- 인증 성공 시
SecurityContext에Authentication객체 저장.
UsernamePasswordAuthenticationToken: Authentication 구현체. 사용자 이름, 비밀번호로 구성된 토큰.- 인증 완료된 이후의
Authentication: 사용자 정보(UserDetails)로 구성됨SecurityContext: 현재 실행 중인 스레드에 연결된 보안 정보를 저장하고 관리
JWT 로그인은 여기에 인증 성공시 토큰을 발급받는 로직을 별도로 추가해야한다!
우선 인증 필터 역할을 할 JwtAuthenticationFilter 부터 구현하자.
앞서 다뤘듯이 인증 객체를 반환받을 AuthenticationManager를 주입받아야 한다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final SecurityService securityService;
로그인 요청으로부터 사용자 이름, 비밀번호를 가져와 인증 토큰을 생성하고, 이를 통해 Authentication 객체를 받아오는 메서드.
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
// 요청에서 로그인 정보 가져오기
ObjectMapper om = new ObjectMapper();
MemberLoginRequest loginParam = om.readValue(request.getInputStream(), MemberLoginRequest.class);
// 인증 토큰 생성
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginParam.name(), loginParam.password());
// 인증
return authenticationManager.authenticate(authenticationToken);
} catch (IOException e) {
return null;
}
}
인증 성공시 동작하는 메서드로 인증된 Authentication 객체에서 UserDetatil를 꺼내, JWT 토큰 발급, response에 access 토큰 DTO 반환.
// 인증 성공 시
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
// authentication에서 userDetails 추출
CustomUserDetails userDetails = (CustomUserDetails) authResult.getPrincipal();
// JWT 토큰 발급
AccessTokenResponse accessTokenResponse = securityService.getAccessTokenResponse(userDetails);
// response body에 access 토큰 DTO 담기
responseWriter.writeAccessTokenResponse(response, accessTokenResponse);
}
사용자의 이름을 기반으로 사용자 정보를 불러오는 메서드.
UserDetails는 Member 엔티티를 통해 생성.
// 이름으로 회원 조회
@Override
public CustomUserDetails loadUserByUsername(String username) {
Member member = memberRepository.findByName(username)
.orElseThrow(() -> new UsernameNotFoundException(MEMBER_NAME_NOT_FOUND.getMessage()));
return new CustomUserDetails(member);
}
사용자의 정보(이름, 비밀번호, 권한 등)들을 갖고 있는 UserDetails.
public record CustomUserDetails(Member member) implements UserDetails {
// 권환 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<Role> roles = member.getRoles();
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getRole()))
.collect(Collectors.toList());
}
// 비밀번호 반환
@Override
public String getPassword() {
return member.getPassword();
}
// 이름 반환
@Override
public String getUsername() {
return member.getName();
}
// 계정 만료 여부
@Override
public boolean isAccountNonExpired() {
return true;
}
// 계정 잠김 여부
@Override
public boolean isAccountNonLocked() {
return true;
}
// 자격 증명 만료 여부
@Override
public boolean isCredentialsNonExpired() {
return true;
}
// 계정 활성화 여부
@Override
public boolean isEnabled() {
return true;
}
}
구현한 인증 필터는 다음과 같이 설정할 수 있다.
인증 필터가 실행될 로그인 요청 경로를 정하고, 로그인 요청 실패시 예외를 처리할 핸들러를 설정할 수 있다.
// 인증 필터 설정
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager, securityService, responseWriter);
jwtAuthenticationFilter.setFilterProcessesUrl("/api/security/login");
jwtAuthenticationFilter.setAuthenticationFailureHandler(new JwtAuthenticationFailureHandler(responseWriter));
이제 SecurityConfig의 FilterChain에 등록하도록 하자.
.addFilter(jwtAuthenticationFilter)