Springで自作のエラーページを表示する
ここではSpring組み込みのWEBサーバーを使用するのではなく別のサーバーにデプロイするケースを考える。つまりEmbeddedServletContainerCustomizer
が使えないケース。
環境
Spring Boot 1.4 + Thymeleaf
やり方
以下は404 Not FoundでnotFound.html
、401 Forbiddenでforbidden.html
、それ以外のエラーでerror.html
を表示するサンプル。
import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.web.AbstractErrorController; import org.springframework.boot.autoconfigure.web.ErrorAttributes; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class CustomErrorController extends AbstractErrorController { private static final String ERROR_PATH= "/error"; @Autowired public CustomErrorController(ErrorAttributes errorAttributes) { super(errorAttributes); } @RequestMapping(ERROR_PATH) public String handleErrors(HttpServletRequest request, Model model) { HttpStatus status = getStatus(request); if (status.equals(HttpStatus.NOT_FOUND)) { return "notFound"; } if (status.equals(HttpStatus.FORBIDDEN)) { return "forbidden"; } model.addAttribute("exception", getErrorAttributes(request, true)); return "error"; } @Override public String getErrorPath() { return ERROR_PATH; } }
補足
モデルにgetErrorAttributes
で取得したエラー情報を渡しているのはerror.html
にエラーの内容を表示するため。
フォームの二重投稿を防止する
submitと同時にボタンをdisabledにする。
やり方
ページロードイベント中に以下を追加。
$('form').on('submit', function() { // 全てのボタンをdisabledにする $('button, input[type="submit"]').prop("disabled", true); }).on('click', '[type=submit]', function() { var form = $(this).closest('form'); // 押されたボタンと同名のhidden inputを作成する if (this.name) { form.children('input[type="hidden"].madeByScript').remove(); form.append($('<input/>', { type: 'hidden', name: this.name, value: this.value, class: 'madeByScript' })); } });
説明
単純にボタンをdisabledにするだけだとボタンに値が設定されていた場合にその値が飛ばなくなる。そこでsubmit前にボタンと同じnameとvalueを持つinputを作成して値を飛ばす。confirmでキャンセルされると作成したinputが残ってしまうため削除処理も入れておく。
ファイルダウンロードの終了をブラウザ側で検知したい
やり方
Cookieを使う。
$('form').on('submit', function() { const currentToken = getCookie("DOWNLOAD_TOKEN"); const downloadTimer = window.setInterval(function () { const newToken = getCookie("DOWNLOAD_TOKEN"); if (newToken != currentToken) { alert('ダウンロード終了!'); window.clearInterval(downloadTimer); } }, 1000 ); }); // クッキー取得 function getCookie( name ) { var parts = document.cookie.split(name + "="); if (parts.length >= 2) return parts.pop().split(";").shift(); }
解説
サーバー側はDOWNLOAD_TOKEN
という名前で毎回ランダムな値をクッキーにセットする。クライアントはsubmit前にDOWNLOAD_TOKEN
の値を取得、submit後は1秒ごとにクッキーを監視する。DOWNLOAD_TOKEN
の値が変わったらダウンロード終了。
WebフォームでEnterキーを押したときにフォーカスを移動させたい
いやTabキー押せよ、って思うけど仕方ない。
やり方
$('form').on('keydown', 'input, button, select', function(e) { if (e.keyCode == 13) { if ($(this).attr("type") == 'submit') return; var form = $(this).closest('form'); var focusable = form.find('input, button[type="submit"], select, textarea') .not('[readonly]').filter(':visible'); if (e.shiftKey) { focusable.eq(focusable.index(this) - 1).focus(); } else { var next = focusable.eq(focusable.index(this) + 1); if (next.length) { next.focus(); } else { focusable.eq(0).focus(); } } e.preventDefault(); } });
ポイント
textareaとsubmitボタンにフォーカスがある場合は移動せずに本来の動作(textareaなら改行、submitならsubmit)を行う。
ブラウザの戻るボタンを禁止したい
本当はそんなことしたくない。
なお、あくまで戻るボタンを封じるだけなので履歴を使うと戻れる。
環境
HTML5に対応したブラウザ
やり方
ページロードイベント中に以下のコードを追加。
history.pushState(null, null); $(window).on('popstate', function(e) { history.pushState(null, null); alert('ブラウザの戻るボタンは使用できません'); });
説明
ポイントは戻るたびにpushState
で余分な履歴を一つ追加すること。これによって本来一つ前だった戻り先が押し出されて二つ前になるため、戻るボタンでは戻れなくなる。
Spring Securityで独自の認証を実装する
環境
Spring Boot 1.4
やりたいこと
ログイン画面で所属部署コード、ユーザーID、パスワードを入力してログイン。認証はDBやAPI等、何かしら独自のロジックで行う。
用意するクラス
ユーザー情報クラス
ユーザー情報を保持するためのクラス。認証に必要な項目だけではなく、氏名や部署名など付随する情報も持てるようにしておくとあとあと便利に使える。
public class User { private String id; private String name; private String deptCode; private String deptName; // 以下getter setter }
AuthenticationFilter
認証のためのトークンを作成するクラス。まずはフォームから受け取ったパラメータでユーザー情報を作成。そしてユーザー情報とパスワードからトークンを作成し、後続の認証処理に渡す。
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; public class CustomAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) { User user = new User(); user.setId(request.getParameter("userId")); user.setDeptCode(request.getParameter("deptCode")); String password = obtainPassword(request); // トークンの作成 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user, password); setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } }
AuthenticationProvider
実際に認証を行うクラス。受け取ったトークンからユーザー情報とパスワードを取得してなんらかの独自認証を行う。ロールの付与もここで行う。
import java.util.ArrayList; import java.util.Collection; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; @Configuration public class CustomAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { User user = (User)auth.getPrincipal(); String password = (String)auth.getCredentials(); // ここで認証とロールの付与 Collection<GrantedAuthority> authorityList = new ArrayList<>(); if (user.getId().equals("admin")) { authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } else if (user.getId().equals("user")) authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); else { throw new BadCredentialsException("Authentication Error"); } return new UsernamePasswordAuthenticationToken(user, password, authorityList); } @Override public boolean supports(Class<?> authentication) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
supports
メソッドには受け取ったトークンが処理すべきものなのか判定する処理を書く。false
を返すとフレームワークは別の認証プロバイダーを探そうとするので、これを利用して複数の認証方法を提供することもできる。
設定
WebSecurityConfigurerAdapter
を継承したクラスで設定を行う。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired CustomAuthenticationProvider authenticationProvider; @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/images/**", "/loginForm"); } @Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated() .and() .logout() .logoutUrl("/logout").permitAll() .logoutSuccessUrl("/loginForm"); CustomAuthenticationFilter filter = new CustomAuthenticationFilter(); filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login", "POST")); filter.setAuthenticationManager(authenticationManagerBean()); filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/loginForm?error")); http.addFilterBefore(filter, CustomAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider(authenticationProvider); } }
上記はログインフォームのURLが/loginForm
、ログインのリクエストを受け取るURLが/login
となるサンプル。ポイントは前述したCustomAuthenticationProvider
とCustomAuthenticationFilter
をセットしているところ。
あとがき
DBで認証するならばAuthenticationProviderは自作せずにDaoAuthenticationProvider
を使用するのが正攻法なのかもしれない。が、調べた感じだとむしろ面倒くさくなりそうではある。