SpringSecurityで独自テーブルを使って認証を行う
SpringBootでSpringSecurityを使って独自認証でログイン機能を実装してみました。
ドキュメントを参考にやってみたけど、詳しく書いてなくて理解に苦しみ結構ハマりました。
最終的にSpringSecurityのソースを見ることで認証オブジェクトの仕組みを理解しました。
概要
SpringSecurityでDBを使用して認証する場合、SpringSecurity付属のテーブル定義を行う必要があります。
今回は、独自テーブルを使った認証を行い、ロールは使用しません。
SpringSecurityの設定
SecurityConfigクラスを作成し、アクセス制限、ログイン処理、ログアウト処理等を定義します。
認証チェック処理クラス
AuthenticationProviderインターフェースを継承して独自クラスを作成します。
このクラスで独自テーブルを参照して認証チェック処理を定義します。
認証オブジェクト作成サービスクラス
UserDetailsServiceインターフェースを継承し独自クラスを作成します。
このクラスで認証後にシステム内で使用する認証オブジェクトを生成します。
認証オブジェクトクラス
Userクラスを継承したクラスを作成し、username、passwordフィールドを定義します。
username、passwordフィールドを定義することで、システム内で認証オブジェクトをオブジェクトで持つことができ、他に必要な情報を持たせることができるようになります。
環境
Eclipse | 4.3 |
---|---|
Java | 1.7 |
SpringBoot | 1.2.4 |
SpringSecurity | 3.2.7 |
Thymeleaf | 2.1.4 |
Doma | 1.0.38 |
Gradle | 2.3.10 |
構成
build.gradle
buildscript { ext { springBootVersion = '1.2.4.RELEASE' } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath("io.spring.gradle:dependency-management-plugin:0.5.1.RELEASE") } } apply plugin: 'java' apply plugin: 'eclipse-wtp' apply plugin: 'idea' apply plugin: 'spring-boot' apply plugin: 'io.spring.dependency-management' apply plugin: 'war' war { baseName = 'demo' version = '1.0' } sourceCompatibility = 1.7 targetCompatibility = 1.7 // for Doma // JavaクラスとSQLファイルの出力先ディレクトリを同じにする processResources.destinationDir = compileJava.destinationDir // コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する compileJava.dependsOn processResources repositories { maven {url 'http://maven.seasar.org/maven2'} mavenCentral() } configurations { providedRuntime } dependencies { compile("org.springframework.boot:spring-boot-starter-aop") // SpringSecurityの依存 compile("org.springframework.boot:spring-boot-starter-security") compile("org.springframework.boot:spring-boot-starter-thymeleaf") compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-jdbc") // htmlでThymeleaf用のSpringSecurityタグ使うためのもの compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity3") compile("org.hibernate:hibernate-validator") compile("org.seasar.doma:doma:1.38.0") compile("org.projectlombok:lombok:1.16.4") compile files("C:/app/lib/jdbc/ojdbc7.jar") providedRuntime("org.springframework.boot:spring-boot-starter-tomcat") } eclipse { classpath { containers.remove('org.eclipse.jdt.launching.JRE_CONTAINER') containers 'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7' } } task wrapper(type: Wrapper) { gradleVersion = '2.3' }
demo/SecurityConfig.java
package demo; 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.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import demo.impl.AuthenticationProviderImpl; import demo.impl.UserDetailsServiceImpl; @Configuration @EnableWebMvcSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserDetailsServiceImpl userDetailsService; @Autowired private AuthenticationProviderImpl authenticationProvider; @Override protected void configure(HttpSecurity http) throws Exception { http .headers() .xssProtection() .frameOptions() .contentTypeOptions() .cacheControl() .and() .authorizeRequests() // 認証対象外のパスを設定する .antMatchers("/", "/login", "/registration/**", "/css/**", "/js/**", "/img/**") // 上記パスへのアクセスを許可する .permitAll() // その他のリクエストは認証が必要 .anyRequest().authenticated() .and() .formLogin() // ログインフォームのパス .loginPage("/") // ログイン処理のパス .loginProcessingUrl("/login") // ログイン成功時の遷移先 .defaultSuccessUrl("/menu") // ログイン失敗時の遷移先 .failureUrl("/login-error") // ログインフォームで使用するユーザー名のinput name .usernameParameter("empNo") // ログインフォームで使用するパスワードのinput name .passwordParameter("password") .permitAll() .and() .rememberMe() .tokenValiditySeconds(86400) // 1ヶ月(秒) .and() .logout() // ログアウトがパス(GET)の場合設定する(CSRF対応) .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) // ログアウトがPOSTの場合設定する //.logoutUrl("/logout") // ログアウト後の遷移先 .logoutSuccessUrl("/") // セッションを破棄する .invalidateHttpSession(true) // ログアウト時に削除するクッキー名 .deleteCookies("JSESSIONID", "remember-me") .permitAll(); } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { // 独自認証クラスを設定する auth .authenticationProvider(authenticationProvider) .userDetailsService(userDetailsService); } }
demo/dto/LoginUser.java
package demo.dto import java.util.ArrayList; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.ToString; import demo.entity.Emp; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.User; @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) public class LoginUser extends User { private static final long serialVersionUID = 1L; // 追加する(テーブルでユーザーのキーとなる値を設定する) public String username; // 追加する public String password; // 独自で必要な項目 public String empNm; public LoginUser(Emp emp) { super(emp.empNo, emp.password, true, true, true, true, new ArrayList<GrantedAuthority>()); username = emp.empNo; password = emp.password; empNm = emp.empNm; } }
demo/impl/AuthenticationProviderImpl.java
package demo.impl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.stereotype.Component; import demo.dao.EmpDao; import demo.entity.Emp; @Component public class AuthenticationProviderImpl implements AuthenticationProvider { private static final Logger log = LoggerFactory.getLogger(AuthenticationProviderImpl.class); @Autowired private EmpDao empDao; @Override public Authentication authenticate(Authentication auth) throws AuthenticationException { String id = auth.getName(); String password = auth.getCredentials().toString(); if ("".equals(id) || "".equals(password) { // 例外はSpringSecurityにあったものを適当に使用 throw new AuthenticationCredentialsNotFoundException("ログイン情報に不備があります。"); } Emp emp = empDao.authEmp(id, password); if (emp == null) { // 例外はSpringSecurityにあったものを適当に使用 throw new AuthenticationCredentialsNotFoundException("ログイン情報が存在しません。"); } return new UsernamePasswordAuthenticationToken(new LoginUser(emp), password, auth.getAuthorities()); } @Override public boolean supports(Class<?> token) { return UsernamePasswordAuthenticationToken.class.isAssignableFrom(token); } }
demo/impl/UserDetailsServiceImpl.java
package demo.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import demo.dao.EmpDao; import demo.dto.LoginUser; import demo.entity.Emp; @Component public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private EmpDao empDao; @Override public UserDetails loadUserByUsername(String empNo) throws UsernameNotFoundException { Emp emp = empDao.findByNo(empNo); if (emp == null) { throw new UsernameNotFoundException("ユーザーが見つかりませんでした。"); } return new LoginUser(emp); } }
demo/web/LoginController.java
package demo.web; import javax.validation.Valid; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.RequestMapping; import demo.form.LoginForm; @Controller public class LoginController { @RequestMapping(value = "/") public String index(Model model) { model.addAttribute(new LoginForm()); return "login/login"; } /* ログイン処理は実装しない。SpringSecurityの処理で行われる。 @RequestMapping(value = "/login") public String login(@Valid LoginForm form, BindingResult result, Model model) { if (result.hasErrors()) { return "login/login"; } return "redirect:/menu"; } */ // SpringConfigで設定したログインできなかった場合の処理を定義する @RequestMapping(value = "/login-error") public String loginError(Model model) { model.addAttribute("loginError", true); return "login/login"; } }
demo/web/MenuController.java
package demo.web; import org.springframework.security.web.bind.annotation.AuthenticationPrincipal; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import demo.dto.LoginUser; @Controller public class MenuController { @RequestMapping(value = "/menu") public String index(@AuthenticationPrincipal LoginUser loginUser, Model model) { // @AuthenticationPrincipalを使うと認証オブジェクトを参照できる。 return "menu/menu"; } }
templates/login.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> </head> <body> <h1>login</h1> <!-- ログインできなかった時のエラーメッセージ --> <p th:if="${loginError}">Login Error!!</p> <form th:action="@{/login}" method="post"> <table> <tr> <td>社員番号</td> <td> <input type="text" name="empNo" /> </td> </tr> <tr> <td>パスワード</td> <td> <input type="password" name="password" /> </td> </tr> </table> <input name="remember-me" type="checkbox" />ログインしたままにする <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="submit" name="login" value="ログイン" /> </form> </body> </html>
templates/menu.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> </head> <body> <h1>menu</h1> <!-- 認証されているか --> <p th:if="${#authorization.expression('isAuthenticated()')}">認証済み</p> <!-- 認証オブジェクトの参照 --> <p th:text="${#authentication.principal.empNm}"></p> </body> </html>
Spring関連本
はじめてのSpring Boot―「Spring Framework」で簡単Javaアプリ開発 (I・O BOOKS)
- 作者: 槇俊明
- 出版社/メーカー: 工学社
- 発売日: 2014/11
- メディア: 単行本
- この商品を含むブログ (8件) を見る
- 作者: 掌田津耶乃
- 出版社/メーカー: 秀和システム
- 発売日: 2014/07/30
- メディア: 単行本
- この商品を含むブログ (2件) を見る
MacにMercurialをインストールする
手順
1.公式サイトよりMac用Mercurialをダウンロードします。
http://mercurial.selenic.com/downloads
2. ダウンロードしたzipファイルを解凍し、pkgファイルをダブルクリックしてインストーラーを起動しインストールします。
3.ターミナルを起動し、ログインユーザーのユーザーディレクトリにMercurail用設定ファイル.hgrcを作成します。
touch ~/.hgrc
4.viエディタ等で.hgrcを開き、次を入力し保存します。
[ui] username = コミット時のユーザー名
5.ターミナルよりMercurialがインストールされているか確認します。
hg --version
結果
Mercurial Distributed SCM (version 3.0.1+20140606) (see http://mercurial.selenic.com for more information) Copyright (C) 2005-2014 Matt Mackall and others This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
使わなくなったMacをお金に換えた2つの方法を体験談を交えて紹介します
私がメインで使用しているパソコンはMacで、3~5年周期で買い替えを行っています。
使わなくなったMacを押入れ等に入れて放置しておくのはもったいないので、毎回売却しています。
Macは値引きがあまりなく価格が高いイメージですが、Windowsパソコンよりも高く売れます。
Windowsパソコンを何十万と出して買っても、売るときには1万以下または売れないという状態が多いと思います。
Macだと買う・売るトータルで考えるとWindowsパソコンより安く買えていることになります。
トータルの費用を抑えるために、Macを高く売った経験談を紹介します。
ネット買取
Macの状態
多少の薄いキズはありましたが、きれいな方だったと思います。
買取までの様子
Macを高価買取してくれるところを探していると「ビワコム」というサイトにたどり着きました。
サイトより個人情報やMacの状態を入力して見積を依頼しました。
Macの状態を見てもらうために、お店にMacを送る必要がありました。
集荷は指定日に業者が訪問してきて、Macを渡すだけでした。
送料はお店負担だったので費用は発生しませんでした。
しばらく経ってから、買取金額の連絡がきました。
買取金額は、面瀬で提示されている上限金額で満足したので、そのまま買取をお願いしました。
後日、指定の銀行口座に買取金額が振り込まれました。
金額
金額 | |
---|---|
購入 | 191,000円 |
売却 | 46,000円 |
実購入費 | 145,000円 |
感想
とても簡単に買取をしてもらい、高額で買い取ってもらえたので満足でした。
他人に任せっきりでMacを処分したいならおすすめです。
Amazonマーケットプレイス
売ったMac
2台目はMacBookAir11インチでした。
売却までの様子
以前ネット買取をしてもらって満足していたのですが、さらに高く売りたいと思いました。
普段から本の売却はAmazonマーケットプレイスを使っていたので、Macを出品してみようと試してみました。
すぐに注文があったのですが、注文主のクレジットカード情報が登録されておらず、こちらとしては発送が全くできない状態でした。
注文主からは、「早く発送してくれ」と何度も問い合わせのメールがあり、どうしようもないので返信だけして、カード情報が登録されるまでずっと待っていました。
いつになってもカード情報が登録されないので、自動的にキャンセルになりました。
売れなかったので少しショックでしたが、諦めずに再度出品しました。
すると数カ月後に注文があり無事売れました。
出品したMacは、アダプターが一部断線しかけていて、充電できたりできなかったりの状態でした。
商品説明にアダプターの断線のこともきっちり書いたので、それで納得して買ってくれる人もいるんだなーと勉強になりました。
金額
金額 | |
---|---|
購入 | 89,000円 |
売却 | 50,099円 |
実購入費 | 38,901円 |
感想
買い取りよりも高く売ることができるし、任意の値段をつけることができます。
商品の状態が悪くても、値段相応のものでも良いと思ってる方がいたりすると、買取では値段がつかなくても、売れる可能性があります。
売れるまでに時間がかかるので、次の新しいモデルが出ると、値段を下げざるをえなくなってきます。
辛抱強く、買取よりも少しでも高く売りたいという方におすすめです。
まとめ
売却のことも考えて使用すると、トータルで安くできます。
基本的にモノは、大事に使うことに越したことはないので、それを心がけましょう。
crontabで第?曜日にタスクを実行する方法
crontabで第?曜日にタスクを実行する方法を紹介します。
サンプル
日曜日にシャットダウンを実行する場合の例です。
# 第1日曜日(5月) 0 0 1-7 5 * [ "$(date '+\%w')" -eq 0 ] && /sbin/shutdown -h now # 第2日曜日(8月) 0 0 8-14 8 * [ "$(date '+\%w')" -eq 0 ] && /sbin/shutdown -h now # 第3日曜日(9、11、12月) 0 0 15-21 9,11-12 * [ "$(date '+\%w')" -eq 0 ] && /sbin/shutdown -h now # 第4日曜日(どの月でも) 0 0 22-28 * * [ "$(date '+\%w')" -eq 0 ] && /sbin/shutdown -h now # 第5日曜日(どの月でも) 0 0 29-31 * * [ "$(date '+\%w')" -eq 0 ] && /sbin/shutdown -h now
解説
日時
先頭の0 0 8-14 8 *
はcrontabの分、時、日、月、曜日の指定です。
曜日を指定しないのがミソです。
コマンド
日時指定の後に実際に実行するコマンドを定義します。
[ "$(date '+\%w')" -eq 0 ]
は、日付を取得し、そのときの曜日が日曜日(0)だったらという条件になっています。
曜日の変更は、crontabの日時設定の所で行うのではなく、コマンド内の条件部分で行います。
&&
は、前の条件が満たされたら次のコマンドを実行する条件分岐の判定文法になっています。
条件が満たされたら、/sbin/shutdown -h now
を実行し、シャットダウンを行います。
週ごとの区切りの日付を次のように定義します。
第1週 | 1-7 |
---|---|
第2週 | 8-14 |
第3週 | 15-21 |
第4週 | 22-28 |
第5週 | 29-31 |
曜日の定義は次の通りです。
月 | 1 |
---|---|
火 | 2 |
水 | 3 |
木 | 4 |
金 | 5 |
土 | 6 |
日 | 7または0 |
- 作者: 大角祐介
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2015/06/06
- メディア: 大型本
- この商品を含むブログ (4件) を見る
Linuxシステム[実践]入門 (Software Design plus)
- 作者: 沓名亮典
- 出版社/メーカー: 技術評論社
- 発売日: 2013/07/03
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (13件) を見る
PhantomJS2.0のバイナリをLinuxで使う
PhantomJS+CasperJSを使ってスクレイピングするプログラムを作成し、Macでは正常に動作していました。
サーバー環境であるLinuxで動作させると、なぜかリンクのクリックでエラーが出てプログラムが動作しません。
Mac環境では、PhantomJS2.0、CasperJS1.1-bata3で問題なく動作していますが、
Linux環境では、PhantomJS1.9.8、CasperJS1.1-bata3で問題が発生しています。
Macと同じ2.0のバイナリを使う方法がないか調査してみました。
ソースからビルドする
公式サイトには、Linux版のバイナリが配布されておらず、各自ビルドするように書かれています。
Binary packages for Linux are still being prepared. There are still issues to be solved until a static build is available (see issue 12948 for more details). In the mean time, it is recommended that you build the Linux version from source.
しかし、問題があるようでビルドしても時間がかかって最終的にエラーでバイナリが生成されません。
現在では、gitからチェックアウトするとビルドできるようになっていますが、「2.0.1-development」ということで正式版ではありません。
PhantomJS2.0のバイナリをダウンロードする
次のサイトにパッチを当てたビルド方法が書かれていて、最後に2.0のバイナリがダウンロードできるようになっています。
ビルドしても時間がかかるだけなので、素直にバイナリをダウンロードしましょう。
Chromeユーザーも必見!ブラウザでファイルをダウンロードするなら10倍速くなるFireFoxアドオンの「DownThemAll!」を使おう!
--- 注意事項 ---
FIreFox57以降では、使用できず、代替のダウンローダープラグインが全くありません。
そのため、FireFox56以下を使用するようにして下さい。
自動更新されてしまった場合、「56をダウンロード→ネット切断→設定で自動更新しない→ネット接続」の手順で56に戻しましょう。
FireFoxのアドオンに「DownThemAll!」というものがあり、これを使ってファイルのダウンロードを行うと通常の10倍でダウンロードが可能です。
https://addons.mozilla.org/ja/firefox/addon/downthemall/addons.mozilla.org
ISOファイルなどのGB単位でダウロードに時間が掛かるファイルに使うとかなり速くダウンロードが可能になります。
MSDNでISOファイルをダウンロードするときに利用すると、途中で切断されることもなくダウンロードできます。
高速にダウンロード出来る機能の他に、ダウンロードを一時停止して途中からダウンロードを行うレジューム機能にも対応しています。
Chrome使いの方も、巨大ファイルをダウンロードする時はFireFoxを使うことをおすすめします。
設定画面
一般
操作
ネットワーク
ダウンロードの上限数、サーバーごとの上限数を最大にしておきます。
プライバシー
フィルター
詳細
ダウンロードの最大分割数を最大の10にして常時高速化するようにしておきます。
ダウンロード時
保存
DownThemAll!を選択すると高速にダウンロードが可能です。
進捗
ダウンロード情報
緑のグラフで分割してダウンロードしているのが分かります。
VMwareFusionのNAT接続でゲストOSのIPアドレスを固定する
手順
1.ゲストOSで「ifconfig」を実行し、ネットワークのMACアドレスを取得します。
MACアドレスは「HWaddr」の部分です。
ifconfig eth1 Link encap:Ethernet HWaddr 00:50:56:2D:7D:C8 inet addr:192.168.152.3 Bcast:192.168.152.255 Mask:255.255.255.0 inet6 addr: fe80::250:56ff:fe2d:7dc8/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:11 errors:0 dropped:0 overruns:0 frame:0 TX packets:20 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:2430 (2.3 KiB) TX bytes:3031 (2.9 KiB)
2.ホストOSで「dhcpd.conf」ファイルを編集します。
sudo vi "/Library/Preferences/VMware Fusion/vmnet8/dhcpd.conf"
3.「dhcpd.conf」にゲストOSの固定IPアドレスを定義します。
最終行にある「###〜」以降に「host」を追加して固定IPアドレスを定義します。
subnet 192.168.152.0 netmask 255.255.255.0 { range 192.168.152.128 192.168.152.254; option broadcast-address 192.168.152.255; option domain-name-servers 192.168.152.2; option domain-name localdomain; default-lease-time 1800; # default is 30 minutes max-lease-time 7200; # default is 2 hours option netbios-name-servers 192.168.152.2; option routers 192.168.152.2; } (中略) ####### VMNET DHCP Configuration. End of "DO NOT MODIFY SECTION" ####### # ゲストOSのIPアドレスを固定する host guest { # guestの部分はなんでもOK hardware ethernet 00:50:56:2D:7D:C8; # ゲストOSのMACアドレス fixed-address 192.168.152.3; # 好みのIPを設定する(3~127は固定用、128〜254はDHCP) }
4.設定を有効にするためにホストOSを再起動します。
5.ゲストOSで設定したIPアドレスになっているか確認します。
CentOS7で作るネットワークサーバ構築ガイド (Network server construction gu)
- 作者: サーバ構築研究会
- 出版社/メーカー: 秀和システム
- 発売日: 2015/03/25
- メディア: 単行本
- この商品を含むブログ (2件) を見る