LoginSignup
26

More than 1 year has passed since last update.

Spring Boot Securityを使ってみる

Last updated at Posted at 2017-12-15

環境

Spring Boot 1.5.9.RELEASE
Java 8
Maven 4.0.0

概要

Spring Securityを使用し、DBにあるログイン情報と照合し認証をする。
DBはH2DBを使用し、ORMはDomaを使用します。

はじめに

Spring Inirializrでプロジェクトを生成します。
spring-initializr.png

今回はORMにDomaを使用するので、注釈処理を設定しておきます。
注釈処理.png
ファクトリー・パス.png

コード

デフォルトから変更しているもののみ記載します。

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');

フォルダ構成は以下の通りです。
パッケージエクスプローラー.PNG

動作確認

http://localhost:8080/security-sample/にアクセスし、
ユーザーIDにtest、パスワードにpassを入力するとメニュー画面に遷移します。
ログイン画面
ログイン.PNG
メニュー画面
メニュー.PNG

認証後の処理について

認証成功時、失敗時の処理については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");

    }

コメントアウトをしていますが、認証成功時、失敗時それぞれで
リダイレクト、フォワード、ハンドラクラスへの処理委譲が行えます。

今回作成したプロジェクトはここにあります

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26