AbstractViewを継承してJasperReportでCSVファイルを作成する
CSVファイルをダウンロードする実装として、CSV形式でレンダリングするviewを実装します。
実装
コントローラー
パラメータに応じてデータを検索します。検索結果はbeanのListに設定します。
org.springframework.web.servlet.view.AbstractView
の具象クラスにパラメータとして下のものを渡します。
- JasperReportの
.japser
ファイル - JasperReportのデータソースになる検索結果
- JasperReportのパラメータになるMap
@Controller @RequestMapping("report") public class ReportController { @Autowired TimeEntriesFacade timeEntrysEntriesFacade; /** * 作業時間をCSVにして出力する * * @return * @throws NotFoundException */ @RequestMapping(value = "/timeEntries/{from_date}/{to_date}", method = RequestMethod.GET) public ModelAndView timeEntries( @PathVariable("from_date") @DateTimeFormat(pattern="yyyy-MM-dd")Date from_date , @PathVariable("to_date") @DateTimeFormat(pattern="yyyy-MM-dd")Date to_date) { List<ReportingTimeEntries>csvRows = timeEntrysEntriesFacade.loadEntries(from_date, to_date); final File jasper = JavaUtil.findResourceFile("jasper_report/timeEntries.jasper"); Map<String,Object> reportParams = new HashMap<String,Object>(); ModelAndView mav = new ModelAndView(new JasperReprotCSVView<ReportingTimeEntries>(jasper, reportParams, csvRows, "time_entries.csv")); return mav; } }
AbstractViewの具象クラス
レスポンスに必要なヘッダーを設定し、response.getOutputStream()で得られるoutputStreamにJaspreReportが書き込みます。JaspreReportを利用する実装はCSVクラスに異状します。
import java.io.File; import java.io.OutputStream; import java.util.List; import java.util.Map; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.sf.jasperreports.engine.JRDataSource; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; import org.springframework.web.servlet.view.AbstractView; /** * * Jasper Reportを利用してCSVを出力するView * */ public class JasperReprotCSVView<T> extends AbstractView { /** The content type for an Excel response */ private static final String CONTENT_TYPE = "text/csv;charset=SHIFT-JIS"; /** * jasperファイル */ private File jasperFile ; /** * CSVの元データ */ private List<T> csvRows ; /** JasperReportに渡すパラメター */ private Map<String,Object> reportParams; /** * <code>Content-Disposition: attachment; filename=xxx</coe>に設定するファイル名 */ private String filename ; /** * デフォルトコンストラクター. * Sets the content type of the view to "text/csv;charset=SHIFT-JIS". */ public JasperReprotCSVView() { setContentType(CONTENT_TYPE); } public JasperReprotCSVView(File jasperFile, Map<String,Object> reportParams, List<T> csvRows, String filename){ this(); this.jasperFile = jasperFile; this.csvRows =csvRows; this.reportParams = reportParams; this.filename = filename; } /* (非 Javadoc) * @see org.springframework.web.servlet.view.AbstractView#generatesDownloadContent() */ @Override protected boolean generatesDownloadContent() { return true; } /* (非 Javadoc) * @see org.springframework.web.servlet.view.AbstractView#renderMergedOutputModel(java.util.Map<java.lang.String,java.lang.Object>, javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { if ( jasperFile == null || ! jasperFile.exists()){ // TODO throw exception!! } // Set the content type. response.setContentType(getContentType()); response.setHeader("Content-Disposition","attachment; filename=" + filename); ServletOutputStream out = response.getOutputStream(); write(jasperFile, reportParams, csvRows, out); out.flush(); } /** * CSVを作成ストリームに書き込みます。 * @param jasperFile jasper reportの.jasperファイル * @param reportParams レポートのパラメータ * @param dataSource データソース * @param out CSVを書き込むストリーム * @throws JRException * @throws JasperFileNotFoundException */ void write(File jasperFile, Map<String,Object> reportParams, List<T> dataSource,OutputStream out) throws JRException, JasperFileNotFoundException { write(jasperFile, reportParams, new JRBeanCollectionDataSource(dataSource), out); } /** * CSVを作成ストリームに書き込みます。 * @param jasperFile jasper reportのJRXMLファイル * @param reportParams レポートのパラメータ * @param dataSource データソース * @param out CSVを書き込むストリーム * @throws JRException * @throws JasperFileNotFoundException */ void write(File jasperFile, Map<String,Object> reportParams, JRDataSource dataSource,OutputStream out) throws JRException, JasperFileNotFoundException { new Csv().write(jasperFile, reportParams, dataSource, out); } }
CSVクラスの実装
ダウンロードしたCSVファイルはExcelで利用することを期待しているので、文字コードをShift-JISにし、改行コードをCRLFにします。
import java.io.File; import java.io.OutputStream; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sf.jasperreports.engine.JRDataSource; import net.sf.jasperreports.engine.JREmptyDataSource; import net.sf.jasperreports.engine.JRException; import net.sf.jasperreports.engine.JRParameter; import net.sf.jasperreports.engine.JasperFillManager; import net.sf.jasperreports.engine.JasperPrint; import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource; import net.sf.jasperreports.engine.export.JRCsvExporter; import net.sf.jasperreports.export.SimpleExporterInput; import net.sf.jasperreports.export.SimpleWriterExporterOutput; /** * CSV形式のデータを作成します。 * */ public class Csv { /** * CSVファイルのエンコード */ final String fileEncode = "MS932"; /** * CSVファイルを作成します。 * @param jasperFile jasper reportの.jasperファイル * @param reportParams レポートのパラメータ * @param dataSource データソース * @param out CSVを書き込むストリーム * @throws JRException * @throws JasperFileNotFoundException 引数 jasperFileに指定されたファイルが存在しない */ public <T> void write(File jasperFile, Map<String,Object> reportParams, List<T> dataSource,OutputStream out) throws JRException, JasperFileNotFoundException { write(jasperFile, reportParams, new JRBeanCollectionDataSource(dataSource), out); } /** * CSVファイルを作成します。 * @param jasperFile jasper reportの.jasperファイル * @param reportParams レポートのパラメータ * @param dataSource データソース * @param out CSVを書き込むストリーム * @throws JRException jasper reportの処理で例外が発生した * @throws JasperFileNotFoundException 引数 jasperFileに指定されたファイルが存在しない */ public void write(File jasperFile, Map<String,Object> reportParams, JRDataSource dataSource,OutputStream out) throws JRException, JasperFileNotFoundException { if ( jasperFile == null || ! jasperFile.exists()){ throw new JasperFileNotFoundException(jasperFile); } Map<String, Object> fillParams = new HashMap<String,Object>(reportParams); fillParams.put("SUBREPORT_DIR", jasperFile.getParentFile().getAbsolutePath()+File.separator); fillParams.put(JRParameter.IS_IGNORE_PAGINATION, Boolean.TRUE); JasperPrint jrprint = JasperFillManager.fillReport(jasperFile.getAbsolutePath(), fillParams, dataSource != null ? dataSource : new JREmptyDataSource()); // 改行コードをCRLFにする jrprint.setProperty("net.sf.jasperreports.export.csv.record.delimiter", "\r\n"); JRCsvExporter exporter = new JRCsvExporter(); exporter.setExporterInput(new SimpleExporterInput(jrprint)); exporter.setExporterOutput(new SimpleWriterExporterOutput(out, fileEncode)); exporter.exportReport(); } }
TODO
spring bootを一つのjarにして実行するとhttp://huruyosi.hatenablog.com/entry/2015/12/08/100445.jasper
ファイルの読み込みに失敗する- JasperReportの定義ファイル
.jrxml
から.jasper
へのコンパイルをビルドの一環として組み込む - エラー実装
- PDFやExcelなどにも転用しやすいリファクタリング
- エラーチェックが面倒だからJasperReprotCSVView のコンストラクターで必要なパラメータを受け取っている。これでよいのか?
Redmine 3.1.1.stable と eclipse 4.3を連携させる
環境
- Redmine 3.1.1.stable
- Redmineのプラグイン https://github.com/joaopedrotaveira/redmine_mylyn_connector
- eclipse 4.3 (pleiades-e4.3-java_20140309)
- eclipseのプラグイン https://github.com/ljader/redmine-mylyn-plugin
インストール
Redmine
Redmine3.xに対応したと思われるものを使います。
$ cd $REDMINE_ROOT/plugins $ sudo git clone git://github.com/danmunn/redmine_mylyn_connector.git $ cd .. $ bundle exec rake redmine:plugins:migrate RAILS_ENV=production
eclipse 旧バージョン プラグイン
Redmine-Mylyn Connector download | SourceForge.net にあるプラグインをインストールを使います。
ソフトウェアサイト 「http://redmin-mylyncon.sourceforge.net/update-site/N/」を指定して インストールします。
インストール後に「タスク・リポジトリー」ビューから「タスク・リポジトリーの追加」を選択し、Redmineが表示されるのを確認します。
eclipse 新バージョン プラグイン
メンテナンスされている プラグインを https://github.com/ljader/redmine-mylyn-plugin から インストールします。最近zipファイルのホスティング先を変更したようで、ソースからビルドします。
$ git clone git://github.com/ljader/redmine-mylyn-plugin.git
$ cd redmine-mylyn-plugin
$ mvn package
成功すると、\net.sf.redmine_mylyn.p2repository\target ディレクトリの下にnet.sf.redmine_mylyn.p2repository-0.4.0-SNAPSHOT.zipが作成されます。インストールはソフトウェア・サイトの追加ダイアログの 「アーカイブ」ボタンを押して、zipファイルを選択し、プラグインをインストールします。
mylynにredmineを登録
「タスク・リポジトリー」ビューから「タスク・リポジトリーの追加」を選択し、 コネクタの一覧からRedmineを選択します。
Redmineリポジトリー設定にRedmineの情報を入力します。
- サーバ:RedmineのURL
- ラベル:mylynでタスク・リポジトリーを識別するための名称
- ユーザid:RedmineにログインできるユーザID
- パスワード:上のRedmineのユーザIDのパスワード
- API-Key:Redmineの個人設定画面からAPIキーを発行し、コピペ
Redmineに登録されているチケットのクエリーから利用するものを選択します。
設定が終わると、「タスク・リスト」ビューにクエリーに合致するチケットが表示されます。
所感
チケットを作成するときの担当者をプルダウンで選択できるといいな。
参考にしたサイト
mysql で 最大値を残して delete する
きっかけ
Oracleでは delete from table_a where id < ( select max(id) from table_a )
とやるのだけど、MySQLでは
ERROR 1093 (HY000): You can't specify target table 'table_a' for update in FROM clause
となってしまう。ふと、Oracleの SQL*Plusでは バインド変数を使って手が込んだ削除を行ったのを思い出し、MySQLのユーザ定義変数で同等のことを行うことが思いついた。
連番を振るのに使われるテクニックを応用してみる。
ユーザ定義変数への代入を試す
mysq> select ( @max_v := max(id)) from table_a; +----------------------+ | ( @max_v := max(id)) | +----------------------+ | 334964562 | +----------------------+ 1 row in set (0.05 sec)
となった。ユーザ定義変数max_v
に select の結果が入っているはず。
mysql> select @max_v ; +-----------+ | @max_v | +-----------+ | 334964562 | +-----------+ 1 row in set (0.00 sec)
delete 文にしてみる
mysql> delete from table_a where id < @max_v;
MySQLのダンプファイルを圧縮しながら書き出す
100Gを超えるログテーブルをmysqldump でダンプする時に、名前つきパイプを使うことで圧縮しながらファイルに保存することができます。こうする事で、圧縮前のダンプファイルを保存するためのディスク領域を確保する必要がないので、ディスクの空き領域が少ない時でも大きなテーブルのダンプファイルを作成できます。
ダンプする時
$ mkfifo dmp.pipe $ cat dmp.pipe | gzip -c > xxtable.dmp.gzip & $ mysqldump -u hoge -p dbname -r dmp.pipe
mkfifo
で名前つきパイプをあらかじめ作成しておき、cat
の出力をgzip
の圧縮をバックグランドプロセスで行います。cat
は名前つきパイプdmp.pipeに書き込まれるのを待っています。
その状態でmysqldumpを行い -r
オプションで書き込み先のファイルに名前つきパイプを指定します。mysqldumpは名前つきパイプに書き込みを行い、バックグラウンドで実行しているcat
がmysqldumpが書き込んだ結果を読み込み、無名パイプを使ってgzip
に送ります。gzip
はcat
が送ったmysqldumpの書き込み結果を圧縮し、リダイレクト先のファイルに書き込みます。
mysqldumpが終了するときに名前つきパイプへの書き込みがクローズされ、cat
コマンドが終了し、それを受けてgzip
が終了します。
インポートする時
$ gzcat xxtable.dmp.gzip >> dmp.pipe & $ mysql -u hoge -p dbname < dmp.pipe
ダンプ作成とは逆のことを行います。
gzcat
が名前つきパイプに展開結果を書き込む処理をバックグラウンドで行います。
mysql
の入力のリダイレクトに名前つきパイプを指定することで、gzcat
が展開した結果をmysqlに送ります。
Jasper Reportのクロス集計で横軸が1ページを超えた時には改ページさせる
クロス集計(crosstab)を使った集計を行った結果が横軸が1ページの幅を超えた時に、縦方向に空きがあるとクロス集計が2行になって表示れます。 stackoverflow.com
この場合には、 crosstabのプロパティ「Column Break Offset」を印刷範囲の横幅と同じにすることで、改ページされます。 iReport Designer 5.6のプロパティウィンドウから設定します。
jrxml ではこのような定義です。
<crosstab columnBreakOffset="810" ignoreWidth="false"> <reportElement x="0" y="0" width="801" height="450" uuid="c8bc733f-47b1-4185-b619-2d85e7440535"/> <crosstabHeaderCell> <cellContents> ~ 省略 ~ </crosstab >
spring boot その9 - spring security で Remember-Me認証を行う
実装したソースは https://github.com/huruyosiathatena/springboot/tree/b5e82c5b8e24f4a7508d7fd86ca6cc311fe43adf にあります。
Remember Me 認証
ログイン画面にある にある「Remember Me」にチェックをつけてログインすることで、次回アクセス時に認証を省略できます。
動き
「Remember Me」にチェックをつけてログインを行うと、「remember-me」という名前でcookieが発行されます。cookieにはtokenが設定されており、spring-security内部でログインIDと対応付けられています。
ブラウザを閉じた後に、認証が必要がURLにアクセスした時にremember-meのcookieが送られてきたら spring-securityが対応付けられたログインIDを探して、登録されていれば認証が省略されます。
Remember Meのトークンの管理
デフォルトではMapを用いているorg.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl
クラスが利用されます。この方法では spring bootを再起動すると消えてしまうので、 org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl
クラスを利用して DB に保存します。
DBに保存するためのテーブルは JdbcTokenRepositoryImpl#setCreateTableOnStartup(boolean createTableOnStartup)
にtrueを指定することで自動的に作られます。しかし、テーブルが存在する場合には失敗するので、あらかじめ作成します。
DDLはリファレンス http://docs.spring.io/spring-security/site/docs/3.2.x/reference/htmlsingle/#remember-me-persistent-token に記載されています。
-- remember me 認証 create table persistent_logins( username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null );
認可 - @Secured
@Securedに指定するロールにIS_AUTHENTICATED_REMEMBERED
を指定することでRemember-Meの認証情報でアクセスが許可されます。
値 | 意味 |
---|---|
IS_AUTHENTICATED_ANONYMOUSLY | ログインしていなくても許可する |
IS_AUTHENTICATED_REMEMBERED | Remember-Me認証でアクセスを許可する |
IS_AUTHENTICATED_FULLY | 現在のセッションでログインしていれば許可する。Remember-Me認証でアクセスした場合には否認される。 |
実際にはロール名と併せて@Secured{"IS_AUTHENTICATED_REMEMBERED","ROLE_STAFF"})
という指定になると思います。
認可 - WebSecurityConfigurerAdapter#configure(HttpSecurity http)
HttpSecurityで設定する認証方法の指定も 上と同様に種類を使い分けます。
現在のセッションで認証が必要な場合には.fullyAuthenticated()
を使います。
http .authorizeRequests() .antMatchers("/sbadmin2/css/**").permitAll() .antMatchers("/sbadmin2/js/**").permitAll() .antMatchers("/sbadmin2/bower_components/**").permitAll() .anyRequest().fullyAuthenticated()
Remember-Me認証も許可する場合には.authenticated()
を使います。
http .authorizeRequests() .antMatchers("/sbadmin2/css/**").permitAll() .antMatchers("/sbadmin2/js/**").permitAll() .antMatchers("/sbadmin2/bower_components/**").permitAll() .anyRequest().authenticated()
テンプレートの記述
Remember-Meのチェックボックスは spring-securityの実装(org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices#rememberMeRequested(HttpServletRequest request, String parameter)
)に従う必要があります。
<input name="remember-me" type="checkbox" value="yes" />Remember Me
- name属性はremember-meでなければなりません。
- value属性は下のいずれかでなければなりません。
true
、on
、1
、yes
有効期間
デフォルトでは前回のアクセスから 2 週間は 有効です。変更する場合は WebSecurityConfigurerAdapter#configure(HttpSecurity http)
内でhttp.rememberMe().tokenValiditySeconds(604800)
という要領で指定します。tokenValiditySecondsに秒数を指定します。
テンプレートの実装
<!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-me" type="checkbox" value="yes" />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>
WebSecurityConfigurerAdapter の実装
package com.hatenablog.huruyosi.springboot.config; import javax.sql.DataSource; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; 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.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; 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().authenticated() .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() .and().rememberMe() .tokenRepository(jdbcTokenRepository()) .tokenValiditySeconds(604800) // remember for a week. ( 1 * 60 * 60 * 24 * 7 ) sec ; } @Autowired public void configAuthentication(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService); } /** * Remember Me 認証に利用するトークンのリポジトリ * @return */ @Bean public PersistentTokenRepository jdbcTokenRepository() { JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl(); repository.setDataSource(dataSource); // repository.setCreateTableOnStartup(true); return repository; } }
コントローラーの実装
package com.hatenablog.huruyosi.springboot.controllers; import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.ModelAndView; @Controller @RequestMapping("/sbadmin2/") @Secured("IS_AUTHENTICATED_REMEMBERED") public class SbAdmin2Controller { @RequestMapping("/index.html") public ModelAndView index() { return new ModelAndView("SbAdmin2Controller/index"); } ~ 省略 ~ }
spring boot その8 - spring security で 認可を行う
実装したソースは https://github.com/huruyosiathatena/springboot/tree/8ffe6e7ab202945b9399b1d34eb4462de223dcb1 にあります。
前回( http://huruyosi.hatenablog.com/entry/2015/08/08/003303 ) は認証を行ったので、今回は認可です。
認可の方法
コントローラーに@Secured
アノテーションを指定して、必要な権限名を指定します。
コントローラーのクラスとメソッドのどちらでも指定可能です。メソッドに指定する場合にはWebSecurityConfigurerAdapter
クラスの具象クラスに@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled=true)
の指定が必要です。前回のソースに含まれています。
@Configuration @EnableWebMvcSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled=true) public class WebSecuirty extends WebSecurityConfigurerAdapter { @Autowired private DataSource dataSource; ~省略~
コントローラーの記載
下の記載では、このように振舞います・
- /sbadmin2/index.html はログインしていればアクセス可能
- /sbadmin2/forms.html はログインして ROLE_STAFFの権限を割り当てられていればアクセス可能
- /sbadmin2/tables.html はログインして ROLE_ADMINの権限を割り当てられていればアクセス可能
@Controller @RequestMapping("/sbadmin2/") @Secured("IS_AUTHENTICATED_FULLY") public class SbAdmin2Controller { @RequestMapping("/index.html") public ModelAndView index() { return new ModelAndView("SbAdmin2Controller/index"); } @Secured("ROLE_STAFF") @RequestMapping("/forms.html") public ModelAndView forms() { return new ModelAndView("SbAdmin2Controller/forms"); } @Secured({"ROLE_ADMIN"}) @RequestMapping("/tables.html") public ModelAndView tables() { return new ModelAndView("SbAdmin2Controller/tables" ); }
テンプレートの表示制御
上のコントローラーの例では /sbadmin2/forms.html と /sbadmin2/tables.html は権限が割り当てられている場合にだけアクセスできます。画面の左側のメニューも権限が割り当て割れている場合にだけ表示するようにします。
<li sec:authorize="hasRole('ROLE_ADMIN')"> <a href="tables.html"><i class="fa fa-table fa-fw"></i> Tables</a> </li> <li sec:authorize="hasRole('ROLE_STAFF')"> <a href="forms.html"><i class="fa fa-edit fa-fw"></i> Forms</a> </li>
sec:authorize
で必要な権限名を指定します。sec:authorize
を使うために xml名前空間を追加します。
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/web/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
spring securityが割り当てられている権限名を取得する方法
ログインしたユーザ情報はorg.springframework.security.core.userdetails.UserDetails
から得ます。
getAuthorities()
メソッドの戻り値に許可されている権限の一覧を入れておきます。
今回はユーザ情報を提供するcom.hatenablog.huruyosi.springboot.service.MyUserDetail#create(Users entity)
で DBの authorities テーブルに登録されている 権限名を得られる様にしています。
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.getUserId(), entity.getLoginId(), entity.getPassword(), entity.getDisplayName(), authorities); } /* (非 Javadoc) * @see org.springframework.security.core.userdetails.UserDetails#getAuthorities() */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return this.authorities; } ~以下省略~
実際の画面
画面左側のメニューが変化しているのがわかると思います。
管理者の場合
スタッフの場合
ゲストの場合
課題
spring securityでは権限を階層化して管理することができます。spring security hierarchyで検索するといくつかサイトがでてきます。
クラスレベルではうまくいくのですが、メソッドレベルでは階層化した権限が効きませんでした。デバッグしてみると 権限チェックが2回行われていることがわかりました。
同じ状態で解決されたブログがありした。
Filterレベル(主として認証)の実装は指定した物を参照しているが、 Methodレベル(主として認可)の実装がデフォルト値が利用されているということでしょうか。
SpringSecurityのMethodSecurityとFilterSecurityではまるprepro.wordpress.com
GlobalMethodSecurityConfig で設定していることまでわかったのですが、カスタマイズがうまくいきません。xml を極力書かずに実装したかったので、今回は挫折しました。