huruyosi’s blog

プログラミングとかインフラとかのメモです。

spring boot その9 - spring security で Remember-Me認証を行う

実装したソースは https://github.com/huruyosiathatena/springboot/tree/b5e82c5b8e24f4a7508d7fd86ca6cc311fe43adf にあります。

Remember Me 認証

ログイン画面にある にある「Remember Me」にチェックをつけてログインすることで、次回アクセス時に認証を省略できます。 f:id:huruyosi:20150816123226p:plain

動き

「Remember Me」にチェックをつけてログインを行うと、「remember-me」という名前でcookieが発行されます。cookieにはtokenが設定されており、spring-security内部でログインIDと対応付けられています。

ブラウザを閉じた後に、認証が必要がURLにアクセスした時にremember-meのcookieが送られてきたら spring-securityが対応付けられたログインIDを探して、登録されていれば認証が省略されます。

Remember Meのトークンの管理

デフォルトではMapを用いているorg.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl クラスが利用されます。この方法では spring bootを再起動すると消えてしまうので、 org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImplクラスを利用して DB に保存します。

DBに保存するためのテーブルは JdbcTokenRepositoryImpl#setCreateTableOnStartup(boolean createTableOnStartup)にtrueを指定することで自動的に作られます。しかし、テーブルが存在する場合には失敗するので、あらかじめ作成します。

DDLはリファレンス http://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#remember-me-persistent-token に記載されています。

-- remember me 認証
create table persistent_logins(
  username varchar(64) not null,
  series varchar(64) primary key, 
  token varchar(64) not null,
  last_used timestamp not null
);

認可 - @Secured

@Securedに指定するロールにIS_AUTHENTICATED_REMEMBEREDを指定することでRemember-Meの認証情報でアクセスが許可されます。

意味
IS_AUTHENTICATED_ANONYMOUSLY ログインしていなくても許可する
IS_AUTHENTICATED_REMEMBERED Remember-Me認証でアクセスを許可する
IS_AUTHENTICATED_FULLY 現在のセッションでログインしていれば許可する。Remember-Me認証でアクセスした場合には否認される。

実際にはロール名と併せて@Secured{"IS_AUTHENTICATED_REMEMBERED","ROLE_STAFF"})という指定になると思います。

認可 - WebSecurityConfigurerAdapter#configure(HttpSecurity http)

HttpSecurityで設定する認証方法の指定も 上と同様に種類を使い分けます。

現在のセッションで認証が必要な場合には.fullyAuthenticated()を使います。

       http
                .authorizeRequests()
                .antMatchers("/sbadmin2/css/**").permitAll()
                .antMatchers("/sbadmin2/js/**").permitAll()
                .antMatchers("/sbadmin2/bower_components/**").permitAll()
                .anyRequest().fullyAuthenticated()

Remember-Me認証も許可する場合には.authenticated()を使います。

       http
                .authorizeRequests()
                .antMatchers("/sbadmin2/css/**").permitAll()
                .antMatchers("/sbadmin2/js/**").permitAll()
                .antMatchers("/sbadmin2/bower_components/**").permitAll()
                .anyRequest().authenticated()

テンプレートの記述

Remember-Meのチェックボックスは spring-securityの実装(org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices#rememberMeRequested(HttpServletRequest request, String parameter))に従う必要があります。

<input name="remember-me" type="checkbox" value="yes" />Remember Me
  • name属性はremember-meでなければなりません。
  • value属性は下のいずれかでなければなりません。trueon1yes

有効期間

デフォルトでは前回のアクセスから 2 週間は 有効です。変更する場合は WebSecurityConfigurerAdapter#configure(HttpSecurity http) 内でhttp.rememberMe().tokenValiditySeconds(604800)という要領で指定します。tokenValiditySecondsに秒数を指定します。

テンプレートの実装

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org"
  xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
  layout:decorator="sbadmin2ContentOnly">
<head>
</head>
<body>
  <div layout:fragment="content">
    <div class="container">
        <div class="row">
            <div class="col-md-4 col-md-offset-4">
                <div class="login-panel panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title">Please Sign In</h3>
                    </div>
                    <div class="panel-body">
                        <div class="alert alert-warning" role="alert" th:if="${param.logout}">You have been logged out</div>
                        <div class="alert alert-danger"  role="alert" th:if="${param.error}">There was an error, please try again</div>
                        <form role="form" method="post" th:action="@{/login}">
                            <fieldset>
                                <div class="form-group">
                                    <input class="form-control" placeholder="E-mail" name="userid" type="text" autofocus="" />
                                </div>
                                <div class="form-group">
                                    <input class="form-control" placeholder="Password" name="password" type="password" value="" />
                                </div>
                                <div class="checkbox">
                                    <label>
                                        <input name="remember-me" type="checkbox" value="yes" />Remember Me
                                    </label>
                                </div>
                                <!-- Change this to a button or input when using this as a form -->
                                <input type="submit" value="login" class="btn btn-lg btn-success btn-block" />
                            </fieldset>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
  </div>
</body>

</html>

WebSecurityConfigurerAdapter の実装

package com.hatenablog.huruyosi.springboot.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.hatenablog.huruyosi.springboot.service.JdbcUserDetailsServiceImpl;

@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled=true)
public class WebSecuirty extends WebSecurityConfigurerAdapter  {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private JdbcUserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/sbadmin2/css/**").permitAll()
                .antMatchers("/sbadmin2/js/**").permitAll()
                .antMatchers("/sbadmin2/bower_components/**").permitAll()
                .anyRequest().authenticated()
        .and().formLogin()
                .loginPage("/login")
                .usernameParameter("userid")
                .passwordParameter("password")
                .defaultSuccessUrl("/sbadmin2/index.html")
                .failureUrl("/login?error").permitAll()
        .and().logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login?logout")
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true).permitAll()
        .and().rememberMe()
                .tokenRepository(jdbcTokenRepository())
                .tokenValiditySeconds(604800) // remember for a week. ( 1 * 60 * 60 * 24 * 7 ) sec
        ;
    }

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.userDetailsService(userDetailsService);
    }
    
     /**
     * Remember Me 認証に利用するトークンのリポジトリ
    * @return
    */
    @Bean
    public PersistentTokenRepository jdbcTokenRepository() {
        JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
        repository.setDataSource(dataSource);
//     repository.setCreateTableOnStartup(true);
        return repository;
    }
}

コントローラーの実装

package com.hatenablog.huruyosi.springboot.controllers;

import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/sbadmin2/")
@Secured("IS_AUTHENTICATED_REMEMBERED")
public class SbAdmin2Controller {

    @RequestMapping("/index.html")
    public ModelAndView index() {
        return new ModelAndView("SbAdmin2Controller/index");
    }
~ 省略 ~
}