티스토리 뷰
Springboot JWT 로그인 - (4) UsernamePasswordAuthenticationFilter를 통한 로그인 로직 구현
구름뭉치 2022. 6. 16. 09:53이전 포스팅에서 정리한 내용을 기반으로 로직을 작성해겠다. 회원 로그인을 검증하기 위한 UsernamePasswordAuthenticationFilter를 상속받아 로그인 검증을 하고 토큰을 발급받는 JwtAthenticationFilter 클래스를 구현해본다.
로그인 검증 로직
- 로그인 요청 (username, password)
- UsernamePasswordAuthenticationFilter에서 [username, password]를 이용해서 정상적인 로그인 여부를 검증
- DI로 받은 AuthenticationManager 객체를 통해 로그인을 시도한다.
- UserDetailsService를 상속받은 PrincipalDetailsService 클래스가 호출되고 loadUserByUsername() 메소드가 실행된다.
- 재정의된 loadUserByUsername() 메소드에서 회원_Repository에 접근하여 회원을 찾고 검증 후
UserDetails를 상속받은 PrincipalDetails 객체에 회원을 담아서 반환한다. - 스프링 시큐리티가 PasswordEncoder를 통해 password를 검증하고 확인이 되면 Authentication을 반환한다.
- UsernamePasswordAuthFilter에서 Authentication을 반환받고 이를 스프링 시큐리티에 다시 반환해준다.
- authentication을 반환하므로서 시큐리티_Session에 저장한다.
(권한 관리를 위해서 세션에 저장)
- authentication을 반환하므로서 시큐리티_Session에 저장한다.
- 검증이 완료되었으므로 JWT를 발급받는다.
JwtAuthenticationFilter 클래스 (UsernamePasswordAuthenticatonFilter 역할)
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
// 2. 정상적인 로그인 여부를 검증한다.
// 3. 로그인 성공
// 4. authentication을 반환해준다.
return super.attemptAuthentication(request, response);
}
// attemptAuthentication()에서 인증이 성공되면 다음 수행되는 메서드, JWT를 발급해준다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");
// 5. JWT 발급
super.successfulAuthentication(request, response, chain, authResult);
}
}
작성한 주석내용들을 채워나가보겠다.
1. Username, Password를 받는다.
request로 넘어오는 [username, password]를 받아서 로그인 요청 객체를 생성 후 Authenticate를 위한 UsernamePasswordAuthenticationToken을 발행한다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
log.info("1. username, password 받는다.");
LoginReq login = om.readValue(request.getInputStream(), LoginReq.class);
log.info(login.toString());
// username, password를 이용해서 token 발급
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());
log.info(authenticationToken.getPrincipal().toString());
log.info(authenticationToken.getCredentials().toString());
log.info("============================================================\n");
} catch (IOException e) {
e.printStackTrace();
}
// 2. 정상적인 로그인 여부를 검증한다.
// 3. 로그인 성공
// 4. authentication을 반환해준다.
return super.attemptAuthentication(request, response);
}
}
- ObjectMapper를 이용하여 HttpServletRequest 요청으로부터 LoginReq 객체를 생성
- 생성된 LoginReq 객체에 접근하여 username, password를 꺼내서 UsernamePasswordAuthenticationToken 발급
- 이때 username은 principal, password는 credentials가 된다.
요청 & 응답
2. 정상적인 로그인 여부를 검증한다.
(1번)에서 전달받은 로그인 정보를 이용해서 생성한 토큰을 가지고 로그인이 유효한지 검증하면된다.
이부분은 매우 명확하다. 회원의 존재 여부와 존재할 경우 해당 토큰의 (Principal == username && credentials == password)를 검증하면 된다. 하지만 패스워드를 비교하는 로직은 시큐리티 내부에서 검증되므로 따로 넣지않아도된다.
아이디/패스워드가 일치하면 알아서 authentication을 반환해주고, 아니면 연결이 종료된다.
authenticationManager 클래스의 authenticate()에 토큰을 넘기면 자동으로
UserDetailsService.class -> loadUserByUsername() 메소드가 실행된다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
// 생략...
// 2. 정상적인 로그인 시도 여부를 검증한다.
log.info("2. 정상적인 로그인 시도 여부를 검증한다.");
// -> 로그인 정보를 가지고 임시로 Auth 토큰을 생성해서 인증을 확인한다.
// -> DI 받은 authenticationManager로 로그인 시도한다.
// -> DetailsService를 상속받은 PrincipalDetailsService가 호출되고 loadUserByUsername() 함수가 실행된다.
// authenticate()에 토큰을 넘기면 PrincipalDetailsService.class -> loadUserByUsername() 메소드 실행된다.
// DB에 저장되어있는 username & password가 일치하면 authentication이 생성된다.
log.info("->> Authenticate Start");
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
log.info("<<-- Authenticate End");
log.info("============================================================\n");
// 3. PrincipalDetails를 세션에 저장한다. (권한 관리를 위해서 세션에 저장)
// 4. JWT 토큰을 만들어서 응답해준다.
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
UserDetailsService를 상속받은 PrincipalUserDetailsService 클래스
@Service
@RequiredArgsConstructor
public class PrincipalDetailService implements UserDetailsService {
private final MemberRepository memberRepository;
// 시큐리티 session -> Authentication -> UserDetails
// 시큐리티 세션(내부 Authentication(내부 UserDetails(PrincipalDetails)))
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
log.info("PrincipalDetailService.loadUserByUsername");
log.info("LOGIN");
Member member = memberRepository.findByUsername(username)
.orElseThrow(() -> new BussinessException(ExMessage.MEMBER_ERROR_NOT_FOUND));
return new PrincipalDetails(member);
}
}
응답
3. 로그인 성공
이 부분이 수행된다는 것은 loadUserByUsername() 메서드에서 성공적으로 회원조회 및 username, password를 통한 검증이 이뤄졌다는 것을 의미한다.
따라서, 여기서 따로 수행해야할 부분은 없다. 다만 직접 확인을 위해 반환받은 authentication 객체에서 PrincipalDetails 객체를 꺼내서 username, password를 출력해보도록 하자.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
// 생략
// 2. 정상인지 로그인 시도를 해본다.
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
// 생략
// 3. 로그인이 되었다.
log.info("3. 로그인 성공.");
// 로그인이 되었다.
// Authentication에 있는 인증된 Principal 객체를 PrincipalDetails 객체로 꺼낸다.
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
log.info("username : " + principalDetails.getMember().getUsername());
log.info("password : " + principalDetails.getMember().getPassword());
log.info("============================================================\n");
// 4. authentication을 반환해준다.
return authentication;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");
super.successfulAuthentication(request, response, chain, authResult);
}
}
응답
- 처음 요청을 받았을 때 출력했던 principal, credentials 중 credentials가 암호화된것을 확인할 수 있다. 이는 실제 DB에 저장된 회원을 잘 조회해서 가져왔다는 것으로 회원 객체는 저장될 때 PasswordEncoder에 의해 password를 암호화해서 저장하기 때문이다.
4. authentication을 반환해준다.
authentication 객체를 시큐리티_session에 저장해야 하므로 반환한다. 세션에 저장하면 편리하게 권한관리를 할 수 있다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
// 2. 정상인지 로그인 시도를 해본다.
// 3. 로그인이 되었다.
// 4. authentication을 반환해준다.
log.info("4. authentication 반환");
return authentication;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");
super.successfulAuthentication(request, response, chain, authResult);
}
}
반환된 Authentication 객체가 세션에 저장된다.
응답
- 정상적으로 모든 인증이 되고 자동적으로 successfulAuthentication() 메소드가 이어서 수행되는 것을 볼 수 있다.
- 여기서 JWT 토큰을 최종적으로 발행해서 반환해주면 클라이언트는 그걸 가지고 요청을 주면 된다.
전체 코드
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
// login 요청을 하면 로그인 시도를 위해서 실행되는 함수
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
log.info("로그인 시도 : JwtAuthenticationFilter.attemptAuthentication");
ObjectMapper om = new ObjectMapper();
try {
// 1. username, password 받는다.
log.info("1. username, password 받는다.");
LoginReq login = om.readValue(request.getInputStream(), LoginReq.class);
log.info(login.toString());
// username, password를 이용해서 token 발급
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(login.getUsername(), login.getPassword());
log.info(authenticationToken.getPrincipal().toString());
log.info(authenticationToken.getCredentials().toString());
log.info("============================================================\n");
// 2. 정상인지 로그인 시도를 해본다.
log.info("2. 정상인지 로그인 시도를 해본다.");
// -> 로그인 정보를 가지고 임시로 Auth 토큰을 생성해서 인증을 확인한다.
// -> DI 받은 authenticationManager로 로그인 시도한다.
// -> DetailsService를 상속받은 PrincipalDetailsService가 호출되고 loadUserByUsername() 함수가 실행된다.
// authenticate()에 토큰을 넘기면 PrincipalDetailsService.class -> loadUserByUsername() 메소드 실행된다.
// DB에 저장되어있는 username & password가 일치하면 authentication이 생성된다.
log.info("->> Authenticate Start");
Authentication authentication =
authenticationManager.authenticate(authenticationToken);
log.info("<<-- Authenticate End");
log.info("============================================================\n");
// 3. 로그인이 되었다.
log.info("3. 로그인 성공.");
// 로그인이 되었다.
// Authentication에 있는 인증된 Principal 객체를 PrincipalDetails 객체로 꺼낸다.
PrincipalDetails principalDetails = (PrincipalDetails) authentication.getPrincipal();
log.info("username : " + principalDetails.getMember().getUsername());
log.info("password : " + principalDetails.getMember().getPassword());
log.info("============================================================\n");
// 4. authentication을 반환해준다.
// authentication 객체를 session에 저장해야 하므로 반환한다. 세션에 저장하면 편리하게 권한관리를 할 수 있다.
// 반환된 Authentication 객체가 세션에 저장된다.
log.info("4. authentication 반환");
return authentication;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// attemptAuthentication() 실행 후 인증이 정상적으로 완료되면 실행된다.
// 따라서, 여기서 JWT 토큰을 만들어서 request 요청한 사용자에게 JWT 토큰을 response 해준다.
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
log.info("인증 완료 : JwtAuthenticationFilter.successfulAuthentication");
super.successfulAuthentication(request, response, chain, authResult);
}
}
전체 응답
다음 포스팅에서 JWT를 발급해보도록 하겠다.
'프로젝트 > JWT 방식 인증&인가 시리즈' 카테고리의 다른 글
Springboot JWT 로그인 - (6) RefreshToken 방식을 사용하도록 변경 & 코드 정리 (5) | 2022.06.24 |
---|---|
Springboot JWT 로그인 - (5) 클라이언트 요청 시 BasicAuthenticationFilter를 이용한 JWT 검증 (6) | 2022.06.20 |
Springboot JWT 로그인 - (3) UserDetails, UserDetailsService 이해 (0) | 2022.06.13 |
Springboot JWT 로그인 - (2) Filter 적용 테스트 (0) | 2022.06.09 |
Springboot JWT 로그인 - (1) Filter에 대한 이해 (0) | 2022.06.06 |
- Total
- Today
- Yesterday