모르지 않다는 것은 아는것과 다르다.

Spring Security

UsernamePasswordAuthenticationFilter 커스텀 하기

채마스 2022. 2. 28. 20:38

개요

  • UsernamePasswordAuthenticationFilter는 Id와 password를 사용하는 form 기반 인증을 처리하는 필터이다.
  • UsernamPasswordAuthenticationFilter 를 상속받아서 Filter를 구성하는 것을 목표로 한다.
  • UsernamPasswordAuthenticationFilter 의 메소드를 재정의 함으로서 커스텀한 필터를 구성할 수 있다.





Login Form 인증 절차

  • 먼저 AntPathRequestMatcher 를 통해서 요청 정보가 매칭되는지 확인한다.
    • 정보가 맞지않으면 다음 필터로 넘어간다.
  • 그 다음 Authentication 객체를 생성한다. (이 부분이 가장 큰 역할이다.)
  • 그 다음 AuthenticationManager 가 AuthenticationProvider 에게 위임해서 인증을 처리한다.
    • 만약 인증이 정상적으로 이루어지면 최종적인 Authentication 객체를 넘겨준다. -> User 와 Authorities 를 가지고 있다.
  • 그 다음 Authentication 객체를 SecurityContext 에 저장한다.





Logout 절차

  • 먼저 AntPathRequestMatcher 를 통해서 요청 정보가 매칭되는지 확인한다.
    • 정보가 맞지않으면 다음 필터로 넘어간다.
  • 그 다음 SecurityContext 에서 Authentication 객채를 가져온다.
  • 그 다음 SecurityContextLogoutHandler 에서 로그아웃을 처리한다.
    • 세션 무효화, 쿠키 삭제, SecurityContext clear
  • 마지막으로 SimpleUrlLogoutSuccessHandler 가 로그아웃 후에 페이지를 지정해준다.





로그인 처리와 로그아웃 처리

  • 로그인
protected void configure(HttpSecurity http) throws Exception {
     http.formLogin()
                .loginPage("/login.html")                   // 사용자 정의 로그인 페이지
                .defaultSuccessUrl("/home")                    // 로그인 성공 후 이동 페이지
                .failureUrl("/login.html?error=true")        // 로그인 실패 후 이동 페이지
                .usernameParameter("username")                // 아이디 파라미터명 설정
                .passwordParameter("password")                // 패스워드 파라미터명 설정
                .loginProcessingUrl("/login")                // 로그인 Form Action Url
                .successHandler(loginSuccessHandler())        // 로그인 성공 후 핸들러
                .failureHandler(loginFailureHandler())        // 로그인 실패 후 핸들러
}
  • 로그아웃
protected void configure(HttpSecurity http) throws Exception {
     http.logout()                                               // 로그아웃 처리
                .logoutUrl("/logout")                           // 로그아웃 처리 URL
                .logoutSuccessUrl("/login")                       // 로그아웃 성공 후 이동페이지
                .deleteCookies("JSESSIONID", "remember-me")    // 로그아웃 후 쿠키 삭제
                .addLogoutHandler(logoutHandler())               // 로그아웃 핸들러
                .logoutSuccessHandler(logoutSuccessHandler())  // 로그아웃 성공 후 핸들러
}






코드 구현

  • RequestLogin.java 클래스 구현
public class RequestLogin {
    @NotNull(message = "Email cannot be null")
    @Size(min = 2, message = "Email not be less than two characters")
    @Email
    private String email;

    @NotNull(message = "Password cannot be null")
    @Size(min = 2, message = "Password not be less than two characters")
    private String password;

}
  • 위와 같이 email, password 를 필드로 갖는 클래스 구현
  • AuthenticationFilter.java 클래스 구현
