Thymeleafコアには含まれない、spring-securityとThymeleafを結び付けて使うためのThymeleaf Extrasモジュールの基本的な使い方に関するメモ。基本的に、このエントリは https://github.com/thymeleaf/thymeleaf-extras-springsecurity のドキュメントを基にしている。このエントリ中で単に「ドキュメント」と書く場合、このURLを指す。
なおACL関連は準備が面倒なので省略している。
基本的な使い方
spring-bootでSpring MVCを想定して進める。pom.xmlにthymeleaf-extras-springsecurity[3|4|5]を追加する。
pom.xml
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.0.RELEASE</version> <relativePath /> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>13</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> </dependencies>
両ライブラリのversionを合わせる必要があるが余り心配しなくても良い
Spring Securityとthymeleaf-extras-springsecurity[3|4|5]のバージョンは合わせる必要があるが、spring-bootの場合はあまり神経質になる必要無い。その理由はspring-boot側にversion指定があるため。たとえば、spring-boot-2.2.0ではspring-security-coreは5.2.0なのでthymeleaf-extras-springsecurity5を使う必要があるが、誤って以下のように4を指定したとする。
<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity4</artifactId> </dependency>
上記のように誤った指定をした場合、mavenで以下のようにビルドエラーとなる*1。
[ERROR] 'dependencies.dependency.version' for org.thymeleaf.extras:thymeleaf-extras-springsecurity4:jar is missing. @ line 43, column 15
java
起動用クラス。
import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; @SpringBootApplication public class App { public static void main(String[] args) { new SpringApplicationBuilder(App.class).web(WebApplicationType.SERVLET).run(args); } }
サンプル表示用に適当な認証・認可を返す認証プロバイダを作る。自前のAuthenticationProvider
を用意してAuthentication
のprincipal
とかsetDetails
とかにサンプル用の適当な名前をつめる。
import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collection; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Component; @Component public class MyAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Collection<? extends GrantedAuthority> authorities = Arrays.asList( new SimpleGrantedAuthority("AUTH001"), new SimpleGrantedAuthority("AUTH002"), new SimpleGrantedAuthority("ADMIN"), new SimpleGrantedAuthority("ROLE_ADMIN")); MyUsernamePasswordAuthenticationToken token = new MyUsernamePasswordAuthenticationToken("kagami", "hoge", authorities); MyDetail details = new MyDetail(); details.hogeValue = "hogeValue"; details.hogeList = Arrays.asList("aaa", "bbb", "ccc"); token.setDetails(details); token.setSampleLocalDateTime(LocalDateTime.now()); return token; } @Override public boolean supports(Class<?> authentication) { return true; } }
サンプル表示用のdetailクラス。
public class MyDetail { String hogeValue; List<String> hogeList; // getter/setter省略 }
以降のサンプルコードは上記のjavaを前提とする。
html
ログイン後の画面/src/main/resources/templates/index.html
にあれこれ表示する。
<!doctype html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <head> <meta charset="UTF-8" /> <title>Hello Thymeleaf</title> </head> <body> <div th:text="${#authentication.principal}"> Authentication.getPrincipal() </div> <div th:text="${#authentication.details.hogeValue}"> Authentication.getDetails().getHogeValue() </div> <div th:text="${#authentication.sampleLocalDateTime}"> MyUsernamePasswordAuthenticationToken.getSampleLocalDateTime() </div> </body> </html>
xmlのNamespace
<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
https://github.com/thymeleaf/thymeleaf-extras-springsecurity によると、namespaceは全バージョンで共通。無かったり間違っててもテンプレートの処理に支障は無いが、IDEの補間とかに影響が出るかもしれない、と書いてある。
使い方
ドキュメントによると、expression utility objectsというSpring Securityのオブジェクトを使うものと、attributesという属性を使うものがある。両者に本質的な差は無く、基本的には同じ事を異なる書き方で書ける、というだけ。ただし、両者でやれる事に若干差はある。
Spring Securityのオブジェクト
#authentication
で認証、#authorization
で認可、のオブジェクトにアクセスできる。基本的には、Thymeleafの他機能とかSpELとかと組み合わせて使用する。
authenticationオブジェクト
これは、例えば以下のように使用する。
<div th:text="${#authentication.principal}"> Authentication.getPrincipal() </div> <div th:text="${#authentication.details.hogeValue}"> Authentication.getDetails().getHogeValue() </div> <div th:text="${#authentication.sampleLocalDateTime}"> MyUsernamePasswordAuthenticationToken.getSampleLocalDateTime() </div>
このエントリでは認証プロバイダでUsernamePasswordAuthenticationToken
を拡張したMyUsernamePasswordAuthenticationToken
を返しているので、そのプロパティにもアクセスできる*2。
authorizationオブジェクト
これは、基本的には、th:if
かth:unless
と組み合わせて画面表示の分岐処理に使用する。
<div th:if="${#authorization.expression('hasRole(''ADMIN'')')}"> ADMIN roleがある。 </div> <div th:unless="${#authorization.expression('hasAuthority(''NOT-HAS-AUTH'')')}"> NOT-HAS-AUTH authorityが無い。 </div>
この#authorization
の実際のクラスはorg.thymeleaf.extras.springsecurity[3|4|5].auth.Authorization
なので、使用可能なメソッドはそちらを確認する。とはいえ、spring-securityのビルトイン関数を呼ぶためのexpression
以外は余り使う機会は無い気がするが、複雑な処理を記述する場合にはgetAuthentication()
で認証オブジェクトにアクセスしたり、IExpressionContext getContext()
が重要なケースが出てきたりするかもしれない。
ビルトイン関数については後述する。
属性
上述のように、#authentication
オブジェクトとec:authentication
属性と書く事に基本的な差は無い。こちらの書き方の方が記述的で分かりやすい、ような気がする。
sec:authentication
以下は、authenticationオブジェクトの基本的な使い方のコード例と同一の出力結果となる。
<div sec:authentication="principal"> Authentication.getPrincipal() </div> <div sec:authentication="details.hogeValue"> Authentication.getDetails().getHogeValue() </div> <div sec:authentication="sampleLocalDateTime"> MyUsernamePasswordAuthenticationToken.getSampleLocalDateTime() </div>
sec:authorize
基本的には、th:if
+ #authorization.expression(...)
のエイリアスと考えて良い。認証や認可が何らかの条件の時だけ要素を出す、が良くある使い方になる。
<div sec:authorize="hasAuthority('ADMIN')"> ADMINのauthorityがある。 </div> <div sec:authorize="principal == 'principal'"> principalが'principal'と一致する。 </div>
SpELの他のオブジェクト、たとえば#vars
, #httpSession
、と組み合わせて使う場合は以下のようにする。いま、/sample
が以下のようなコードとして、
@GetMapping("/sample") public ModelAndView hoge(HttpSession session) { session.setAttribute("name1", "ADMIN"); ModelAndView model = new ModelAndView("index"); model.addObject("name", "ADMIN"); return model; }
sessionやModelAndView
の値とsec:authorize
を組み合わせるには以下のようにする。
<div sec:authorize="${hasAuthority(#vars.name)}"> ModelAndViewのnameの値(=ADMIN)のauthorityがある </div> <div sec:authorize="${hasAuthority(#httpSession.getAttribute('name1'))}"> HttpSessionのname1の値(=ADMIN)のauthorityがある </div>
sec:authorize-url
ユーザが指定のURLにアクセス可能かどうか、をチェックする。configが以下のようになっている、とする。
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // .antMatchers("/").permitAll() // .antMatchers("/admin").hasAuthority("ADMIN") // .antMatchers(HttpMethod.POST, "/admin-post-only").hasAuthority("ADMIN") // .anyRequest().authenticated(); http .formLogin() .and() .httpBasic(); } }
<div sec:authorize-url="/admin"> /adminにアクセス可能な認可がある。 </div>
HTTPメソッドも指定できる。なお未指定時のデフォルトはGET
。
<div sec:authorize-url="POST /admin-post-only"> POST /admin-post-onlyにアクセス可能な認可がある。 </div>
Spring Securityのビルトイン関数
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#el-common-built-in に一覧がある。また、対応クラスはorg.springframework.security.access.expression.SecurityExpressionRoot
なので必要に応じてソースも参照する。
以下は関数一覧の抄訳。その後に使用例のサンプルコードも示す。
Expression | Description |
---|---|
hasRole([role]) |
現在のprincipalがそのroleを持つ場合にtrue を返します。デフォルトでは、引数のroleの先頭が'ROLE'で開始しない場合'ROLE'を追加します。この挙動はDefaultWebSecurityExpressionHandler のdefaultRolePrefix でカスタマイズ可能です。 |
hasAnyRole([role1,role2]) |
現在のprincipalがroleのいずれか1つ以上のrole(カンマ区切りの文字列リスト)を持つ場合にtrue を返します。デフォルトでは、引数のroleの先頭が'ROLE'で開始しない場合'ROLE'を追加します。この挙動はDefaultWebSecurityExpressionHandler のdefaultRolePrefix でカスタマイズ可能です。 |
hasAuthority([authority]) |
現在のprincipalがauthorityを持つ場合にtrue を返します。 |
hasAnyAuthority([authority1,authority2]) |
現在のprincipalがいずれか1つ以上のauthorities(カンマ区切りの文字列リスト)を持つ場合にtrue を返します。 |
principal |
現在のユーザを表すprincipalオブジェクト。 |
authentication |
SecurityContext から取得する現在のAuthentication 。 |
permitAll |
常にtrue |
denyAll |
常にfales |
isAnonymous() |
現在のprincipalがanonymousの場合にtrue を返します。 |
isRememberMe() |
現在のprincipalがremember-meの場合にtrue を返します。 |
isAuthenticated() |
ユーザが非anonymousの場合にtrue を返します。 |
isFullyAuthenticated() |
ユーザが非anonymous and 非remember-meの場合にtrue を返します。 |
hasPermission(Object target, Object permission) |
ユーザがtargetにpermissionのアクセス権を持つ場合にtrue を返します。例:hasPermission(domainObject, 'read') |
hasPermission(Object targetId, String targetType, Object permission) |
ユーザがtargetにpermissionsのアクセス権を持つ場合にtrue を返します。hasPermission(1, 'com.example.domain.Message', 'read') |
hasRole([role])
<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}"> ADMIN roleがある。 </div> <div th:if="${#authorization.expression('hasRole(''ADMIN'')')}"> ADMIN roleがある。 </div>
hasAnyRole([role1,role2])
<div th:if="${#authorization.expression('hasAnyRole(''ADMIN'', ''HOGE'')')}"> ADMIN, HOGE のいずれかのroleがある。 </div>
hasAuthority([authority])
<div th:if="${#authorization.expression('hasAuthority(''AUTH001'')')}"> AUTH001のauthorityがある。 </div>
hasAuthorityのAND
詳細はSpring Expression Language(SpEL式)だがandを取ればよい。
<div th:if="${#authorization.expression('hasAuthority(''AUTH001'') and hasAuthority(''AUTH002'')')}"> AUTH001 and AUTH002 のauthorityがある。 </div>
hasAnyAuthority([authority1,authority2])
<div th:if="${#authorization.expression('hasAnyAuthority(''AUTH001'', ''HOGE'')')}"> AUTH001, HOGE のいずれかのauthorityがある。 </div>
principal
<div th:if="${#authorization.expression('principal == ''kagami''')}"> principalがkagami </div>
authentication
<div th:if="${#authorization.expression('authentication.details.hogeValue == ''hogeValue''')}"> Authentication.getDetails().getHogeValue()がhogeValue </div>
permitAll
<div th:text="${#authorization.expression('permitAll')}">常にtrue</div>
denyAll
<div th:text="${#authorization.expression('denyAll')}">常にfalse</div>
isAnonymous()
<div th:text="${#authorization.expression('isAnonymous()')}">anonymousユーザならtrue</div>
isRememberMe()
<div th:text="${#authorization.expression('isRememberMe()')}">remember-meユーザならtrue</div>
isAuthenticated()
<div th:text="${#authorization.expression('isAuthenticated()')}">not anonymousユーザならtrue</div>
isFullyAuthenticated()
<div th:text="${#authorization.expression('isFullyAuthenticated()')}">not anonymous and not remember-meユーザならtrue</div>