Session VS JWT

60jong·2022년 8월 10일
0

Spring

목록 보기
2/9

Todoary 프로젝트에서는 로그인 및 상태유지를 위해 Spring Security + JWT를 사용했다.
그런데 막상 우리가 왜 Session + Cookie를 사용하지 않고, JWT를 사용했는가에 대한 의문이 생겼다.

Session VS JWT

우리가 Session 대신 JWT를 이용한 이유는

  • 서버에서 Session에 대한 정보를 통제할 필요가 없다.

  • 다중 서버를 운영할 시에, 세션 A는 서버 A에만 존재하므로 서버 A가 장애가 발생하면 서버 B에는 세션 A를 새로 할당해야한다. 따라서 완전한 stateless를 위해 JWT를 사용했다.

그러나 우리의 서버는 단일 서버이기에 어느 방식을 사용하든 문제가 될 것은 없어보였다.

그래서 Sesson + Cookie를 이용한 로그인 방식을 구현해보았다.

그에 앞서 Spring Security의 폼 로그인 방식의 과정을 다시 정리해보았다.

Form Login 과정

  1. Form Login 요청 (/login) <- loginProcessingUrl("/login")
  1. AbstractAuthenticationProcessingFilter의 구현체인
    UsernamePasswordAuthenticationFilter (이하 Ufilter)가 request에서 username과 password를 받아서 UsenamePasswordAuthenticationToken (이하 Utoken)을 생성
  1. 이후 UfilterAuthenticationManager의 구현체 (대게 이미 구현돼있는ProviderManger가 사용됨)에게 Utoken을 전달한다.
  1. ProviderManagerAuthenticationProvider(이하 Aprovider)의 여러 구현체들과 협력해서 인증을 진행하는데, 전달 받은 Utoken을 인증할 수 있는 Aprovider를 찾아서 (대부분 DB에서 user를 찾으므로 DaoAuthenticationProvider가 선택됨 + 내부의 UserDetailsServiceloadbyUsername()을 통해 email에 맞는 유저를 검색) Utoken을 넘겨주면서 DaoAprovider가 실제 인증을 하는 함수인 authenticate()을 통해 인증을 진행함.
  1. 로그인에 성공하면 UserDetails 객체를 Authentication 객체에 담아 Security Context에 저장한다.

(이 과정을 찾아보고 내부 클래스 뒤져보는데 8시간 넘게 걸렸다...)

Session

-폼 로그인-

JWT를 이용할 때에는 WebSecurityConfigurerAdapter를 상속받아서 설정을 했는데, 이번부터는 권장되는 방식인 SecurityFilterChain객체를 빈으로 등록함으로써 설정을 하는 방식을 사용했다. 이전과 크게 다를 게 없었다.
참고.
https://spring.io/blog/2022/02/21/spring-security-without-the-websecurityconfigureradapter

SecurityConfig

SecurityConfig.java

@Configuration
public class SecurityConfig {
    private final UserDetailsService userDetailsService;
    private final AuthenticationSuccessHandler successHandler = new LoginSuccessHandler();
    private final AuthenticationFailureHandler failureHandler = new LoginFailureHandler();
    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/user/**").authenticated()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().permitAll();

        http.formLogin()
                .loginPage("/signin") // "/signin"으로 요청하면 로그인 페이지 출력
                .loginProcessingUrl("/login") // "/login" 이 호출되면 시큐리티가 낚아채서 로그인을 진행한다. 
                실제 로그인을 진행할 url(POST로 요청해야됨)
                .usernameParameter("email") // username대신 email을 이용해 인증할 것이다.
                .passwordParameter("password")// 
                .successHandler(successHandler) // 로그인 성공시 동작
                .failureHandler(failureHandler); // 로그인 실패시 동작

        http.sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 필요한 경우에만 세션 생셩
                .maximumSessions(1) // 동일한 계정으로는 1개의 세션만 생성 가능
                .sessionRegistry(sessionRegistry()); // sessionRegistry로 전체 세션 관리

        http.rememberMe()
                .key("ykj") // remember-me token을 생성할 때 사용할 키
                .rememberMeParameter("remember-me") // 자동 로그인 체크박스의 이름
                .tokenValiditySeconds(600) // remember-me token의 유효 기간(600) + 만료되면 자동으로 삭제됨
                .userDetailsService(userDetailsService) // remember-me token을 decode해 얻은 user의 정보로 user를 찾을 service
                .authenticationSuccessHandler(successHandler); // 자동 로그인 성공시 동작

        return http.build();
    }
}


자동 로그인을 하면, JSESSIONID와 remember-me 토큰도 같이 발급된 것을 확인할 수 있다.

LoginSuccessHandler

LoginSuccessHandler.java

public class LoginSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        System.out.println(authentication.getName());
        RequestCache requestCache = new HttpSessionRequestCache();
        SavedRequest savedRequest= requestCache.getRequest(request, response);
        String uri = "/";
        if (savedRequest != null) {
            uri = savedRequest.getRedirectUrl();
            requestCache.removeRequest(request,response);
        }
        response.sendRedirect(uri);
    }
}

로그인 성공 시, RequestCache를 통해 원래 요청했던 페이지의 url로 redirect해준다.

LoginFailureHandler

LoginFailureHandler.java

public class LoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        System.out.println(exception);
        response.sendRedirect("/signin");
    }
}

로그인 실패 시, 다시 로그인하는 화면으로 redirect해준다,


세션 모두 조회

이 빈은 정말 겨우 얻은 정보ㅜㅜ
로그인한 모든 유저들의 세션 리스트를 얻고 싶었는데, 정보를 찾기 힘들었다.

  • SesseionRegistry를 빈으로 등록하고 이를 http.sessionManagement에 등록
@RequestMapping(value = "/admin/sessions", method = RequestMethod.GET)
@ResponseBody
public List getSessions() {
        List principals = sessionRegistry.getAllPrincipals();

        if (principals != null) {
            List<SessionInformation> sessionInformations = new ArrayList<>();
            for (Object principal : principals) {
                sessionInformations.addAll(sessionRegistry.getAllSessions(principal, false));
            }
            return sessionInformations;
        }

        return Collections.EMPTY_LIST;
}


다른 아이디의 두 세션이 존재함을 확인할 수 있다.

자세한 코드는 github https://github.com/60jong/Security-Session


Spring Security + JWT + OAuth2 를 처음 공부할 때는 너무 헤맸고, 많이 힘들었다. 하지만 여러 자료를 참고해 Custom하게 구현을 했기에 어느 정도 Spring Security를 이해했다고 생각했지만, 이번에 다시 Spring Security를 공부하며 새로운 내용 + 몰랐던 내용이 너무 많았다. 세션을 이용한 방식이 어쩌면 우리 프로젝트에 더 적합하다고 생각이 들지만 더 공부해보며 능숙하고 깊게 이해하고 싶어졌다. (08-11)

profile
울릉도에 별장 짓고 싶다

0개의 댓글