
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関連本
- 作者: 槇俊明
- 出版社/メーカー: 工学社
- 発売日: 2014/11
- メディア: 単行本
- この商品を含むブログ (8件) を見る
- 作者: 掌田津耶乃
- 出版社/メーカー: 秀和システム
- 発売日: 2014/07/30
- メディア: 単行本
- この商品を含むブログ (2件) を見る