✅ 커밋 & 푸시 & 머지 완료

현재 로그인 로직에 대한 설명
SpringSecurity 필터 체인에 커스텀 필터를 추가하여 로그인한 사용자 정보를 인증하는 로직이다. 구현시 신경썼던 부분은 탈퇴한 회원은 로그인이 안되도록 설정한 부분이다. 왜냐하면 회원 탈퇴시 바로 회원정보를 삭제하지 않고, 회원의 상태를 '탈퇴' 로 표시하여 스케줄링을 이용해 30일간 보관되도록 구현했기때문에 추가 처리를 해주지 않으면 탈퇴한 회원의 정보로 로그인이 가능한 불상사가 일어나게 된다. 그래서 커스텀 필터를 구현할 때, 탈퇴 회원을 검사하는 로직을 집어넣어서 탈퇴 회원의 로그인이 불가하도록 했다.
'.apply(new CustomFilterConfigurer())'
필터 체인을 보면 추가된 커스텀 필터를 확인할 수 있다.
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity(debug = true)
public class SecurityConfiguration {
private final JwtTokenizer jwtTokenizer;
private final MemberRepository memberRepository;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;
private final CustomOAuth2UserService oAuth2UserService;
@Bean
public SecurityFilterChain filterChain (HttpSecurity http) throws Exception {
http
.headers().frameOptions().sameOrigin()
.and()
.csrf().disable()
.cors(Customizer.withDefaults())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin().disable()
.httpBasic().disable()
.apply(new CustomFilterConfigurer()) //커스텀 필터 !
.and()
.exceptionHandling()
.authenticationEntryPoint(new MemberAuthenticationEntryPoint())
.accessDeniedHandler(new MemberAccessDeniedHandler())
.and()
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler)
.failureHandler(oAuth2LoginFailureHandler)
.userInfoEndpoint()
.userService(oAuth2UserService);
return http.build();
}
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager manager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter authentication = new JwtAuthenticationFilter(manager, jwtTokenizer,memberRepository); //jwtAuthenticationFilter attemptAuthentication() 메서드에서 로그인 처리
authentication.setFilterProcessesUrl("/members/login");
authentication.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
authentication.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter verification = new JwtVerificationFilter(jwtTokenizer);
builder .addFilter(authentication)
.addFilterAfter(verification, JwtAuthenticationFilter.class);
}
}
}
커스텀 필터를 구현한 configure 메소드를 좀 더 자세히 살펴보자.
public class CustomFilterConfigurer extends AbstractHttpConfigurer<CustomFilterConfigurer, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager manager = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter authentication = new JwtAuthenticationFilter(manager, jwtTokenizer,memberRepository);
authentication.setFilterProcessesUrl("/members/login");
authentication.setAuthenticationSuccessHandler(new MemberAuthenticationSuccessHandler());
authentication.setAuthenticationFailureHandler(new MemberAuthenticationFailureHandler());
JwtVerificationFilter verification = new JwtVerificationFilter(jwtTokenizer);
builder .addFilter(authentication)
.addFilterAfter(verification, JwtAuthenticationFilter.class);
}
}
① AbstractHttpConfigurer 를 확장한 CustomFilterConfigurer 클래스를 정의한다.
사용자 인증을 위해 JWT토큰을 사용하는 필터들을 HttpSecurity에 추가하고 구성하기 위함이다.
클래스 내의 configure 메소드에서는 HttpSecurity 객체를 받아와서 보안구성을 진행한다.
② AuthenticationManager
SpringSecurity에서 인증을 책임지는 주요 인터페이스로, builder.getSharedObject(...) 코드는 HttpSecurity 빌더에서 이미 구성된 AuthenticationManager를 가져오는 코드이다. 다시말해 HttpSecurity의 컨텍스트 안에 있는 AuthenticationManager 객체를 검색하여 사용하기 위한 코드다!
③ JwtAuthenticationFilter
"/members/login" url로 들어온 로그인 요청이 있을 때 활성화되는 필터로, 사용자가 로그인을 시도할 때 이 필터가 해동 요청을 가로채서 사용자의 인증 정보를 검증하고 JWT 토큰을 발급한다. 만약 인증에 실패하면 실패 핸들러가 호출된다.
④ JwtVericicationFilter
로그인 요청이 아닌, 즉, 이미 인증에 성공한 사용자의 요청시에 실행되는 필터로, 사용자가 시스템과 상호작용 중 보내는 모든 요청에서 JWT 토큰의 유효성을 검증한다. 로그인 후의 모든 요청에 대해 이 필터가 JWT토큰을 확인해서 유효한 사용자인지 검증한다.
⑤ addFilter & addFilterAfter
이 메서드들은 HttpSecurity에 필터를 추가한다. addFilter는 주어진 필터를 필터 체인에 추가하고, addFilterAfter는 지정된 필터 뒤에 새 필터를 추가한다. 현재 로직은 JwtAuthenticationFilter가 JwtVerificationFilter보다 먼저 추가되도록 설정돼 있다.
로그인 로직 동작흐름
❶ "members/login" url 로 로그인 요청이 들어옴
❷ 요청 가로채기
FilterChain에 정의된 CustomFilterConfigurer (커스텀필터)로 인해
JwtAuthenticationFilter가 이 요청을 가로챔
❸ JwtAuthenticationFilter의 attemptAuthentication 실행
JwtAuthenticationFilter의 attemptAuthentication 이 실행되는데,
이 메소드는 요청본문을 loginDto 객체로 변환하고 isDeletedMember로 탈퇴회원인지 확인.
그리고 사용자가 입력한 email과 password로 UsernamePasswordAuthenticationToken 생성한 뒤,
AuthenticationManager에게 사용자 정보가 담긴 이 객체를 넘김.
❹AuthenticationManager 가 내부적으로 인증 수행
AuthenticationManager(인증매니저)는 제공된 사용자 정보를 사용해 인증을 수행
이때, UserDetailsService를 통해 사용자 정보를 로드하고 비번이 일치하는지 확인함
👉 인증실패시,
❺successfulAuthentication 호출
인증매니저가 인증에 성공했다면, successfulAuthentication을 호출해서
현재 가지고 있는 인증된 사용자 정보를 바탕으로 JWT 토큰(access, refresh) 을 생성하고 HTTP 응답헤더에 생성된 토큰을 담음.
그다음 MemberAuthenticationSuccessHandler 의 onAuthenticationSuccess 메소드를 호출하여
LoginResponseDto에 필요한 사용자 정보와 JWT 토큰들을 담아 JSON형태로 응답본문에 작성.
👉 onAuthenticationSuccess 메소드는 사용자정보(MemberDetail)와 토큰추출 및 응답을 구성하는 역할!
❻ 다음 필터로 넘어감
다음 필터인 JwtVerificationFilter는 OncePerRequestFilter를 상속하는데, 이런 경우 Spring Security는 JwtVerificationFilter를 실행하기 전, suouldNotFilter 메소드를 먼저 호출하여 JwtVerificationFilter의 실행 여부를 결정함. 즉, 로그인 요청시 JwtVerificationFilter는 동작하지 않고 다음 필터로 건너 뛰게 됨.
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenizer jwtTokenizer;
private final MemberRepository memberRepository;
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//log.info("attemptAuthentication start");
ObjectMapper objectMapper = new ObjectMapper();
LoginDto loginDto = objectMapper.readValue(request.getInputStream(), LoginDto.class);
String email = loginDto.getEmail();
//log.info("email : "+email);
if(isDeletedMember(email)) throw new AuthenticationServiceException("Cannot login");
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDto.getEmail(), loginDto.getPassword());
//log.info("attemptAuthentication end");
return authenticationManager.authenticate(authenticationToken); //내부적으로 인증 후 Authentication 객체반환
}
private boolean isDeletedMember(String email) {
Optional<Member> member = memberRepository.findByEmail(email);
Member findMember = member.orElseThrow(()->new BusinessLogicException(ExceptionCode.MEMBER_NOT_FOUND));
Member.MemberStatus status = findMember.getStatus();
return status == Member.MemberStatus.DELETE;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws IOException, ServletException {
Member member = (Member)authResult.getPrincipal();
String accessToken = delegateAccessToken(member);
String refreshToken = delegateRefreshToken(member);
response.setHeader("Authorization","Bearer "+ accessToken);
response.setHeader("Refresh", refreshToken);
this.getSuccessHandler().onAuthenticationSuccess(request,response,authResult); //인증성공한 사용자를 어디로 이동시킬지 결정하는 핸들러메서드 실행
}
public String delegateAccessToken(Member member) {
Map<String, Object> claims = new HashMap<>();
claims.put("memberId", member.getMemberId());
claims.put("username", member.getEmail());
claims.put("isAdmin", member.getIsAdmin()); //추가 1
String subject = String.valueOf(member.getMemberId()); //✅
Date expiration = jwtTokenizer.getExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodedBase64SecretKey(jwtTokenizer.getSecretKey());
String accessToken = jwtTokenizer.generateAccessToken(claims,subject,expiration,base64EncodedSecretKey);
return accessToken;
}
private String delegateRefreshToken(Member member) {
String subject = member.getEmail();
Date expiration = jwtTokenizer.getExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
String base64EncodedSecretKey = jwtTokenizer.encodedBase64SecretKey(jwtTokenizer.getSecretKey());
String refreshToken = jwtTokenizer.generateRefreshToken(subject,expiration,base64EncodedSecretKey);
return refreshToken;
}
}
- 로그인 인증 성공 핸들러
@Slf4j
public class MemberAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//log.info("Login Successful 로그인 인증성공");
MemberDetailService.MemberDetail memberDetail = (MemberDetailService.MemberDetail) authentication.getPrincipal();
Long memberId = memberDetail.getMemberId();
String nickname = memberDetail.getNickname();
String imageUrl = memberDetail.getImageUrl(); //이미지 포함여부
String accessToken = response.getHeader("Authorization");
String refreshToken = response.getHeader("Refresh");
if(accessToken != null && accessToken.startsWith("Bearer ")) {
accessToken = accessToken.substring(7);
}
LoginResponseDto responseDto = new LoginResponseDto();
responseDto.setMemberId(memberId);
responseDto.setNickname(nickname);
responseDto.setImageUrl(imageUrl);
responseDto.setAccessToken(accessToken);
responseDto.setRefreshToken(refreshToken);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(responseDto));
}
}
- 로그인 인증 실패 핸들러
@Slf4j
public class MemberAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException ae) throws IOException, ServletException {
//log.error("Login Failed 로그인 인증실패 : {}", ae.getMessage());
sendErrorResponse(response);
}
private void sendErrorResponse(HttpServletResponse response) throws IOException {
Gson gson = new Gson();
ErrorResponse errorResponse = ErrorResponse.of(HttpStatus.UNAUTHORIZED);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.getWriter().write(gson.toJson(errorResponse, ErrorResponse.class));
}
}
질문노트
🆀
🆀
>
🆀
고민노트
🆀
→
→
🅰 나의 결론 :
'프로젝트 > [더공] 구현기능' 카테고리의 다른 글
4. 회원 탈퇴 & 특정 기간동안 재가입 방지 (0) | 2024.01.05 |
---|---|
3. JwtVerificationFilter (0) | 2024.01.05 |
1. 회원가입 로직 (0) | 2024.01.01 |