본문 바로가기
웹개발/스프링시큐리티

5. 스프링 시큐리티 - Remember Me 기능 구현

by 어컴띵 2021. 4. 19.

사용자가 로그인 후 로그인을 유지 시켜주는 기능이 있다. 사용자 세션이 만료가 되어도 Remember Me 쿠키가 셋팅되어 있으면 자동으로 로그인하는 기능이다. 스프링 시큐리티에서 제공해주는 Rmember Me기능을 구현해보겠다.

 

이번 포스트는 이전 포스트에 이어서 작성하니 참조하기 바란다.

 

세션 설정

테스트를 위한 세션시간을 조정하기 위해 HttpSessionListener인터페이스 구현체를 하나 만든다. @WebListener 어노테이션으로 리스너를 등록해주고, 이 리스너는 스캔할수 있게 Application클래스에 @ServletComponentScan추가한다. 세션을 10초로 설정을 해준다. 10초이지만 시간이 더 지나야 세션이 만료된다.

SessionListener

@WebListener
public class SessionListener implements HttpSessionListener {

    @Override
    public void sessionCreated(HttpSessionEvent se) {
        System.out.println("session created");
        se.getSession().setMaxInactiveInterval(10);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        System.out.println("session destroyed");
    }
}

스프링시큐리티 설정

rmemberMe 기능은 토큰방식과 db방식이 있는데 여기서는 DB 방식으로 진행을 한다. DB방식의 경우 테이블을 생성하여 로그인시에 해당 테이블에 관련 정보를 저장하고 세션이 만료된경우 사용자의 Remember Me가 사용으로 설정(쿠키에 remember-me가 있는경우)이 되어 있는경우 테이블에 정보와 쿠키의정보를비교하여 자동으로 로그인을 시켜준다. 그렇게 때문에 테이블을 하나 생성을 하고 테이블에 데이타를 저장하는 서비스객체를 선언하고 UserDetailsService를 명시해줘야 한다.

 

Rmemeber Me 관련 테이블 엔티티

@Getter
@Setter
@Entity
@Table(name = "persistent_logins")
public class PersistentLogins {

    @Id
    private String series;
    private String username;
    private String token;
    private Date lastUsed;
}

 

SecurityConfig

rememberMe()메서드를 추가하고 userDetailsService와 PersistentTokenRepository를 가져오는 메서드를 작성한다.

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    UserDetailsService userDetailsService;
    @Autowired
    DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                .authorizeRequests( authorize -> authorize
                        .antMatchers("/css/**","/js/**","/index").permitAll()
                        .antMatchers("/user/**").hasRole("USER")
                )
                .formLogin( formLogin -> formLogin
                        .loginPage("/login"))
                .rememberMe().key("rememberMe")
                    .userDetailsService(userDetailsService)
                    .tokenRepository(tokenRepository())
        ;
    }

    @Bean
    public PersistentTokenRepository tokenRepository(){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

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

로그인페이지 remember me 체크 쿠키적용

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
    <meta name="generator" content="Jekyll v4.1.1">
    <title>Signin Template · Bootstrap</title>
    <link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/sign-in/">
    <!-- Bootstrap core CSS -->
    <link href="/css/bootstrap.min.css" rel="stylesheet">
    <style>
        .bd-placeholder-img {
            font-size: 1.125rem;
            text-anchor: middle;
            -webkit-user-select: none;
            -moz-user-select: none;
            -ms-user-select: none;
            user-select: none;
        }

        @media (min-width: 768px) {
            .bd-placeholder-img-lg {
                font-size: 3.5rem;
            }
        }
    </style>
    <!-- Custom styles for this template -->
    <link href="/css/signin.css" rel="stylesheet">
</head>
<body class="text-center">
<form action="/login" class="form-signin" method="post">
    <a href="/"><img class="mb-4" src="/images/bootstrap-solid.svg" alt="" width="72" height="72"></a>
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="username" class="sr-only">User name</label>
    <input type="text" name="username" id="username" class="form-control" placeholder="User name" required autofocus>
    <label for="inputPassword" class="sr-only">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required>
    <div class="checkbox mb-3">
        <label>
            <input name="remember-me" id="rememberMe" type="checkbox" th:checked="${rememeberMe}"> Remember me
        </label>
    </div>
    <div th:if="${param.error}" class="alert alert-danger">
        메일 혹은 비밀번호를 다시 확인해 주세요.
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
    <div class="float-right text-dark">
        <p><a href="/register">회원가입</a></p>
    </div>

    <p class="mt-5 mb-3 text-muted">&copy; 2017-2020</p>
</form>
<script
        src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
        crossorigin="anonymous"></script>
<script src="/js/js.cookie.min.js"></script>
<script>
    $("#rememberMe").click(function () {
        if ($("input:checkbox[id='rememberMe']").is(":checked")) {
            Cookies.set("rememberMe", true);
        } else {
            Cookies.set("rememberMe", false);
        }
        alert("rememberMe")
    })
    if (Cookies.get("rememberMe") === 'true') {
        $("input:checkbox[id='rememberMe']").prop("checked", true);
    }
</script>
</body>
</html>

RememberMeAuthenticationFilter

스프링 시큐리티에서 Remember Me기능을 사용하게 되면 RememberMeAuthenticationFilter클래스에서 처리를 한다. RememberMeAuthenticationFilter에서 PersistentTokenBaseRememberMeService를 이용해서 remember-me 쿠키가 있는지 체크해서 쿠키가 있으면 해당 쿠키로 db에서 같은 토큰이 있는지 검색을 하고, 존재하는 경우 새로운 토큰으로 db값을 업데이트하며, 토큰의 username으로 UserDetailsService의 loadUserByUsername 메서드를 호출해서 UserDetails를 반환한다. 그리고 AbstractRememberMeService의 createSuccessfulAuthentication메서드를 실행하고 Authentication 을 리턴한다. 그러면 이 Authentication으로 ProviderManager에서 authentication 메서드를 실행하고 이 메서드는 여러개의  AuthenticationProvider중에  RememberMeAuthenticationProvider의 authentication 메서드를 실행하여 인증된 Authentication 을 반환하면 Remember Me 기능으로 자동으로 로그인하게 된다.  

마치며

이상 스프링시큐리티에서 제공하는 Remember me 기능을 구현해봤다. 먼저 테스트를 위해 세션만료설정의 위한 Listener을 작성하고 @WebListener과 @ServletComponentScan 어노테이션을 이용하여 리스너가 등록되게하였다. 그리고 Remember Me 기능을 위한 rememberMe() 메소드를 이용하여 config()메서드에 추가하고 관련 클래스인 UserDetailsService와 PesistentTokenRepository도 같이 추가하였다. 그리고 로그인 페이지에서 remember me 체크시 쿠키에 저장하여 다음 로그인시에도 remember me가 체크된 상태가되게 구현을 하였다. 그리고 remember me 기능을 체크하는 RememberMeAuthenticationFilter에서의 처리도 알아 보았다.