huruyosi’s blog

プログラミングとかインフラとかのメモです。

spring boot その7 - spring security で 認証を行う

実装したソースは https://github.com/huruyosiathatena/springboot/tree/8bb72d41f62d28039f5c7d0ccda8f4036ecea8ca にあります。

ログインを実装する

spring security を利用してログイン画面を作ります。今回は認証を行うだけで権限に基づいた制御は次回( http://huruyosi.hatenablog.com/entry/2015/08/10/015844 )で行います。

認証に利用するユーザ情報

DBに保存されているテーブルを利用します。

create table users (
  user_id int auto_increment,
  login_id varchar(256),
  password varchar(256),
  display_name varchar(255),
  enabled boolean,
  primary key(id)
);

create table authorities (
  user_id int,
  authority varchar(256),
  FOREIGN KEY (user_id) REFERENCES users(user_id)
);

usersテーブル

列名 意味
user_id 主キー
login_id ログイン画面に入力するID
password パスワード。べたな状態。ハッシュ化次の次
display_name ユーザの表示名
enabled 1なら有効

authorities テーブル

列名 意味
login_id 権限を許可されたユーザのID
authority 許可された権限名

データ投入の方法

src/main/resources/schema.sql と src/main/resources/data.sql を作成します。 schema.sqlにはDDLを記載し、data.sql にはデータ投入のDMLを記載します。アプリ起動時に spring boot が SQLスクリプトを実行します。

pom.xml

spring security と一緒にDBアクセスを行うためにJPA 及び h2Databaseのドライバーを追加します。

articact-id thymeleaf-extras-springsecurity3 は テンプレートで認証情報を参照するために追加しました。  ${#authentication.principal} と書くと MyUserDetail のインスタンスを得ることができます。

     <!-- JPA Database -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>

        <!-- security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity3</artifactId>
        </dependency>

src/main/resources/application.yml

DBの接続先を定義し、spring securiyのBASIC認証を無効にします。

spring:
  secuirt:
    basic.enable: false

  datasource:
    url: jdbc:h2:mem:sbadmin2;MODE=MYSQL
    username: sa
    password:
    driverClassName: org.h2.Driver

  jpa:
    hibernate.ddl-auto: none
    database-platform: org.hibernate.dialect.H2Dialect

DBアクセスを実装

com.hatenablog.huruyosi.springboot.entity.Users

usersテーブルのエンティティを実装します。

@Entity
@Table(name="users")
public class Users {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    protected Integer id;

    @Column(name="login_id")
    protected String loginId;

    protected String name;

    protected String password;

    @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    protected List<Authorities> authorities;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<Authorities> getAuthorities() {
        return authorities;
    }

    public void setAuthorities(List<Authorities> authorities) {
        this.authorities = authorities;
    }


    public String getLoginId() {
        return loginId;
    }

    public void setLoginId(String loginId) {
        this.loginId = loginId;
    }

    /**
    * {@link UserDetails}に変換します。
    * @return
    */
    public UserDetails toMyUserDetail() {
        return MyUserDetail.create(this);
    }

}

com.hatenablog.huruyosi.springboot.entity.Authorities

authoritiesテーブルのエンティティです。

@Entity
@Table(name="authorities")
public class Authorities {

    @Id
    private com.hatenablog.huruyosi.springboot.entity.type.AuthoritiesId id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id", insertable = false, updatable = false)
    private Users user;

    public Authorities() {
    }

    /** コンストラクタ */
    public Authorities(AuthoritiesId id){
        this.id = id;
    }

    public AuthoritiesId getId() {
        return id;
    }

    public void setId(AuthoritiesId id) {
        this.id = id;
    }

    public Users getUser() {
        return user;
    }

    public void setUser(Users user) {
        this.user = user;
    }

}

com.hatenablog.huruyosi.springboot.entity.type.AuthoritiesId

authoritiesテーブルが複合主キーなので 主キーのクラスを作成します。

@Embeddable
public class AuthoritiesId implements Serializable {

    private int user_id;
    private String authority;

    public AuthoritiesId() {
    }

    /** コンストラクタ */
    public AuthoritiesId(int user_id, String authority) {
        super();
        this.user_id = user_id;
        this.authority = authority;
    }

    public int getUser_id() {
        return user_id;
    }
    public void setUser_id(int user_id) {
        this.user_id = user_id;
    }
    public String getAuthority() {
        return authority;
    }
    public void setAuthority(String authority) {
        this.authority = authority;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result
                + ((authority == null) ? 0 : authority.hashCode());
        result = prime * result + user_id;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        AuthoritiesId other = (AuthoritiesId) obj;
        if (authority == null) {
            if (other.authority != null)
                return false;
        } else if (!authority.equals(other.authority))
            return false;
        if (user_id != other.user_id)
            return false;
        return true;
    }

}

com.hatenablog.huruyosi.springboot.repository.UserRepository

usersのDAOを用意します。

@Repository
public interface UserRepository extends JpaRepository<Users, Integer>{

    /**
    * {@link Users#get
    * @param loginId
    * @return
    */
    public Users findByLoginId(String loginId);
}

認証処理

認証に必要な処理を WebSecurityConfigurerAdapter を継承した WebSecuirty クラスに実装します。

ApplicationSecurity#configure(HttpSecurity http) メソッド

ログインとログアウトの設定を行います。

意味
.antMatchers("/sbadmin2/css/").permitAll().antMatchers("/sbadmin2/js/").permitAll().antMatchers("/sbadmin2/bower_components/**").permitAll() cssjavascript関係を認証状態に関係なく許可する
loginPage("/login") ログイン画面のパスは /login
usernameParameter("userid") ログインフォームのログインIDが設定されているinputタグのname属性の値
passwordParameter("password") ログインフォームのパスワードが設定されているinputタグのname属性の値
defaultSuccessUrl("/sbadmin2/index.html") ログイン成功後のリダイレクトのパスは/sbadmin2/index.html"
failureUrl("/login?error") ログインに失敗した時のパスは/login?error
logoutRequestMatcher(new AntPathRequestMatcher("/logout")) ログアウト画面のURLは/logout
logoutSuccessUrl("/login?logout") ログアウトに成功した時のリダイレクト先のパスは/login?logout
deleteCookies("JSESSIONID") ログアウトしたら cookieの JSESSIONID を削除
invalidateHttpSession(true) ログアウトしたらセッションを無効にする

configAuthentication(AuthenticationManagerBuilder auth) メソッド

認証を行う org.springframework.security.core.userdetails.UserDetailsServiceを実装したクラスを指定します。

com.hatenablog.huruyosi.springboot.config.WebSecuirty

package com.hatenablog.huruyosi.springboot.config;

import javax.sql.DataSource;

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.method.configuration.EnableGlobalMethodSecurity;
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 com.hatenablog.huruyosi.springboot.service.JdbcUserDetailsServiceImpl;

@Configuration
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled=true)
public class WebSecuirty extends WebSecurityConfigurerAdapter  {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private JdbcUserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/sbadmin2/css/**").permitAll()
                .antMatchers("/sbadmin2/js/**").permitAll()
                .antMatchers("/sbadmin2/bower_components/**").permitAll()
                .anyRequest().fullyAuthenticated()
        .and().formLogin()
                .loginPage("/login")
                .usernameParameter("userid")
                .passwordParameter("password")
                .defaultSuccessUrl("/sbadmin2/index.html")
                .failureUrl("/login?error").permitAll()
        .and().logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/login?logout")
                .deleteCookies("JSESSIONID")
                .invalidateHttpSession(true).permitAll();
    }

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth)
            throws Exception {
        auth.userDetailsService(userDetailsService);
    }
}

com.hatenablog.huruyosi.springboot.service.JdbcUserDetailsServiceImpl

org.springframework.security.core.userdetails.UserDetailsService を実装します。 loadUserByUsernameメソッドの引数には、ログイン画面で入力された ユーザIDが渡されるので、usersテーブルがlogin_idと入力されたユーザIDが一致するレコードを検索します。usersテーブルから検索して、レコードが無い場合には org.springframework.security.core.userdetails.UsernameNotFoundException を送出します。

usersテーブルにレコードがある場合には org.springframework.security.core.userdetails.UserDetailsを実装したクラスcom.hatenablog.huruyosi.springboot.service.MyUserDetailインスタンスを生成して返却します。

/**
 * DBを参照してユーザ情報を提供します。
 *
 */
@Component
public class JdbcUserDetailsServiceImpl implements UserDetailsService  {

    @Autowired
    private UserRepository userRepository;

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
    */
    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        if ( username == null || username.isEmpty() ){
            throw new UsernameNotFoundException("username is empty");
        }

        Users foundUser = userRepository.findByLoginId(username);
        if( foundUser != null ){
            return foundUser.toMyUserDetail();
        }
        throw new UsernameNotFoundException( username + "is not found");
    }
}

com.hatenablog.huruyosi.springboot.service.MyUserDetail

spring securityが提供する org.springframework.security.core.userdetails.User がありますが、独自の属性を提供するために org.springframework.security.core.userdetails.UserDetailsを実装したクラスを作成します。

/**
 * 本システム用 のユーザ情報
 *
 */
public class MyUserDetail implements UserDetails {
    private int userId;
    private String username;
    private String password;
    private String displayName;
    private Collection<GrantedAuthority> authorities;


    public MyUserDetail(int userId, String username, String password,
            String displayName, Collection<GrantedAuthority> authorities) {
        super();
        this.userId = userId;
        this.username = username;
        this.password = password;
        this.displayName = displayName;
        this.authorities = authorities;
    }

    /**
    * {@link Users}を元にインスタンスを生成します。
    * @param user 生成元になるユーザ
    * @return
    */
    public static UserDetails create(Users entity) {
        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
        for(Authorities auth: entity.getAuthorities()){
            authorities.add(new SimpleGrantedAuthority(auth.getId().getAuthority()));
        }
        return new MyUserDetail(entity.getId(), entity.getLoginId(), entity.getPassword(), entity.getName(), authorities);
    }


    /**
    * 永続化された{@link Users}の{@link Users#getId()}を取得します。
    * @return
    */
    public int getUserId(){
        return this.userId;
    }

    /**
    * 永続化された{@link Users}の{@link Users#getName()}を取得します。
    * @return
    */
    public String getDisplayName(){
        return this.displayName;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#getAuthorities()
    */
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.authorities;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#getPassword()
    */
    @Override
    public String getPassword() {
        return this.password;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#getUsername()
    */
    @Override
    public String getUsername() {
        return this.username;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isAccountNonExpired()
    */
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isAccountNonLocked()
    */
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isCredentialsNonExpired()
    */
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    /* (非 Javadoc)
    * @see org.springframework.security.core.userdetails.UserDetails#isEnabled()
    */
    @Override
    public boolean isEnabled() {
        return true;
    }
}

src/main/java/com/hatenablog/huruyosi/springboot/controllers/LoginController.java

ログイン画面を表示するコントローラーです。

package com.hatenablog.huruyosi.springboot.controllers;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class LoginController {

    @RequestMapping(value="/login")
    public String login(){
        return "/SbAdmin2Controller/login";
    }
}

src/main/resources/templates/SbAdmin2Controller/login.html

SBAdmin2のテンプレートからの変更箇所 * formタグにmethod属性とth:action属性を追加 * <input name="email">のname属性を userid に変更 * エラーとログアウトのメッセージを表示する<div class="alert">を追加 * ログインボタンをaタグからinputタグに変更

<!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="sbadmin2ContentOnly">
<head>
</head>
<body>
  <div layout:fragment="content">
    <div class="container">
        <div class="row">
            <div class="col-md-4 col-md-offset-4">
                <div class="login-panel panel panel-default">
                    <div class="panel-heading">
                        <h3 class="panel-title">Please Sign In</h3>
                    </div>
                    <div class="panel-body">
                        <div class="alert alert-warning" role="alert" th:if="${param.logout}">You have been logged out</div>
                        <div class="alert alert-danger"  role="alert" th:if="${param.error}">There was an error, please try again</div>
                        <form role="form" method="post" th:action="@{/login}">
                            <fieldset>
                                <div class="form-group">
                                    <input class="form-control" placeholder="E-mail" name="userid" type="text" autofocus="" />
                                </div>
                                <div class="form-group">
                                    <input class="form-control" placeholder="Password" name="password" type="password" value="" />
                                </div>
                                <div class="checkbox">
                                    <label>
                                        <input name="remember" type="checkbox" value="Remember Me" />Remember Me
                                    </label>
                                </div>
                                <!-- Change this to a button or input when using this as a form -->
                                <input type="submit" value="login" class="btn btn-lg btn-success btn-block" />
                            </fieldset>
                        </form>
                    </div>
                </div>
            </div>
        </div>
    </div>
  </div>
</body>

</html>

src/main/resources/templates/sbadmin2.html

  • ログアウトのaタグにhref属性を追加
  • User Profileのリンクにログインしているユーザ名を表示する
                <li class="dropdown">
                    <a class="dropdown-toggle" data-toggle="dropdown" href="#">
                        <i class="fa fa-user fa-fw"></i>  <i class="fa fa-caret-down"></i>
                    </a>
                    <ul class="dropdown-menu dropdown-user">
                        <li><a href="#"><i class="fa fa-user fa-fw"></i> User Profile(<span  th:text="${#authentication.principal.displayName}"></span>)</a>
                        </li>
                        <li><a href="#"><i class="fa fa-gear fa-fw"></i> Settings</a>
                        </li>
                        <li class="divider"></li>
                        <li><a href="login.html" th:href="@{/logout}"><i class="fa fa-sign-out fa-fw"></i> Logout</a>
                        </li>
                    </ul>
                    <!-- /.dropdown-user -->
                </li>
                <!-- /.dropdown -->
````