회원가입로직은 저번 글에서 이미 다룸
1. 사용자의 요청이 필터를 타고 ID와 비밀번호를 가지고 들어오면 UserPasswordAuthenticationFilter
가 username
과 password
를 꺼내서 로그인을 진행하기 위해서 AuthenticationManager
에게 넘겨준다.
AuthenticationManager
은 DB로부터 회원정보를 가져와서 검증을 진행한다.
검증이 완료되면 sucessfullAutn
이 동작하는 데, JWT를 만들어서 사용자에게 응답을 준다.
UserPasswordAuthenticationFilter
을 직접 만들어야한다.스프링 시큐리티는 클라이언트의 요청이 여러개의 필터를 거쳐
최종적으로는 DispatcherServlet(Controller)으로 향한다.
향하는 중 중간 필터에서 요청을 가로챈 후 검증(인증/인가)을 진행한다.
스프링 부트 어플리케이션는 TomCat
이라는 서블릿 컨테이너 위에서 동작한다.
요청이 왔을 경우 서블릿 필터를 다 통과한 이유 스프링 부트에 전달된다.
필터가 요청을 가로채서 회원정보를 검증하게 된다.
모든 요청을 시큐리티 필터로 전달한다.(가로채서 보낸다.)
가로챈 요청은 SecurityFilterChain
으로 전달해 적절하게 거부,리디렉션, 서블릿을 요청 전달을 진행한다.
UsernamePasswordAuthenticationFilter
따라서 강제로 우리가 커스텀해서 진행해야한다.
UserPasswordAuthenticationFilter
LoginFilter
LoginFilter
는 UserPasswordAuthenticationFilter
을 상속받아 구현한 jwt필터이다.username
과 password
를 꺼내서 로그인을 진행하기 위해서 AuthenticationManager
에게 넘겨준다.package com.jwt.jwtstudy_youtube.jwt;
import jakarta.servlet.FilterChain;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
public class LoginFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
public LoginFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override //필수 -> ⭐ 로그인 전달 메서드
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
//클라이언트 요청에서 username, password 추출
String username = obtainUsername(request);
String password = obtainPassword(request);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(username, password, null);
//인증진행 -> AuthenticationManager에게 DTO라는 바구니에 담아서 유효성 검사 맡기기
return authenticationManager.authenticate(authToken);
}
//로그인 성공시 실행 메소드
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
//(여기서 JWT를 발급하면 됨)
System.out.print("성공==============================");
}
//로그인 실패시 실행 메소드
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
System.out.println("실패==============================");
}
}
package com.jwt.jwtstudy_youtube.service;
import com.jwt.jwtstudy_youtube.dto.CustomUserDetails;
import com.jwt.jwtstudy_youtube.entity.UserEntity;
import com.jwt.jwtstudy_youtube.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override //필수
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//구현해야할 동작
// 1. 사용자 유효한지 검사
// -> 유효한 경우 UserDetails 반환
UserEntity userData = userRepository.findByUsername(username);
if (userData != null) {
return new CustomUserDetails(userData);
}
return null;
}
}
username
을 기반으로 사용자 정보를 로드하고 이를 UserDetail
로 반환해 리턴한다.
UserDetailsService
의 loadUserByUsername
의 반환값을 세션에 저장할 것이다. (로그인 완료)결국 우리 프로젝트에서는, 유효성 검사 역할을 수행한다.
package com.jwt.jwtstudy_youtube.dto;
import com.jwt.jwtstudy_youtube.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class CustomUserDetails implements UserDetails {
private final UserEntity userEntity;
public CustomUserDetails(UserEntity userEntity) {
this.userEntity = userEntity;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> collection = new ArrayList<>();
collection.add(new GrantedAuthority() {
@Override
public String getAuthority() {
return userEntity.getRole();
}
});
return collection;
}
@Override
public String getPassword() {
return userEntity.getPassword();
}
@Override
public String getUsername() {
return userEntity.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
getAuthorities() 메서드
: 인증된 사용자가 가진 권한을 반환하는 메서드다. 보통 어떤 URL에 대한 접근을 허용 여부 결정에 쓰인다. 따라서 Spring Security는 이 메서드를 사용하여 사용자가 특정 리소스, 기능에 접근 권한이 있는지 확인할 수 있다. 반환값 : Collection<? extends GrantedAuthority> - 사용자가 가진 모든 권한 ("ROLE_USER", "ROLE_ADMIN"...) 모두를 반환한다.
package com.jwt.jwtstudy_youtube.config;
import com.jwt.jwtstudy_youtube.jwt.LoginFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutFilter;
@Configuration
@EnableWebSecurity //security를 위한 것이라고 알려주는 기능
public class SecurityConfig {
private final AuthenticationConfiguration authenticationConfiguration;
public SecurityConfig(AuthenticationConfiguration authenticationConfiguration) {
this.authenticationConfiguration = authenticationConfiguration;
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
~~~ 이 부분은 다른 글 참조 ~~~
//jwt로그인을 위한 필터등록
http.addFilterAt(new LoginFilter(authenticationManager(authenticationConfiguration)), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
잘 보면 이 글에서 따로 컨트롤러를 두지 않은걸 볼 수 있다.
또한 어떤 코드에서도 "/login"
경로를 지정하지 않은 걸 확인할 수 있다.
그러면 지정 부분은 어디있을까..?!!?
바로 UsernamePasswordAuthenticationFilter에 있다 ^__^
private static final AntPathRequestMatcher
DEFAULT_ANT_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher("/login","POST");
따라서 JWT 로그인시 "/login"
주소로 POST
방식 요청하지 않으면 원하는 대로 동작하지 않으니까 주의하자! 🩷👍👍
새로 알게 되는 부분이 있으면 뿌듯하고 실력이 향상된다고...(아닌가요?ㅜ) 생각돼서
공부할 재미가 난다 🫶🫶