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() | cssとjavascript関係を認証状態に関係なく許可する |
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 --> ````