본문 바로가기
Spring/Spring Security

spring security1 (spring boot , 타임리프 사용 / form로그인) - 구조 분석 및 로직 코드 작성

by 태옹 2022. 3. 18.

개인적으로 공부한 내용을 정리한 글입니다.

틀린 내용이 있을 수 있습니다. 있다면 댓글로 알려주시면 감사하겠습니다😊


이미지로 확인하는 spring security 구조

출처 : https://tech.junhabaek.net/spring-security-usernamepasswordauthenticationfilter%EC%9D%98-%EB%8D%94-%EA%B9%8A%EC%9D%80-%EC%9D%B4%ED%95%B4-8b5927dbc037

 

  1. AuthenticationFilter로 먼저 요청이 들어옴
  2. 아이디와 비밀번호를 기반으로 UserPasswordAuthenticationToken을 발급함
  3. AuthentivationFilter는 UserPasswordAuthenticationToken을 AuthenticationManager(실제로 인증을 처리하는 AuthenticationProvider를 가지고 있음)한테 전달함
  4. AuthenticationManager는 전달받은 UsernamePasswordToken을 순차적으로 AuthenticaionProvider들에게 전달하여 실제 인증의 과정을 수행
  5. AuthenticationProvider에서 아이디 조회 후, UserDetailsService로부터 아이디 기반으로 DB에서 데이터 조회(UserDetailsService는 인터페이스이므로 상속받아 재정의한 클래스 작성 필요)
  6. 반환한 데이터를 인증처리 후 인증된 토큰을 AuthenticationManager에게 반환
  7. AuthenticationManager는 AuthenticationFilter에게 토큰 전달
  8. AuthenticationFilter는 LoginSuccessHandler로 토큰을 전달하고 SecurityContextHolder에 인증된 토큰을 저장함(그래서 stateless한 상태이지만 로그인을 한 번만 해도 계속 유지가 되는 것)

 

 

이해를 위해 요약하자면..

(요약이라서 이름을 줄이긴 했는데 사실 줄이면 안됨! 왜냐면 수많은 Filter, Manager, Provider가 존재하기 때문)

 

 

Filter에서 http request 먼저 받음

-> 일단 토큰을 발급 (아직 인증 전)

-> 인증 안된 토큰을 Manager한테 전달 (아직 인증 전)

-> ManagerProvider들한테 인증하라고 전달 (아직 인증 전)

-> Provider이 실질적으로 인증 처리 (인증 중)

-> 인증된 토큰을 다시 Manager에게 반환 (인증 완료)

-> ManagerFilter에게 전달 (인증 완료)

-> Filter는 유효한 계정인 경우 SuccessHandler로, 유효하지 않은 계정인 경우 FailureHandler로 전달 (인증 완료)

& 유효한 계정의 경우만 ContextHolder라는 세션에 저장함 

 

 

사실 원리를 먼저 공부하고 만들긴 했는데 어렵다고 소문난 security라서 그런지 정말 이해가..하나도 안됐다.

실습으로 진행하면서 원리를 확인하는 게 더 이해가 빠를 것 같다..!🥲

 


spring boot로 spring security 사용하기

 

📋 진행 순서

1. build.gradle dependency 설정

2. SecurityConfig 클래스 생성하기 (WebSecurityConfigurerAdapter클래스 상속)

3. User 부분 작성하기 (UserDetails인터페이스 구현)

4. 가장 앞단인 customAuthenticationFilter 만들기 (UsernamePasswordAuthenticationFilter클래스 상속)

5. AuthenticationManager 생성 과정 확인하기

6. 실제 인증을 처리하는 CustomAuthenticationProvider 만들기 (AuthenticationProvider인터페이스 구현)

7. 인증 결과를 반환하는 과정 확인하기 

8. 인증 성공을 처리하는 customLoginSuccessHandler 만들기 (SimpleUrlAuthenticationSuccessHandler클래스 상속)

9. 인증 실패를 처리하는 customLoginFailureHandler 만들기 (SimpleUrlAuthenticationFailureHandler클래스 상속)

10. view 페이지 만들기

11. 실행 결과 확인하기

 


 

1. build.gradle dependency 설정

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '2.6.3'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

