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 --> ````
Play framework 1.2.7 で “Request exceeds 8192 bytes”が発生した
事象
Play framework 1.2.7 で実装した RestAPIに c# で実装した RestClient から GET /path/to/api?var1=hoge&var2=huga~以下省略
といった要領でQueryStringのパラメータを大量に設定すると logs/application.log に Request exceeds 8192 bytes
と出力され処理が異常終了します。
原因
ログのメッセージを出力しているのは play.server.PlayHandler#exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e)
です。
@Override public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception { try { // If we get a TooLongFrameException, we got a request exceeding 8k. // Log this, we can't call serve500() Throwable t = e.getCause(); if (t instanceof TooLongFrameException) { Logger.error(t, "Request exceeds 8192 bytes"); } e.getChannel().close(); } catch (Exception ex) { } }
ならばと、例外 TooLongFrameException を送出している箇所を探すと、org.jboss.netty.handler.codec.http.HttpMessageDecoder.readLine(ChannelBuffer buffer, int maxLineLength)
でした。例外を送出する時に 変数 sb を確認すると、GET /path/to/api?var1=hoge&var2=huga~以下省略
が設定されていました。
private String readLine(ChannelBuffer buffer, int maxLineLength) throws TooLongFrameException { StringBuilder sb = new StringBuilder(64); int lineLength = 0; while (true) { byte nextByte = buffer.readByte(); if (nextByte == HttpCodecUtil.CR) { nextByte = buffer.readByte(); if (nextByte == HttpCodecUtil.LF) { return sb.toString(); } } else if (nextByte == HttpCodecUtil.LF) { return sb.toString(); } else { if (lineLength >= maxLineLength) { // TODO: Respond with Bad Request and discard the traffic // or close the connection. // No need to notify the upstream handlers - just log. // If decoding a response, just throw an exception. throw new TooLongFrameException( "An HTTP line is larger than " + maxLineLength + " bytes."); } lineLength ++; sb.append((char) nextByte); } } }
上限値の maxLineLength は HttpMessageDecoderのデフォルトコンストラクタで決まっていました。
対策
今回は時間の関係でQueryStringのパラメータ名を短くして対応しました。
が、maxLineLength の上限を引き上げるには play.server.play.server#getPipeline()
メソッドで行っている new HttpRequestDecoder()
に引数を指定すれば上限を引き上げられます。たぶん。
play-1.2.7.jar を作り直す必要があるので、QueryStringのパラメータ名を短くすることにしました。
spring boot その6 - hot deploy
実装したソースは https://github.com/huruyosiathatena/springboot/tree/bf14db6cee0c0eba88894932722a514053f49bbd にあります。
再起動せずにjavaのソースコードの変更を反映
小さな修正を行う都度にjavaを再起動するのは手間なので pom.xmlを編集してhot deployを有効にします。
<dependency> <groupId>org.springframework</groupId> <artifactId>springloaded</artifactId> </dependency>
UltraMonkey-L7 の sslproxy に Logjam 対策を行う
Firefox ver39 で サイトにアクセスできなくなってしまった。
Firefox でアクセスすると
SSL received a weak ephemeral Diffie-Hellman key in Server Key Exchange handshake message. (エラーコード: ssl_error_weak_server_ephemeral_dh_key) というエラーが出るようになりました。
Firefox 39 サイト互換性情報 - Mozilla | MDN の 「1023 ビット未満の DHE キーは受け入れられなくなりました」にしています。
対応
DHE鍵の鍵長が512 だったので 2048 にします。
を参考にして、sslproxy に適用します。
鍵長を 2048 bit にして作成
$ sudo openssl dhparam -out /etc/l7vs/sslproxy/dh2048.pem 2048
/etc/l7vs/sslproxy/sslproxy.target_1.cf を修正
/etc/l7vs/sslproxy/sslproxyadm.cf に sslproxyadm.cf:conf_file = "/etc/l7vs/sslproxy/sslproxy.target_1.cf"
と定義されているので /etc/l7vs/sslproxy/sslproxy.target_1.cf
を修正します。
ssl_options = "SSL_OP_NO_SSLv2" ssl_options = "SSL_OP_NO_SSLv3" tmp_dh_file = "dh2048.pem" cipher_list = "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"
- 上の二つで SSL の v2とv3 の の利用を禁止します。
- tmp_dh_file に 2048bit で作り直したDHE鍵を指定します。
- cipher_list で exportを禁止します。(たぶん)
変更後に再起動して https://weakdh.org/sysadmin.html で自サイトの検査を行い、 Good News! This site uses strong (2048-bit or better) key exchange parameters and is safe from the Logjam attack.
なりました。
spring boot その5 - SB Admin2 を組み込む
実装したソースは https://github.com/huruyosiathatena/springboot/tree/4589c56c81f9907a55356d44704a3ff08235fe39 にあります。
レイアウトを SB Admin2 に変更
レイアウトでもう少し楽するために SB Admin2 を組み込みます。2015年8月2日時点でSB Admin2のバージョンは 1.0.7でした。
SB Admin2から必要なファイルをコピーする方針
startbootstrap-sb-admin-2-1.0.7.zip の内容
SB Admin2 をダウンロードしてzipの中を見ると、 SB Admin2自体と 利用しているコンポーネントが入っていました。
startbootstrap-sb-admin-2-1.0.7 +- bower_components | +- bootstrap | | +- dist | | +- fonts | | +- grunt | | +- js | | +- less | +- bootstrap-social | ~ 省略 ~ + dist | +- css | +- js +- js +- less +- pages +- index.html
Spring Bootのフォルダ構成
public フォルダに sbadmin2 フォルダを作成して、各distフォルダのファイルをコピーします。
SB Admin2の pages/blank.html を元にして layout を作成
まずは、簡単な pages/blank.html を再現しつつ、ブランクページを作成します。
pages/blank.htmlをコピー
startbootstrap-sb-admin-2-1.0.7.zip の pages/blank.html を bootstrap プロジェクトの src/main/resources/templates フォルダに sbadmin2.html という名前でコピーします。
html タグに xmlns を追加
thymeleafを利用するために xmlns を追加します。
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout">
<div>layout:fragment </div> でコンテンツが入る場所を指定する
<div id="page-wrapper">の子にコンテンツを入れます
<!-- Page Content --> <div id="page-wrapper"> <div layout:fragment="content"></div> </div> <!-- /#page-wrapper -->
コントローラー作成
新しく SbAdmin2Controller を作成し、blankアクションを作成します。
package com.hatenablog.huruyosi.springboot.controllers; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/sbadmin2/") public class SbAdmin2Controller { @RequestMapping("/blank") public ModelAndView blank() { return new ModelAndView("SbAdmin2Controller/blank"); } }
テンプレート SbAdmin2Controller/blank.html を作成
新しく src/main/resources/templates/SbAdmin2Controller/blank.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="sbadmin2"> <body> <div layout:fragment="content"> <div class="container-fluid"> <div class="row"> <div class="col-lg-12"> <h1 class="page-header">Blank</h1> </div> <!-- /.col-lg-12 --> </div> <!-- /.row --> </div> <!-- /.container-fluid --> </div> </body> </html>
ブラウザからアクセス
アプリを起動して ブラウザから http://localhost:8080/sbadmin2/blank にアクセスします。 metaタグとinputタグに閉じタグがないのでテンプレートのコンパイルエラーが発生します。 テンプレートモードをLEGACYHTML5することで、閉じタグの修正をせずとも利用できるようすが、今回は閉じタグを追加しました。
閉じタグを直し終わるとがっかりするレイアウトで表示されます。css へのリンクを直していないからです。
css と js のリンクを直しつつ、startbootstrap-sb-admin-2-1.0.7から 依存するjsとcssをコピー
崩れているレイアウトを直していきます。
SB Admin2の css と js
startbootstrap-sb-admin-2-1.0.7/dist の css と js フォルダを プロジェクトの public/sbadmin2 にコピーします。
そして、src/main/resources/templates/sbadmin2.html の cssとjsの読み込みを修正します。
<!-- Custom CSS --> <link th:href="@{/sbadmin2/css/sb-admin-2.css}" rel="stylesheet" />
src/main/resources/templates/sbadmin2.htmlの最後の方にあります。
<!-- Custom Theme JavaScript --> <script th:src="@{/sbadmin2/js/sb-admin-2.js}"></script>
依存するコンポーネント
bower_components フォルダ配下のコンポーネントを直していきます。
startbootstrap-sb-admin-2-1.0.7/bower_components/jquery/dist をプロジェクトの public/sbadmin2/bower_components/jquery へコピーし、 startbootstrap-sb-admin-2-1.0.7/startbootstrap-sb-admin-2-1.0.7/bower_components/bootstrap/dist を プロジェクトの public/sbadmin2/bower_components/bootstrap へコピーします。
そして、src/main/resources/templates/sbadmin2.html のcss と js の読み込みを修正します。
<!-- Bootstrap Core CSS --> <link th:href="@{/sbadmin2/bower_components/bootstrap/css/bootstrap.min.css}" rel="stylesheet" /> ~途中省略 ~ <!-- jQuery --> <script th:src="@{/sbadmin2/bower_components/jquery/jquery.min.js}"></script> <!-- Bootstrap Core JavaScript --> <script th:src="@{/sbadmin2/bower_components/bootstrap/js/bootstrap.min.js}"></script>
再度ブラウザからアクセスします。それっぽい見た目になってきました。
同様の方法で metisMenu と font-awesome を修正します。font-awesome はdist フォルダがないので、cssとfonts フォルダをコピーしました。 これで見られる状態になりました。
プロジェクトの public の状態
blank 以外のページ
作業の流れ
startbootstrap-sb-admin-2-1.0.7/pages のhtmlファイルを組み込んでいきます。
- src/main/resources/templates/SbAdmin2Controller にファイルをコピー
<html>
の属性をblankと同じにします。- link タグで読み込んでいるcss のうち、レイアウトの /src/main/resources/templates/sbadmin2.html で読み込んでいるものを削除し、残ったものはページ固有のcssファイルなので
href
属性をth:href
属性に書き換えます、 - bodyタグの
<div id="page-wrapper">
を<div layout:fragment="content">
に置き換え、親の<div id="wrapper">
の開始タグと終了タグを削除します。 - bodyタグの
nav
タグを削除します。 - bodyタグの最後にある
<script>
タグのうち、レイアウトの /src/main/resources/templates/sbadmin2.html で読み込んでいるものを削除します。残りはページ固有のものなので、レイアウトに反映できるようにします。 - コントローラーにアクションを追加します。
ページ固有のJavaScriptをレイアウトに反映する。
レイアウトの /src/main/resources/templates/sbadmin2.html に JavaScriptを記載するための fragmentを作成します。
<!-- Metis Menu Plugin JavaScript --> <script th:src="@{/sbadmin2/bower_components/metisMenu/metisMenu.min.js}"></script> <div layout:fragment="moreScripts" th:remove="tag"></div> <!-- Custom Theme JavaScript --> <script th:src="@{/sbadmin2/js/sb-admin-2.js}"></script>
JavaScript用のfragmentができたので、コンテンツのテンプレートに記載します。たとえばtales.html ではこの様にしました。
<div layout:fragment="moreScripts"> <!-- DataTables JavaScript --> <script th:src="@{/sbadmin2/bower_components/datatables/media/js/jquery.dataTables.min.js}"></script> <script th:src="@{/sbadmin2/bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js}"></script> <!-- Page-Level Demo Scripts - Tables - Use for reference --> <script> $(document).ready(function() { $('#dataTables-example').DataTable({ responsive: true }); }); </script> </div>
login.html 用のレイアウト
login.html ではナビゲーションを表示しないレイアウトを作成しました。
spring boot その4 - Thymeleaf の layout を利用して、 ページ固有の title、meta、 style、script タグを出力する
実装したソースは https://github.com/huruyosiathatena/springboot/tree/3cf89896ff8e272b437f988f1c39f2d4a988be0e にあります。
layoutを使って 主コンテンツ以外を出力する
一つ前の記事で Thymeleaf のlayoutを使いました。
webページを作成していると、html <div layout:fragment="content">
よりも外側の共通部分にタグを出力する必要が出てきます。
今回は下の4つのタグをページ単位に設定します。
- title タグ
- meta タグ
- style タグ
- script タグ
title タグ
方法1) titleタグのテキストをそのまま反映する
テンプレートの head タグ内に titleタグを記載すれば、 レイアウトのtitleタグが置き換わります。
<!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>a page </title> </head> <body> <div layout:fragment="content"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> </body> </html>
方法2) layout:title-pattern を使う
titleタグに layout:title-pattern 属性を設定して タイトルの形式を指定します。
<title layout:title-pattern="$CONTENT_TITLE - $DECORATOR_TITLE">Starter Template for Bootstrap</title>
とすると、$CONTENT_TITLE には テンプレートの titleタグのテキストが代入され、 $DECORATOR_TITLEには レイアウトの titleタグのテキストが代入されます。
たとえば thymeleaf.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>a page</title> </head> <body> <div layout:fragment="content"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> </body> </html>
と記載すると、出力される html のtitle タグのテキストが a page - Starter Template for Bootstrapになります
metaタグ
コンテンツページに head
タグ内に書きます。
<!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>a page </title> <meta name="description" content="ページ固有の情報を thymeleaf の layout に適用する方法 " /> </head> <body> <div layout:fragment="content"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> </body> </html>
と書くと になります。レイアウトのmetaタグの後ろに挿入されました。
style タグ
metaタグと同様です。
<!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>a page </title> <meta name="description" content="ページ固有の情報を thymeleaf の layout に適用する方法 " /> <style type="text/css"> h1 { text-align: left; } </style> </head> <body> <div layout:fragment="content"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> </body> </html>
と書くと になります。linkタグと script の間に挿入されました。
scriptタグ
metaやstyleと同様に head タグ内に書くのもアリですが、 body 内の最後に配置します。
レイアウトに <div layout:fragment>
を使ってscriptタグを配置する場所を指定します。th:remove="tag"
を指定することで、子要素は残し、<div layout:fragment="moreScripts" th:remove="tag">
のタグだけを削除します。
レイアウト
<!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="/bootstrap/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/assets/js/ie10-viewport-bug-workaround.js"></script> <div layout:fragment="moreScripts" th:remove="tag"> </div> </body>
コンテンツ
<!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>a page </title> <meta name="description" content="ページ固有の情報を thymeleaf の layout に適用する方法 " /> <style type="text/css"> h1 { text-align: left; } </style> </head> <body> <div layout:fragment="content"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> <div layout:fragment="moreScripts"> <script type="text/javascript"> alert("exec moreScripts !!"); </script> </div> </body> </html>
とすると になります。依存関係のために順序指定場合、たとえば、jQueryよりも前に実行したい場合には レイアウトが
<div layout:fragment="beforejQuery" th:remove="tag"> </div> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="/bootstrap/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/assets/js/ie10-viewport-bug-workaround.js"></script> <div layout:fragment="moreScripts" th:remove="tag"> </div> </body>
となる。はず。(未検証)
spring boot その3 - テンプレートエンジンの Thymeleaf を組み込む
実装したソースは https://github.com/huruyosiathatena/springboot/tree/db743062dad8411906e959d00e3326c4249ca50f にあります。
view に Thymeleaf を使う
Tthymeleaf を利用して、 前回( spring boot その2 - bootstrapを組み込んで静的なページを表示する - huruyosi’s blog )の静的ファイルと同等の表示を行います。
pom.xml に依存関係を追加
dependencies に追加します。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
コントローラーにアクションを追加
HelloControllerに thymeleaf メソッドを追加します。 メソッドの戻り値が org.springframework.web.servlet.ModelAndView になり テンプレートの情報を渡します。
@RequestMapping("/thymeleaf") public ModelAndView thymeleaf() { Map<String, String> model = new HashMap<String, String>(); model.put("message", "hello world"); return new ModelAndView("HelloController/thymeleaf", model); }
テンプレートファイルを作成
テンプレートファイルは src/main/resources/templates の下におきます。テンプレートファイルは コントローラーのクラス名のディレクトリを作成し、その下に置きます。今回は src/main/resources/templates/HelloController/thymeleaf.html になります。
テンプレートの<span th:text="${message}">param1 content</span>
にアクションの model.put("message", "hello world");
で設定したhello worldが表示されます。
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- The above 3 meta tags *must* come first in the head; any other head content must come *after* these tags --> <meta name="description" content="" /> <meta name="author" content="" /> <title>Starter Template for Bootstrap</title> <!-- Bootstrap core CSS --> <link href="/bootstrap/css/bootstrap.min.css" rel="stylesheet" /> <!-- Custom styles for this template --> <link href="starter-template.css" rel="stylesheet" /> <!-- Just for debugging purposes. Don't actually copy these 2 lines! --> <!--[if lt IE 9]><script src="/assets/js/ie8-responsive-file-warning.js"></script><![endif]--> <script src="/assets/js/ie-emulation-modes-warning.js"></script> <!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries --> <!--[if lt IE 9]> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <![endif]--> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Project name</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Home</a></li> <li><a href="#about">About</a></li> <li><a href="#contact">Contact</a></li> </ul> </div><!--/.nav-collapse --> </div> </nav> <div class="container"> <div class="starter-template"> <h1>using template ending Thymeleaf </h1> <p class="lead"><span th:text="${message}">param1 content</span></p> </div> </div><!-- /.container --> <!-- Bootstrap core JavaScript ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script> <script src="/bootstrap/js/bootstrap.min.js"></script> <!-- IE10 viewport hack for Surface/desktop Windows 8 bug --> <script src="/assets/js/ie10-viewport-bug-workaround.js"></script> </body> </html>
ホットデブロイとか thymeleaf のキャッシュ
ホットデブロイ
model.put("message", "hello world");
のhello world
を変更した後に画面への表示に反映させるためには再起動を行う必要があります。それでは時間がかかるので eclipseのデバッグから起動すると、ホットデブロイの状態になります。
thymeleaf のキャッシュ
thymeleaf の変更も再起動が必要なので、対策を行います。
thymeleaf のキャッシュを無効にするために application.yml に spring.thymeleaf.cache: false
と定義します。開発中にだけキャッシュを無効にするために 開発用のファイルを作成します。
application.yml
spring: profiles.active: dev
application-dev.yml
spring: thymeleaf.cache: false
ファイルを追加した状態のプロジェクト
src/main/resoruce にテンプレートファイルとapplication.ymlを追加しました。