#環境
Spring Boot 1.5.9.RELEASE
Java 8
Maven 4.0.0
#概要
Spring Securityを使用し、DBにあるログイン情報と照合し認証をする。
DBはH2DBを使用し、ORMはDomaを使用します。
#はじめに
Spring Inirializrでプロジェクトを生成します。
今回はORMにDomaを使用するので、注釈処理を設定しておきます。
#コード
デフォルトから変更しているもののみ記載します。
pom.xml(※一部抜粋)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.seasar.doma.boot/doma-spring-boot-starter -->
<dependency>
<groupId>org.seasar.doma.boot</groupId>
<artifactId>doma-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>
UserEntity.java
package com.example.springbootsecuritysample.entity;
import java.util.Collection;
import org.seasar.doma.Entity;
import org.seasar.doma.Id;
import org.seasar.doma.jdbc.entity.NamingType;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Getter;
import lombok.Setter;
/**
* USERテーブルのEntity
* @author T.Harao
*
*/
@Entity(naming = NamingType.SNAKE_UPPER_CASE)
@Getter
@Setter
public class UserEntity implements UserDetails {
@Id
private String userId;
private String password;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getUsername() {
return userId;
}
/**
* UserDetailsServiceでチェックするパスワードを返却する
* Lombokを使用している場合、フィールドに「password」があれば、
* @GetterでgetPassword()を生成してくれる為、不要
*/
@Override
public String getPassword() {
return password;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDao.java
package com.example.springbootsecuritysample.dao;
import org.seasar.doma.Dao;
import org.seasar.doma.Select;
import org.seasar.doma.boot.ConfigAutowireable;
import com.example.springbootsecuritysample.entity.UserEntity;
/**
* USERテーブルにアクセスするDAO
* @author T.Harao
*
*/
@Dao
@ConfigAutowireable
public interface UserDao {
@Select
public UserEntity selectByUserId(String userId);
}
AuthService.java
package com.example.springbootsecuritysample.service;
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.Service;
import com.example.springbootsecuritysample.dao.UserDao;
import com.example.springbootsecuritysample.entity.UserEntity;
/**
* 認証を扱うService
* @author T.Harao
*
*/
@Service
public class AuthService implements UserDetailsService {
@Autowired
private UserDao dao;
/**
* ユーザー読み込み
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if(username == null || "".equals(username)) {
throw new UsernameNotFoundException("ユーザーIDが未入力です");
}
UserEntity user = dao.selectByUserId(username);
if(user == null) {
throw new UsernameNotFoundException("ユーザーIDが不正です。");
}
return user;
}
}
IndexForm.java
package com.example.springbootsecuritysample.form;
import org.hibernate.validator.constraints.NotEmpty;
import lombok.Getter;
import lombok.Setter;
/**
* IndexControllerで使用するForm
* @author T.Harao
*
*/
@Getter
@Setter
public class IndexForm {
@NotEmpty
private String userId;
@NotEmpty
private String password;
}
IndexController.java
package com.example.springbootsecuritysample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import com.example.springbootsecuritysample.form.IndexForm;
/**
* ログイン
* @author T.Harao
*
*/
@Controller
@RequestMapping({"/", "/index"})
public class IndexController {
@ModelAttribute
public IndexForm initForm(){
return new IndexForm();
}
/**
* 初期表示
* @param mv
* @return
*/
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public ModelAndView index(ModelAndView mv) {
mv.setViewName("index/index");
return mv;
}
/**
* 認証エラー時
* @param mv
* @return
*/
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.POST)
public ModelAndView login(@ModelAttribute @Validated IndexForm form, BindingResult result, ModelAndView mv) {
if(!result.hasErrors()) {
mv.addObject("errorMessage", "ログイン情報が間違っています");
}
mv.setViewName("index/index");
return mv;
}
}
MenuController.java
package com.example.springbootsecuritysample.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
/**
* メニュー
* @author T.Harao
*
*/
@Controller
@RequestMapping("/menu")
public class MenuController {
/**
* 初期表示
* @param mv
* @return
*/
@RequestMapping(value = {"/", "/index"}, method = RequestMethod.GET)
public ModelAndView index(ModelAndView mv) {
mv.setViewName("menu/index");
return mv;
}
}
FailureHandler.java
package com.example.springbootsecuritysample.config.handler;
import java.io.IOException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
/**
* 認証失敗時のハンドラ
* @author T.Harao
*
*/
@Component
public class FailureHandler implements AuthenticationFailureHandler {
/**
* 認証失敗時
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
//「/」にForwardする
RequestDispatcher dispatch = request.getRequestDispatcher("/");
dispatch.forward(request, response);
}
}
SuccessHandler.java
package com.example.springbootsecuritysample.config.handler;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
/**
* 認証成功時のハンドラ
* @author T.Harao
*
*/
@Component
public class SuccessHandler implements AuthenticationSuccessHandler {
/**
* 認証成功時
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
//「/menu/」にリダイレクトする
response.sendRedirect(request.getContextPath() + "/menu/");
}
}
SecurityConfig.java
package com.example.springbootsecuritysample.config;
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.crypto.password.NoOpPasswordEncoder;
import com.example.springbootsecuritysample.config.handler.FailureHandler;
import com.example.springbootsecuritysample.config.handler.SuccessHandler;
import com.example.springbootsecuritysample.service.AuthService;
/**
* セキュリティ設定
* @author T.Harao
*
*/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthService service;
@Autowired
private FailureHandler failureHandler;
@Autowired
private SuccessHandler successHandler;
/**
* WebSecurityの設定
*/
@Override
public void configure(WebSecurity web) throws Exception {
// 静的リソース(images、css、javascript)とH2DBのコンソールに対するアクセスはセキュリティ設定を無視する
web.ignoring().antMatchers("/css/**", "/fonts/**", "/images/**", "/js/**", "/h2-console/**");
}
/**
* HttpSecurityの設定
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//認可の設定
http.authorizeRequests()
//認証無しでアクセスできるURLを設定
.antMatchers("/", "/index/**").permitAll()
//上記以外は認証が必要にする設定
.anyRequest().authenticated();
//ログイン設定
http.formLogin()
//認証処理のパスを設定
.loginProcessingUrl("/index/login")
//ログインフォームのパスを設定
.loginPage("/")
.loginPage("/index/**")
//認証成功時にリダイレクトするURLを設定
.defaultSuccessUrl("/menu/")
//認証失敗時にforwardするURLを設定
.failureForwardUrl("/")
//認証成功時にforwardするURLを設定
//.successForwardUrl("/")
//認証成功時に呼ばれるハンドラクラスを設定
//.successHandler(successHandler)
//認証失敗時にリダイレクトするURLを設定
//.failureUrl("/menu/")
//認証失敗時に呼ばれるハンドラクラスを設定
//.failureHandler(failureHandler)
//ユーザー名、パスワードのパラメータ名を設定
.usernameParameter("userId").passwordParameter("password");
}
/**
* 設定
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//パスワードは平文でDBに登録する為、「NoOpPasswordEncoder」を設定する
auth.userDetailsService(service)
.passwordEncoder(NoOpPasswordEncoder.getInstance());
}
}
layout.html
<!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"
>
<head>
<meta charset="UTF-8" />
<title layout:title-pattern="$DECORATOR_TITLE - $CONTENT_TITLE">Spring Securityテスト</title>
<link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}" media="all" />
<link rel="stylesheet" type="text/css" href="/css/bootstrap-theme.min.css" th:href="@{/css/bootstrap-theme.min.css}" media="all" />
<script type="text/javascript" src="/js/jquery-1.12.4.min.js" th:src="@{/js/jquery-1.12.4.min.js}"></script>
<script type="text/javascript" src="/js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}"></script>
</head>
<body>
<div class="contents" layout:fragment="contents"></div>
</body>
</html>
index/index.html
<!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="layout"
>
<head>
<title>ログイン</title>
</head>
<body>
<div layout:fragment="contents">
<form class="form-horizontal" method="POST" action="/index/login/" th:action="@{/index/login}" th:object="${indexForm}">
<div th:text="${errorMessage}?: ''" class="col-sm-offset-2 text-danger"></div>
<div class="form-group">
<p th:if="${#fields.hasErrors('*{userId}')}" th:errors="*{userId}" class="col-sm-offset-2 text-danger"></p>
<label for="user-id" class="col-sm-2 control-label">ユーザーID</label>
<div class="col-sm-5">
<input type="text" class="form-control" id="user-id" th:field="*{userId}" placeholder="ユーザーID" />
</div>
</div>
<div class="form-group">
<p th:if="${#fields.hasErrors('*{password}')}" th:errors="*{password}" class="col-sm-offset-2 text-danger"></p>
<label for="password" class="col-sm-2 control-label">パスワード</label>
<div class="col-sm-5">
<input type="password" class="form-control" id="password" th:field="*{password}" placeholder="パスワード" />
</div>
</div>
<div class="form-group">
<input type="submit" class="btn btn-primary col-sm-2 col-sm-offset-2" name="login" value="ログイン" />
<input type="reset" class="btn btn-default col-sm-2 col-sm-offset-1" name="clear" value="クリア" />
</div>
</form>
</div>
</body>
</html>
menu/index.html
<!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="layout"
>
<head>
<title>メニュー</title>
</head>
<body>
<div layout:fragment="contents">
<h2>メニュー</h2>
</div>
</body>
</html>
application.yaml
#spring
spring:
profiles:
active: dev
datasource:
url: jdbc:h2:./db
#server
server:
contextPath: /security-sample
#doma
doma:
dialect: h2
application-dev.yaml
#spring
spring:
h2:
console:
enabled: true
thymeleaf:
cache: false
application-production.yaml
#spring
spring:
h2:
console:
enabled: false
thymeleaf:
cache: true
selectByUserId.sql
select
user_id
,password
from
user
where
user_id = /*userId*/''
schema.sql
--drop table if exists user;
create table if not exists user (
user_id varchar(30) not null primary key
,password varchar(30) not null
);
data.sql
insert into user (user_id,password) values ('test','pass');
#動作確認
http://localhost:8080/security-sample/にアクセスし、
ユーザーIDにtest
、パスワードにpass
を入力するとメニュー画面に遷移します。
ログイン画面
メニュー画面
#認証後の処理について
認証成功時、失敗時の処理についてはSecurityConfig.java
の以下の部分に記載します。
SecurityConfig.java
/**
* HttpSecurityの設定
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
//認可の設定
http.authorizeRequests()
//認証無しでアクセスできるURLを設定
.antMatchers("/", "/index/**").permitAll()
//上記以外は認証が必要にする設定
.anyRequest().authenticated();
//ログイン設定
http.formLogin()
//認証処理のパスを設定
.loginProcessingUrl("/index/login")
//ログインフォームのパスを設定
.loginPage("/")
.loginPage("/index/**")
//認証成功時にリダイレクトするURLを設定
.defaultSuccessUrl("/menu/")
//認証失敗時にforwardするURLを設定
.failureForwardUrl("/")
//認証成功時にforwardするURLを設定
//.successForwardUrl("/")
//認証成功時に呼ばれるハンドラクラスを設定
//.successHandler(successHandler)
//認証失敗時にリダイレクトするURLを設定
//.failureUrl("/menu/")
//認証失敗時に呼ばれるハンドラクラスを設定
//.failureHandler(failureHandler)
//ユーザー名、パスワードのパラメータ名を設定
.usernameParameter("userId").passwordParameter("password");
}
コメントアウトをしていますが、認証成功時、失敗時それぞれで
リダイレクト、フォワード、ハンドラクラスへの処理委譲が行えます。
今回作成したプロジェクトはここにあります