spring security를 사용하기 위한 설정은 두가지이다. 

이외의 전체 프로젝트에 필요한 dependency설정들은 깃헙에서 확인할 수 있다.

(참고로 두번째에 thymleaf-extras-springsecurity5는 타임리프에서 로그인 정보를 확인할 수 있게하는 설정이다.)

 


2. SecurityConfig 클래스 생성하기 (WebSecurityConfigurerAdapter클래스 상속)

 

SecurityConfig 클래스는 WebSecurityConfigurerAdapter라는 추상 클래스를 상속받은 클래스이다.

오버라이딩한 함수부터 먼저 작성해보면 아래와 같다.

@Configuration
@EnableWebSecurity  //Spring Security를 활성화
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserService userService;

    //WebSecurity는 FilterChainProxy를 생성하는 필터입니다. 다양한 Filter 설정을 적용할 수 있습니다.
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/font/**");
        // Spring Security에서 해당 요청은 인증 대상에서 제외시킵니다. = 모두 접근 가능
    }


    //HttpSecurity를 통해 HTTP 요청에 대한 보안을 설정할 수 있습니다.
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.csrf().disable();

//        모든 작업 끝나면 경로마다 권한 부여해야 함
        http.authorizeRequests()
                .antMatchers("/user").authenticated()
                .antMatchers("/admin").hasAuthority("ADMIN")   //인증 사용자만 허용
//                .antMatchers("/welcome").authenticated()   //인증 사용자만 허용
                .antMatchers("/login").anonymous();    //인증되지 않은 사용자만 허용
//                .antMatchers("/join").permitAll()    //모든 사용자 허용
//                .anyRequest().authenticated();

        http.formLogin()
                .usernameParameter("userid")
                .passwordParameter("password")
                .loginPage("/login")
                .permitAll()
                .and()
                .addFilterBefore(customAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);


        http.logout()
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);   // 세션 날리기
                
//        http.exceptionHandling()
//                .accessDeniedPage("/error");  // 에러 페이지 만들게되면 설정해도 좋을 듯
    }

}

본인의 프로젝트에 맞게 옵션을 추가적으로 붙이면 되는데,

내가 진행하는 프로젝트의 경우 user의 id와 password를 받아서 처리하기 때문에 이 설정대로 진행하겠다.

 

그리고 이 SecurityConfig클래스같은 경우, security에 대한 모든 설정을 가진 클래스이기 때문에 다른 설정들이 추가되어 후반에 다시 등장할 예정이다.

 

해당 프로젝트에서는 /login, /user, /admin 경로에 연결할 세 개의 view를 만들어보겠다.

로그인하지 않은 경우 login.html페이지만 확인할 수 있고, 로그인한 후 어드민 권한을 가진 유저만 /admin에 접근할 수 있도록 설정한다.

 

addFilterBefore메소드를 통해 UsernamePasswordAuthenticationFilter를 사용하는 설정에서 우리가 직접 커스텀한 CustomAuthenticationFilter를 사용하는 설정으로 변경해준다.

 

 

여기서 antMatchers("/경로").hasRole()을 사용하거나 hasAuthority()를 사용하는 두 가지 경우가 있는데, (access()도 있지만 일단 생략)있는데 두 메소드의 차이는 "ROLE_"문자열의 차이이다. 만약 hasRole()를 사용하고 싶은 경우, 아래의 join.html코드를 value="ROLE_ADMIN"처럼 ROLE_문자열을 앞에 붙여주면 정상적으로 작동한다.

<input type="radio" name="authRole" value="ROLE_ADMIN,ROLE_USER"> admin
<input type="radio" name="authRole" value="ROLE_USER" checked="checked"> user <br>

 

아래의 코드처럼 value에 USER_문자열을 제외하면 hasAuthority()를 사용해야 원하는 동작을 수행하게 된다. 

<input type="radio" name="authRole" value="ADMIN,USER"> admin
<input type="radio" name="authRole" value="USER" checked="checked"> user <br>

spring security에서 hasRole메소드를 처리할 때 자동으로 "ROLE_"문자열을 추가하기 때문에 아래의 코드처럼 작성했을 때 작동하게 된다.

.antMatchers("/admin").hasRole("ADMIN")   //인증 사용자만 허용

3. User 부분 작성하기 (UserDetails인터페이스 구현)

User에 관련된 부분을 만드는 단계이다.

User(VO), UserDto, UserRepository, UserService를 만들어보자.

 

먼저 User(VO)를 만든다.

User클래스는 UsernamePasswordToken을 생성할 때 사용되는 UserDetails 인터페이스를 구현한 클래스이다.

 

UserDetails 인터페이스를 구현하기 전에는 아래와 같이 User의 칼럼 정보를 정의하고,

@Entity
@Getter
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column
    private Long code;

    @Column
    private String userid;

    @Column
    private String password;

    protected User() {
    }

    @Builder
    public User(Long code, String userid,  String password) {
        this.code = code;
        this.userid = userid;
        this.password = password;
    }
}

UserDetails 인터페이스를 구현하면 아래와 같이 코드를 작성할 수 있다.

(이 부분은 https://shinsunyoung.tistory.com/78 님의 글에서 자세한 설명을 확인할 수 있다.)

 

간략히 말하자면, 사용자는 권한 정보를 컬렉션 형태로 가지고 있어서 ','로 구분된 복수 개의 권한(단수 개일 수도 있음)을 분리해서 roles에 저장한다. 어드민의 경우에는 일반사용자와 어드민의 권한을 동시에 가질 수 있기 때문에 /user와 /admin에 모두 접근이 가능하게 된다.

// 사용자의 권한을 콜렉션 형태로 반환
    // 단, 클래스 자료형은 GrantedAuthority를 구현해야함
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> roles = new HashSet<>();
        for (String role : authRole.split(",")) {
            roles.add(new SimpleGrantedAuthority(role));
        }
        return roles;
    }

    @Override
    public String getUsername() {
        return getUserid();
    }

    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        // 만료되었는지 확인하는 로직
        return true; // true -> 만료되지 않았음
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        // 계정 잠금되었는지 확인하는 로직
        return true; // true -> 잠금되지 않았음
    }

    // 패스워드의 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        // 패스워드가 만료되었는지 확인하는 로직
        return true; // true -> 만료되지 않았음
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        // 계정이 사용 가능한지 확인하는 로직
        return true; // true -> 사용 가능
    }

 

이어서 UserDto도 작성해준다.

@Getter
@Setter
@NoArgsConstructor
public class UserDto {
    private Long code;
    private String userid;
    private String password;
    private String authRole;

    @Builder
    public UserDto(Long code, String userid, String password, String authRole) {
        this.code = code;
        this.userid = userid;
        this.password = password;
        this.authRole = authRole;
    }

    public User toEntity(){
        return User.builder()
                .code(code)
                .userid(userid)
                .password(password)
                .authRole(authRole)
                .build();
    }
}

 

UserRepository는 Spring data JPA를 이용하여 간편하게 작성한다.

public interface UserRepository extends JpaRepository<User , Long> {
    Optional<User> findByUserid(String userid);
}

 

그 다음으로는 UserDetailsService 인터페이스를 구현한 UserService클래스를 작성한다.

UserDetailsService 인터페이스는 위와 같이 UserDetails타입을 반환하는 loadUserByUsername이라는 메소드를 가지고 있다. 인터페이스이기 때문에 loadUserByUsername메소드는 구현이 필요하다.

 

UserService클래스는 다음과 같이 작성했다.

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    @Transactional
    public String saveUser(UserDto user) {
        //패스워드 인코딩
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        user.setPassword(encoder.encode(user.getPassword()));

        return userRepository.save(user.toEntity()).getUserid();

    }

    //아이디로 유저 검색
    @Override  //반환값 다운캐스팅 (UserDetails->User)
    public User loadUserByUsername(String userid) throws UsernameNotFoundException {
        return userRepository.findByUserid(userid).orElseThrow(() -> new UsernameNotFoundException(userid));
    }
}

여기서 로그인은 아이디로 유저를 검색하는 기능인 loadUserByUsername()을 사용하면 되고, 만약 회원가입까지 구현이 필요하다면 saveUser()를 생성하여 유저가 입력한 패스워드를 암호화하여 DB에 저장할 수 있도록 하는 함수를 추가로 작성하면 된다. (참고로 BCryptPasswordEncoder는 BCrypt 해싱 함수(BCrypt hashing function)를 사용해서 비밀번호를 인코딩한다.= 암호화 )

 

loadUserByUsername의 경우 기존 함수의 반환값이 UserDetails이지만 User로 다운캐스팅하여 사용한다.


 

4. customAuthenticationFilter 만들기 (UsernamePasswordAuthenticationFilter클래스 상속)

 

스프링 시큐리티는 SpringSecurityFilterChain이라는 필터를 등록하는데, 내부 구조를 보면 여러 개의 필터들이 체인으로 묶여서 순서대로 인증 및 인가 과정을 진행한다.

필터와 체인에 대한 자세한 내용은 아래 블로그를 참고해보면 좋을 것 같다.

https://flyburi.com/584

 

[SpringSecurity] Authentication(인증) 관련 클래스와 처리

Spring Security에 대해 큰 흐름은 알지만, 처음부터 적용하는게 아니면 어떤 권한을 주고 권한 체크하는 로직만 추가하거나 수정하며 생각없이 쓰게 되는데, 어떤 흐름으로 되는지 전보다 좀 더 살

flyburi.com

 

스프링 시큐리티에는 다양한 필터들이 내장되어있는데, 필터들은 각 쓰임에 맞게 적절한 필터를 골라 사용하면 된다.

이 클래스는 UsernamePasswordAuthenticationFilter 클래스를 상속받은 클래스이다.

UsernamePasswordAuthenticationFilter는 Spring Security에서 폼 형식의 로그인 방식으로 작동할 때 사용할 수 있는 Filter인데, SecurityConfig에 http.formLogin()라고 작성하면 자동으로 해당 필터를 사용하도록 등록된다.

 

위에서 설명한 것처럼 http request가 들어오면 UsernamePasswordAuthenticationFilter가 먼저 요청을 받게 되는데 사실 이 클래스는 커스텀하지 않아도 상관없다. 

UsernamePasswordAuthenticationFilter 클래스를 확인하면 username과 password를 기본값으로 설정해서 받고있다.

내가 진행하는 프로젝트의 경우 username이라는 컬럼이 이미 존재해서 내가 개발중에 헷갈릴까봐 그냥 userid로 설정을 변경하기 위해 커스텀 클래스를 만들어 진행했다.

username이 아닌 다른 의미를 가진 칼럼을 사용해야 한다면 가독성을 위해 커스텀하여 사용하는 것이 좋을 것 같다.

 

먼저 SecurityConfig.java에 다음 코드를 추가해준다.

 //1 - 가장 첫 요청이 Filter를 통해 들어옴
    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        //3 - 발급받은 토큰을 Authentication Manager한테 전달
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        customAuthenticationFilter.setFilterProcessesUrl("/loginProcess");
//        customAuthenticationFilter.setAuthenticationSuccessHandler(customLoginSuccessHandler());
//        customAuthenticationFilter.setAuthenticationFailureHandler(customLoginFailureHandler());
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

중간에 주석처리한 Handler들은 순서대로 진행하기 위해 마지막에 추가해주도록 한다.

CustomAuthenticationFilter객체를 생성하고, 해당 객체에 FilterProcessUrl이라는 것을 세팅한다.

이 url은 Controller에 정의하거나, view를 생성하여 연결되는 url이 아니라 로그인 과정을 진행하는 사이에 임의로 설정하는 url이다. (그러므로 login.html페이지 혹은 다른 용도의 url을 넣지 않도록 주의하자. 원하는 대로 동작하지 않을 위험이 있다.)

 

 

우리가 지금 봐야하는 부분은 new CustomAuthenticationFilter()이다.

매개변수로 authenticationManager()을 호출하고 있는데, 이 함수는 AuthenticationManager 객체를 반환해주는 것을 확인할 수 있다. 필터 생성 시에 매니저를 세팅하는 것이다. (그러나 이 매니저는 아직 구현되지 않은 인터페이스이다.)

 

지금부터 진행할 단계를 정말 간단히 요약해본다면 Filter는 http요청을 가장 먼저 받고, 토큰을 발급받은 뒤 매니저에게 토큰을 전달한다. 이 내용을 떠올리며 진행해보자.

 

UsernamePasswordAuthenticationFilter를 구현한 CustomAuthenticationFilter클래스는 다음와 같다.

public class CustomAuthenticationFilter  extends UsernamePasswordAuthenticationFilter {
    public CustomAuthenticationFilter(AuthenticationManager authenticationManager) {
        super.setAuthenticationManager(authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if(request.getParameter("userid")!=null && request.getParameter("password")!=null){
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(request.getParameter("userid"), request.getParameter("password"));
            setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
        return null;
    }
}

 

여기서 생성된 토큰은 아직 인증되지 않은 상태이다. UsernamePassword의 코드를 확인해보면 두 가지의 생성자를 가지고 있는데, Filter에서 만들어지는 토큰의 경우에는 매개변수를 두 개만 받는 토큰이 발급된다. 

매개변수를 세 개 받는 아래의 생성자의 경우에는 인증이 완료된 토큰을 발급한다. (setAuthenticated=true)

이 토큰은 실제 Provider에서 인증을 성공한 경우에 발급된다. (그니까 아직은 인증이 안된 상태의 토큰)

 

AbstractAuthenticationProcessingFilter클래스의 doFilter메소드는 다음과 같은 코드로 작성되어있다.

위에 첨부한 spring security구조 순서를 보면, 여러 개의 Filter들이 체인으로 엮여 doFilter메소드를 통해 인증을 확인하는 과정을 거친다. doFilter에서는 attemptAuthentication를 호출해서 http request와 response를 매개변수로 보내는 과정을 확인할 수 있다.

 

attemptAuthentication메소드에서는 사용자가 폼에 입력한 userid와 password값을 기반으로 UsernamePasswordAuthenticationToken을 생성하여 매니저에게 전달하는 과정을 확인할 수 있다.

 


5. AuthenticationManager 생성 과정 확인하기

 

Filter에서 토큰을 생성해서 매니저(AuthenticationManager)에게 세팅해주긴 했지만 이 매니저는 사실 인터페이스다.

 

스프링 시큐리티에서 사용하는 가장 최상단의 빌더는 SecurityBuilder이다. 

SecurityBuilder는 AuthenticationManager를 작성할 때 사용하며, 메모리 인증, LDAP 인증, JDBC 기반 인증, User Details Service 추가 및 AuthenticationProvider를 추가할 때 편리하게 사용할 수 있다고 한다.

 

우리는 AuthenticationManager를 구현하기 위해 SecurityBuilder를 구현한 AbstractSecurityBuilder...를 상속받은 AbstractConfiguredSecurityBuilder...를 상속받은 AuthenticationManagerBuilder를 사용할 것이다!(복잡복잡)

 

 

SecurityConfig.java

//2. 반환한 데이터를 인증처리 후 인증된 토큰을 AuthenticationManager에게 반환
@Override   // AuthenticationManagerBuilder : 인증을 수행하기 위해 ProviderManager를 생성
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
    authenticationManagerBuilder.authenticationProvider(customAuthenticationProvider());
}

AuthenticationManagerBuilder의 authenticationProvider메소드를 통해 우리가 새롭게 커스텀할 customAuthenticationProvider를 세팅해주도록 한다.

 

AuthenticationManagerBuilder가 빌드를 수행할 때 performBuild메소드가 실행된다.

performBuild메소드에는 ProviderManager객체를 생성하고 최종적으로 해당 객체를 반환하는 것을 확인할 수 있다.  (위에서 언급된 AuthenticationManager랑 ProviderManager의 차이를 확인하자.)

ProviderManager 생성자를 확인해보면 다음과 같은 코드로 작성되어있다.

매개변수를 보면 AuthenticationProvider타입의 리스트와 AuthenticationManager객체를 입력받는 것을 확인할 수 있다. 

결국 ProviderManager라는 애가 AuthenticationManager와 AuthenticationProvider리스트 객체를 가지고 있는 것이다.

 

이후 단계에서는 리스트로 저장된 AuthenticationProvider들을 For문을 사용해서 authenticate처리를 하게된다.

(추가하는 코드 아님!) ProviderManager의 authenticate메소드에서 AuthenticationProvider리스트를 반복문을 통해 인증 처리를 수행하고 있는 것을 확인할 수 있다.

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   Class<? extends Authentication> toTest = authentication.getClass();
   AuthenticationException lastException = null;
   AuthenticationException parentException = null;
   Authentication result = null;
   Authentication parentResult = null;
   int currentPosition = 0;
   int size = this.providers.size();
   for (AuthenticationProvider provider : getProviders()) {
      if (!provider.supports(toTest)) {
         continue;
      }
      if (logger.isTraceEnabled()) {
         logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
               provider.getClass().getSimpleName(), ++currentPosition, size));
      }
      try {
         result = provider.authenticate(authentication);
         if (result != null) {
            copyDetails(authentication, result);
            break;
         }
        // ....생략
      }
    }
}

 


6. 실제 인증을 처리하는 CustomAuthenticationProvider 만들기

(AuthenticationProvider인터페이스 구현)

 

AuthenticationProvider는 실제 인증을 처리하는 아주아주 핵심적인 부분이다.

AuthenticationProvider 도 인터페이스이기 때문에 인증 처리를 구현한 CustomAuthenticationProvider 클래스를 만들어주어야 한다.

먼저 SecurityConfig에 다음 코드를 추가한다.

 

SecurityConfig.java

@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
    return new BCryptPasswordEncoder();
}


@Bean//CustomAuthenticationProvider : 인증 처리 핵심 로직
public CustomAuthenticationProvider customAuthenticationProvider() {
    return new CustomAuthenticationProvider(userService, bCryptPasswordEncoder());
}

bCryptPasswordEncoder메소드는 유저를 save할 때 사용한 암호화 방식을 다시 복호화하기 위해 매개변수로 입력한다.

인증 처리 핵심 로직을 작성할 CustomAuthenticationProvider클래스를 생성하자.

 

 

CustomAuthenticationProvider.java

@RequiredArgsConstructor
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder passwordEncoder;

    @Override //실제 인증에 대한 부분★★★★★
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;

        // AuthenticaionFilter에서 생성된 토큰으로부터 아이디와 비밀번호를 조회함
        String userid = token.getName();
        String password = (String) token.getCredentials();
        User user = (User) userDetailsService.loadUserByUsername(userid);

        if(!passwordEncoder.matches(password, user.getPassword())){
            throw new BadCredentialsException(user.getUserid() + " Invalid password");
        }
        return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

 

 

위에서 등장했던 AuthenticationProvider인터페이스를 구현하였기 때문에 authenticate메소드를 오버라이딩해서 인증 로직을 작성한다. 

UsernamePasswordAuthenticationToken에서는 name을 id값으로, credentials을 pw값으로 사용하기 때문에 토큰에서 getName, getCredentials메소드를 통해 사용자 id와 pw를 확인하고 pw값은 복호화한 후 실제 값과 match시킨다.

pw값이 일치하지 않는 경우 Exception을 띄우고, 일치하는 경우 토큰을 다시 생성하는데, 이 때는 위에서 언급했듯이 매개변수 세 개 짜리의 생성자가 호출되기 때문에 setAuthenticated=true인 토큰이 재발급된다.

 

 


7. 인증 결과를 반환하는 과정 확인하기 

위에 첨부한 그림을 다시 가지고와봤다. 사진을 보면서 다시 구조를 파악해보자.

 

지금까지의 내용은 Filter를 거쳐 Manager를 통해 생성된 Provider들이 인증 처리를 수행하는 것 까지 확인해봤다.

그럼 다시 반대로 인증을 완료하고 새로운 토큰이 반환되면 Manager가 확인하고 이걸 Filter에게 전달하는 순서로, 즉 거꾸로 진행이 된다. Filter는 인증 결과를 통해 success 혹은 failure핸들러를 호출한다.

 


8. 인증 성공을 처리하는 customLoginSuccessHandler 만들기 

(SimpleUrlAuthenticationSuccessHandler클래스 상속)

 

SecurityConfig.java

 //1 - 가장 첫 요청이 Filter를 통해 들어옴
    @Bean
    public CustomAuthenticationFilter customAuthenticationFilter() throws Exception {
        //3 - 발급받은 토큰을 Authentication Manager한테 전달
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManager());
        customAuthenticationFilter.setFilterProcessesUrl("/loginProcess");
        customAuthenticationFilter.setAuthenticationSuccessHandler(customLoginSuccessHandler()); //여기
        customAuthenticationFilter.setAuthenticationFailureHandler(customLoginFailureHandler()); //여기
        customAuthenticationFilter.afterPropertiesSet();
        return customAuthenticationFilter;
    }

아까 추가하지 않았던 두 줄의 주석을 풀어주자. 지금을 위해 남겨두었던 코드이다.

인증 성공 혹은 실패를 처리하는 핸들러 메소드를 세팅해준다.

아래에는 두 핸들러를 생성하는 메소드를 추가한다.

@Bean
public CustomLoginSuccessHandler customLoginSuccessHandler() {
    return new CustomLoginSuccessHandler();
}

@Bean
public CustomLoginFailureHandler customLoginFailureHandler() {
    return new CustomLoginFailureHandler();
}

 

SimpleUrlAuthenticationSuccessHandler.java

//AuthenticationProvider를 통해 인증이 성공될 경우 처리
@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException {
        //나중에 사용자의 정보를 꺼낼 경우에도 SecurityContextHolder의 context에서 조회 가능함
        SecurityContextHolder.getContext().setAuthentication(authentication);
        response.sendRedirect("/user");
    }
}

인증 성공을 처리하는 successHandler에서는 인증 결과를 context에 넣고 redirect해준다.

context안에 저장된 인증 객체는 이후에도 인증 과정을 거쳐야 할 때 Filter가 context 안에 원하는 정보가 있으면 찾아서 사용할 수 있다. 세션에도 현재 인증된 상태로 저장된다.

 


9. 인증 실패를 처리하는 customLoginFailureHandler 만들기 

(SimpleUrlAuthenticationFailureHandler클래스 상속)

 

SimpleUrlAuthenticationFailureHandler.java

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String msg = "Invalid userid or Password";

        // exception 관련 메세지 처리
        if (exception instanceof DisabledException) {
            msg = "DisabledException account";
        } else if(exception instanceof CredentialsExpiredException) {
            msg = "CredentialsExpiredException account";
        } else if(exception instanceof BadCredentialsException) {
            msg = "BadCredentialsException account";
        }

        setDefaultFailureUrl("/login?error=true&exception=" + msg);

        super.onAuthenticationFailure(request, response, exception);
    }
}

이 경우는 인증을 실패했을 때 msg를 반환한다. 이 msg를 로그인 화면에 출력할 수 있다.

 

 

로직을 수행하는 코드는 여기까지이다!

다음 게시물에서는 view를 적용해서 실제로 폼 로그인을 동작시키는 코드를 소개해보겠다.🤓

 


참고한 블로그 글

https://mangkyu.tistory.com/76

 

[SpringBoot] Spring Security란?

대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능

mangkyu.tistory.com

Spring Boot | 로그인 구현하기 (Spring Security)

 

Spring Boot | 로그인 구현하기 (Spring Security)

[스프링 부트 (Spring Boot)/게시판 만들기] - 1 | 스프링 부트 프로젝트 만들기 위의 과정을 통해 생성된 프로젝트입니다. 구성환경 SpringBoot, Gradle, Thymeleaf, Jpa(JPQL), Jar, MariaDB 스프링 시큐리티(Sp..

kitty-geno.tistory.com

https://tech.junhabaek.net/spring-security-usernamepasswordauthenticationfilter%EC%9D%98-%EB%8D%94-%EA%B9%8A%EC%9D%80-%EC%9D%B4%ED%95%B4-8b5927dbc037

 

Spring Security UsernamePasswordAuthenticationFilter의 더 깊은 이해

FormLogin시 사용되는 UsernamePasswordAuthenticationFilter가 어떤 방식으로 동작하는지 그 동작방식으로 인해 왜 Jwt 인증방식에 그대로 사용될 수 없는지 그러면 어떻게 사용해야? 다른 해결책은? 같은 내

tech.junhabaek.net

댓글