본격적인 코드 구현 전에 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)