public class AuthenticationFilter extends UsernamePasswordAuthenticationFiler {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            RequestLogin creds = new ObjectMapper().readValue(request.getInputStream(), RequestLogin.class);
        }

        return getAuthenticationManager().authenticate(
            new UsernamePasswordAuthenticationToken(
                creds.getEmail(),
                creds.getPassword(),
                new ArrayList<>()
            )
        );
    } catch (IOException e){
        throw new RuntimeException(e);
    }

    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication, authResult) throws IOException, ServletException{

        String userName = ((User)authResult.getPrincipal()).getUsername();
        UserDto userDetails = userService.getUserDetailsByEmail(userName);

        String token = Jwts.builder()
                .setSubject(userDetails.getUserId())
                .setExpiration(new Date(System.currentTimeMillis()
                        + Long.parseLong(env.getProperty("token.expiration_time"))))
                .signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret"))
                .compact();

        response.addHeader("token", token);
        response.addHeader("userId", userDetails.getuserId());
    }
}
  • 먼저 요청정보를 처리하는 attemptAuthentication 메소드를 재정의한다.
  • Post 로 넘어오는 값은 Request Param 으로 받을 수 없기 때문에 getInputStream() 으로 받고, ObjectMapper() 를 이용해서 RequestLogin.class 타입으로 변환시켜준다.
  • UsernamePasswordAuthenticationToken 를 생성해서 AuthenticationManager 에게 인증을 요청한다.
  • successfulAuthentication 메소드는 로그인이 끝난 후 토큰을 만드는 작업을 진행 한다.
  • setSubject() 를 통해서 어떠한 내용을 가지고 토큰을 만들건지 설정할 수 있다.
  • signWith(SignatureAlgorithm.HS512, env.getProperty("token.secret")) 에서 env.getProperty("token.secret") 값을 가지고 HS512 암호화 알고리즘을 통해서 암호화할 수 있다.
  • WebSecurity.java
@Configuration
@EnableWebSecurity
public class WevSecurity extends WebSecurityConfigurerAdapter{
    private UserService userService;
    private BCryptPasswordEncoder bCryptPasswordEncoder;
    private Environment env;

    @Autowired
    public WebSecurity(UserService userService, Environment env, BCryptPasswordEncoder bCryptPasswordEncoder){
        this.userService = userService;
        this.env = env;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests().antMatchers("/**") // 모든 요청을 권한검증을 함
                .hasIpAddress("192.168.0.8") // IP 변경
                .and()
                .addFilter(getAuthenticationFilter()); // 필터 추가

        http.headers().frameOptions().disable();
    }

    private AuthenticationFilter getAuthenticationFilter() throws Exception {
        AuthenticationFilter authenticationFilter = new AuthenticationFilter(
            new AuthenticationFilter(authenticationManager(), userService, env);
        );
        //authenticationFilter.setAuthenticationManager(authenticationManager());


        return authenticationFilter;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder);
    }
}
  • configure(HttpSecurity http) 는 권한에 관련된 부분이고, configure(AuthenticationManagerBuilder auth) 는 인증에 관련된 부분이다.
  • 당연하겠지만, 인증이되어야 권한 부여가 가능하다.
  • addFilter(getAuthenticationFilter()) 를 통해서 필터를 추가해 줄 수 있다.
  • authenticationFilter.setAuthenticationManager(authenticationManager()) 를 통해서 필터에 AuthenticationManager 를 지정해준다. -> AuthenticationManager 가 인증을 처리한다. -> new AuthenticationFilter(authenticationManager(), userService, env); 생성자로 대체
  • yml 파일 에서 정보를 가져오기 위해서는 Environment 객체가 필요하다.
  • auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder); 에서 userDetailsService 의 매개변수로 들어오는 userService 는 UserDetailsService 를 상속받는 클래스여야만 한다.
  • UserService.java
public interface UserService extends UserDetailsService {
    UserDto createUser(UserDto userDto);
    UserDto getUserByUserId(String userId);
    Iterable<UserEntity> getUserByAll();
    UserDto getUserDetailsByEmail(String userName);
}
  • 위와 같이 UserDetailsService 를 상속받아서 UserService 를 구현했다.
    ```java
  • UserServiceImpl implements UserService {
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {}@Override
    public UserDto getUserDetailsByEmail(String email){}
    }
  • UserEntity userEntity = userRepository.findByEmail(email); if(userEntity == null) throw new UsernameNotFoundException(email); UserDto userDto = new ModelMapper().map(userEntity, UserDto.class); return userDto;
  • //.. @Override 이하 생략
  • UserEntity userEntity = userRepository.findByEmail(username); if(username == null){ throw new UsernameNotFoundException(username); } return new User(userEntity.getEmail(), userEntity.getEnoryptedPwd(), true, true, true, new ArrayList<>());




REFERENCES

  • 이도원님의 MSA 강의
  • 정수원님의 스프링 시큐리티