반응형
이번에는 시큐리티의 인증 제공자와 인증에 성공, 실패했을때 결과를 핸들링할 handler 클래스를 작성하자
1. 인증 제공자 클래스 작성 - CustomAuthenticationProvider
1-1. CustomAuthenticationProvider 클래스 작성 (인증 제공자)
- 이 코드는 CustomAuthenticationProvider라는 클래스로, 스프링 시큐리티의 AuthenticationProvider 인터페이스를 구현한 사용자 정의 인증 제공자이다. 이 클래스는 사용자의 인증을 처리하는 역할을 한다.
@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {
private final UserDetailsService userDetailsService;
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.debug("2.CustomAuthenticationProvider");
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// AuthenticationFilter에서 생성된 토큰으로부터 ID, PW를 조회
String loginId = token.getName();
String userPassword = (String) token.getCredentials();
// Spring security - UserDetailsService를 통해 DB에서 username으로 사용자 조회
SecurityUserDetailsDto securityUserDetailsDto = (SecurityUserDetailsDto) userDetailsService.loadUserByUsername(loginId);
// 대소문자를 구분하는 matches() 메서드로 db와 사용자가 제출한 비밀번호를 비교
if (!bCryptPasswordEncoder().matches(userPassword, securityUserDetailsDto.getUserDto().password())) {
throw new BadCredentialsException(securityUserDetailsDto.getUsername() + "Invalid password");
}
// 인증이 성공하면 인증된 사용자의 정보와 권한을 담은 새로운 UsernamePasswordAuthenticationToken을 반환한다.
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(securityUserDetailsDto, userPassword, securityUserDetailsDto.getAuthorities());
return authToken;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
1-2. 클래스 설명
- "CustomAuthenticationProvider는 사용자 인증 처리에 핵심적인 역할을 한다. 이 클래스는 사용자 이름과 비밀번호를 기반으로 인증 과정을 수행하는데, 이 과정은 CustomAuthenticationFilter, AuthenticationManager를 거쳐 마지막으로 CustomAuthenticationProvider에서 처리된다.
1-3. authenticate 메서드 설명
- authenticate 메서드는 Authentication 객체를 매개변수로 받아 사용자 이름과 비밀번호를 추출하며, 내가 재정의한 CustomUserDetailsService를 사용해 데이터베이스에서 사용자 정보를 로드한다. 로드된 사용자 정보는 Bcrypt로 암호화된 비밀번호와 사용자가 입력한 폼의 비밀번호를 비교하는 데 사용된다. 인증 과정이 성공하면, 인증된 사용자 정보와 권한이 포함된 UsernamePasswordAuthenticationToken 객체를 반환한다. 반면, 인증이 실패하면 BadCredentialsException이 발생한다. 이 과정은 시스템의 보안성을 높이는 데 중요한 역할을 하며, 사용자 인증에 있어서 핵심적인 부분을 담당한다."
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
log.debug("2.CustomAuthenticationProvider");
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
// AuthenticationFilter에서 생성된 토큰으로부터 ID, PW를 조회
String loginId = token.getName();
String userPassword = (String) token.getCredentials();
// Spring security - UserDetailsService를 통해 DB에서 username으로 사용자 조회
SecurityUserDetailsDto securityUserDetailsDto = (SecurityUserDetailsDto) userDetailsService.loadUserByUsername(loginId);
// 대소문자를 구분하는 matches() 메서드로 db와 사용자가 제출한 비밀번호를 비교
if (!bCryptPasswordEncoder().matches(userPassword, securityUserDetailsDto.getUserDto().password())) {
throw new BadCredentialsException(securityUserDetailsDto.getUsername() + "Invalid password");
}
// 인증이 성공하면 인증된 사용자의 정보와 권한을 담은 새로운 UsernamePasswordAuthenticationToken을 반환한다.
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(securityUserDetailsDto, userPassword, securityUserDetailsDto.getAuthorities());
return authToken;
}
1-4. supports 메서드
- 이 메서드는 AuthenticationProvider가 특정 Authentication 타입을 지원하는지 여부를 반환한다. 여기서는 UsernamePasswordAuthenticationToken 클래스를 지원한다고 명시되어 있다.
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
1-5. 동작 시점
- 사용자가 로그인을 시도할 때, CustomAuthenticationFilter 클래스의 attemptAuthentication 메서드에서 생성된 UsernamePasswordAuthenticationToken 객체가 AuthenticationManager에 전달된다.
- 전달된 토큰을 받은 AuthenticationManager는 등록된 AuthenticationProvider(인증 제공자)들 중에서 해당 토큰을 처리할 수 있는 제공자를 찾는다. 이 때 바로 위에 있는 supports 메서드가 사용된다.
- 만약 CustomAuthenticationProvider가 해당 토큰을 지원한다면 (supports 메서드가 true를 반환한다면), CustomAuthenticationProvider 클래스 내부의 authenticate 메서드가 호출되어 사용자의 인증을 처리하게 된다.
요약하면, CustomAuthenticationProvider 클래스는 사용자의 인증을 처리하는 사용자 정의 인증 제공자로, 사용자가 로그인을 시도할 때 해당 제공자의 authenticate 메서드가 호출되어 인증 과정이 수행된다.
2. 인증 성공 핸들러 작성 - CustomAuthSuccessHandler
2-1. 인증에 성공했을때 동작할 CustomAuthSuccessHandler 클래스 작성
- CustomAuthSuccessHandler 클래스는 스프링 시큐리티의 SavedRequestAwareAuthenticationSuccessHandler를 상속받아 사용자 정의 인증 성공 핸들러를 구현한 것이다. 이 핸들러는 사용자가 인증에 성공했을 때 수행되는 로직을 정의한다.
@Slf4j
@RequiredArgsConstructor
@Component
public class CustomAuthSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
log.debug("3.CustomLoginSuccessHandler");
// 1. 사용자와 관련된 정보를 모두 조회한다.
UserDto userDto = ((SecurityUserDetailsDto) authentication.getPrincipal()).getUserDto();
// 2. 조회한 데이터를 JSONObject 형태로 파싱한다.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
JSONObject userDtoObject = new JSONObject(objectMapper.writeValueAsString(userDto));
HashMap<String, Object> responseMap = new HashMap<>();
JSONObject jsonObject;
// 3-1. 사용자의 상태가 '휴먼 상태' 인 경우에 응답값으로 전달할 데이터
if (Objects.equals(userDto.status(), "D")) {
responseMap.put("userInfo", userDtoObject);
responseMap.put("resultCode", 9001);
responseMap.put("token", null);
responseMap.put("failMessage", "휴면 계정입니다.");
jsonObject = new JSONObject(responseMap);
}
// 3-2. 사용자의 상태가 '휴먼 상태'가 아닌 경우에 응답값으로 전달할 데이터
else {
// 1. 일반 계정일 경우 데이터 세팅
responseMap.put("userInfo", userDtoObject);
responseMap.put("resultCode", 200);
responseMap.put("failMessage", null);
jsonObject = new JSONObject(responseMap);
// JWT 토큰 생성
String token = TokenUtils.generateJwtToken(userDto);
jsonObject.put("token", token);
// 쿠키에 JWT 토큰 저장
Cookie jwtCookie = new Cookie("jwt", token);
jwtCookie.setHttpOnly(true); // JavaScript에서 쿠키에 접근할 수 없도록 설정
jwtCookie.setPath("/"); // 모든 경로에서 쿠키에 접근 가능하도록 설정
response.addCookie(jwtCookie); // 응답에 쿠키 추가
}
// 4. 구성한 응답값을 전달한다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try (PrintWriter printWriter = response.getWriter()){
printWriter.print(jsonObject); // 최종 저장된 '사용자 정보', '사이트 정보'를 Front에 전달
printWriter.flush();
}
}
}
2-2. onAuthenticationSuccess 메서드
- 이 메서드는 사용자가 인증에 성공했을 때 호출된다.
@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) throws IOException {
log.debug("3.CustomLoginSuccessHandler");
// 1. 사용자와 관련된 정보를 모두 조회한다.
UserDto userDto = ((SecurityUserDetailsDto) authentication.getPrincipal()).getUserDto();
// 2. 조회한 데이터를 JSONObject 형태로 파싱한다.
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
JSONObject userDtoObject = new JSONObject(objectMapper.writeValueAsString(userDto));
HashMap<String, Object> responseMap = new HashMap<>();
JSONObject jsonObject;
// 3-1. 사용자의 상태가 '휴먼 상태' 인 경우에 응답값으로 전달할 데이터
if (Objects.equals(userDto.status(), "D")) {
responseMap.put("userInfo", userDtoObject);
responseMap.put("resultCode", 9001);
responseMap.put("token", null);
responseMap.put("failMessage", "휴면 계정입니다.");
jsonObject = new JSONObject(responseMap);
}
// 3-2. 사용자의 상태가 '휴먼 상태'가 아닌 경우에 응답값으로 전달할 데이터
else {
// 1. 일반 계정일 경우 데이터 세팅
responseMap.put("userInfo", userDtoObject);
responseMap.put("resultCode", 200);
responseMap.put("failMessage", null);
jsonObject = new JSONObject(responseMap);
// JWT 토큰 생성
String token = TokenUtils.generateJwtToken(userDto);
jsonObject.put("token", token);
// 쿠키에 JWT 토큰 저장
Cookie jwtCookie = new Cookie("jwt", token);
jwtCookie.setHttpOnly(true); // JavaScript에서 쿠키에 접근할 수 없도록 설정
jwtCookie.setPath("/"); // 모든 경로에서 쿠키에 접근 가능하도록 설정
response.addCookie(jwtCookie); // 응답에 쿠키 추가
}
// 4. 구성한 응답값을 전달한다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try (PrintWriter printWriter = response.getWriter()){
printWriter.print(jsonObject); // 최종 저장된 '사용자 정보', '사이트 정보'를 Front에 전달
printWriter.flush();
}
}
2-3. 로직의 동작 순서:
- 사용자 정보 조회: Authentication 객체에서 사용자 정보를 가져온다.
- 데이터 파싱: 조회한 사용자 데이터를 JSONObject 형태로 변환한다.
- 응답 데이터 구성: 사용자의 상태에 따라 응답 데이터를 다르게 구성한다.
- 휴면 상태: 사용자의 상태가 '휴먼 상태'인 경우, 해당 정보와 함께 특정 응답 코드와 메시지를 응답 데이터에 포함시킨다.
- 일반 상태: 사용자의 상태가 '휴먼 상태'가 아닌 경우, 사용자 정보와 함께 정상 응답 코드를 응답 데이터에 포함시킨다. 또한, JWT 토큰을 생성하고 이를 응답 데이터와 쿠키에 포함시킨다.
- 응답 전송: 구성한 응답 데이터를 클라이언트에 전송한다.
2-5. 주요 부분 설명:
- JWT 토큰 생성:
- TokenUtils.generateJwtToken(userDto)를 통해 사용자 정보를 기반으로 JWT 토큰을 생성한다.
- TokenUtils.generateJwtToken(userDto)를 통해 사용자 정보를 기반으로 JWT 토큰을 생성한다.
- 쿠키 저장:
- 생성된 JWT 토큰을 jwt라는 이름의 쿠키에 저장하고, 이 쿠키를 응답에 포함시킨다. 이렇게 함으로써 클라이언트는 이후의 요청에서 이 쿠키를 포함하여 서버에 전송할 수 있다.
요약하면, CustomAuthSuccessHandler는 사용자가 인증에 성공했을 때 수행되는 로직을 정의하는 핸들러이다. 사용자의 상태에 따라 다른 응답을 구성하고, 일반 상태의 사용자에게는 JWT 토큰을 발급하여 응답과 쿠키에 포함시킨다.
3. 인증 실패 핸들러 작성 - CustomAuthFailureHandler
3-1. 인증이 실패했을때 동작할 CustomAuthFailureHandler 클래스 작성
- CustomAuthFailureHandler클래스는 스프링 시큐리티의 AuthenticationFailureHandler 인터페이스를 구현하여 사용자 정의 인증 실패 핸들러를 정의한 것이다. 이 핸들러는 사용자의 인증 요청이 실패했을 때 수행되는 로직을 정의한다.
@Slf4j
@Configuration
@Component
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
/**
* 이 메서드는 HTTP 요청, HTTP 응답, 그리고 인증 예외를 인자로 받는다. 인증 예외의 타입에 따라 실패 메시지를 설정하고, 이를 JSON 형태로 클라이언트에 응답한다.
*/
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException {
// [STEP.1] 클라이언트로 전달할 응답값을 구성한다.
JSONObject jsonObject = new JSONObject();
String failMessage = "";
// [STEP.2] 발생한 Exception에 대해서 확인한다.
if (exception instanceof AuthenticationServiceException) {
failMessage = "로그인 정보가 일치하지 않습니다.";
} else if (exception instanceof LockedException) {
failMessage = "계정이 잠겨 있습니다.";
} else if (exception instanceof DisabledException) {
failMessage = "계정이 비활성화되었습니다.";
} else if (exception instanceof AccountExpiredException) {
failMessage = "계정이 만료되었습니다.";
} else if (exception instanceof CredentialsExpiredException) {
failMessage = "인증 정보가 만료되었습니다.";
}
// [STEP.3] 응답값을 구성하고 전달한다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try (PrintWriter printWriter = response.getWriter()) {
log.debug(failMessage);
HashMap<String, Object> resultMap = new HashMap<>();
resultMap.put("userInfo", null);
resultMap.put("resultCode", 9999);
resultMap.put("failMessage", failMessage);
jsonObject = new JSONObject(resultMap);
printWriter.print(jsonObject);
printWriter.flush();
}
}
}
3-2. onAuthenticationFailure 메서드 설명
- 이 메서드는 사용자의 인증 요청이 실패했을 때 호출된다.
@Override
public void onAuthenticationFailure(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception
) throws IOException {
// [STEP.1] 클라이언트로 전달할 응답값을 구성한다.
JSONObject jsonObject = new JSONObject();
String failMessage = "";
// [STEP.2] 발생한 Exception에 대해서 확인한다.
if (exception instanceof AuthenticationServiceException) {
failMessage = "로그인 정보가 일치하지 않습니다.";
} else if (exception instanceof LockedException) {
failMessage = "계정이 잠겨 있습니다.";
} else if (exception instanceof DisabledException) {
failMessage = "계정이 비활성화되었습니다.";
} else if (exception instanceof AccountExpiredException) {
failMessage = "계정이 만료되었습니다.";
} else if (exception instanceof CredentialsExpiredException) {
failMessage = "인증 정보가 만료되었습니다.";
}
// [STEP.3] 응답값을 구성하고 전달한다.
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json");
try (PrintWriter printWriter = response.getWriter()) {
log.debug(failMessage);
HashMap<String, Object> resultMap = new HashMap<>();
resultMap.put("userInfo", null);
resultMap.put("resultCode", 9999);
resultMap.put("failMessage", failMessage);
jsonObject = new JSONObject(resultMap);
printWriter.print(jsonObject);
printWriter.flush();
}
}
3-3. 로직의 동작 순서
- 응답 데이터 초기화: 초기 응답 데이터를 설정한다.
- 예외 타입 확인: 발생한 AuthenticationException의 타입을 확인하여 실패 메시지를 설정한다. 각 예외 타입에 따라 다른 메시지를 설정한다.
- 응답 데이터 구성 및 전송: 설정된 실패 메시지와 함께 응답 데이터를 구성하고 클라이언트에 전송한다.
3-4. 주요 부분 설명
예외 타입에 따른 메시지 설정: 발생한 AuthenticationException의 타입에 따라 다음과 같은 메시지를 설정한다.
- AuthenticationServiceException: 로그인 정보가 일치하지 않습니다.
- LockedException: 계정이 잠겨 있습니다.
- DisabledException: 계정이 비활성화되었습니다.
- AccountExpiredException: 계정이 만료되었습니다.
- CredentialsExpiredException: 인증 정보가 만료되었습니다.
- 응답 데이터 전송: 설정된 실패 메시지와 함께 응답 데이터를 JSON 형태로 클라이언트에 전송한다.
요약하면, CustomAuthFailureHandler는 사용자의 인증 요청이 실패했을 때 수행되는 로직을 정의하는 핸들러이다. 발생한 예외 타입에 따라 다른 실패 메시지를 설정하고, 이 메시지와 함께 응답 데이터를 클라이언트에 전송한다.
Jwt의 유효성을 검증하는 인터셉터 클래스를 작성하자👇🏻👇🏻
반응형
'Spring > Spring Security' 카테고리의 다른 글
Spring Boot 3 & Security 6 시리즈: JWT Util 클래스 작성 (7편) (0) | 2023.08.07 |
---|---|
Spring Boot 3 & Security 6 시리즈: JWT 검증 인터셉터 작성하기 (6편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: JWT 인증 필터 JwtAuthorizationFilter 작성(4편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: WebConfig 클래스 작성 (3편) (0) | 2023.08.07 |
Spring Boot 3 & Security 6 시리즈: SecurityConfig 클래스 작성하기 (2편) (4) | 2023.08.07 |