login認証画面も、spring boot + spring security で実装した サンプルコードはよく見かけますが、 SSO認証部分をOpenAM等の別サービスに役割分担したい為、試してみました。
目次
- 参考url
- dir 構成
- pom.xml
- config/WebSecurityConfig.java
- action/*Action.java
- action/ErrorAction.java
- filter/AppPreAuthenticatedFilter.java
- model/User.java
- repository/UserRepository.java
- service/AppUserDetails.java
- service/AppUserDetailsService.java
- resources/application.properties
参考url
今回の http header/cookie認証の為、参考にしました。というより、写経です
以下は、エラー画面表示の参考にしました
以下の 14章 チュートリアルで、まず、Springの基本?を確認しました。
dir 構成
PATH | NOTE |
---|---|
pom.xml | 通常のform認証同様、spring-boot-starter-security 使用 |
config/WebSecurityConfig.java | 認証要のpathや、login filterを指定 |
action/*Action.java | 通常?のspring bootの @Controller です |
action/ErrorAction.java | implements ErrorController で 認証や権限等のerror表示 |
filter/AppPreAuthenticatedFilter.java | login filterです。認証情報取得以外は spring securityに任せます |
model/User.java | @Entity ですが、@Proxy(lazy = false) もつけています |
repository/UserRepository.java | 通常の JpaRepository |
service/AppUserDetails.java | spring security が参照する為、model/User.java を内包 |
service/AppUserDetailsService.java | implements AuthenticationUserDetailsService |
resources/application.properties | postgresへの接続設定のみ記載しています |
pom.xml
余計な dependency も記載していますが、 通常のform認証同様、spring-boot-starter-security 使用がポイントです。
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>jp.end0tknr</groupId> <artifactId>MySpring2</artifactId> <version>0.0.1-SNAPSHOT</version> <name>MySpring2</name> <description>seasar2 to spring</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
config/WebSecurityConfig.java
認証要のpathや、login filterに使用する AppPreAuthenticatedFilter を記載しています。
package jp.end0tknr.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AccountStatusUserDetailsChecker; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import jp.end0tknr.domain.service.AppUserDetailsService; import jp.end0tknr.filter.AppPreAuthenticatedFilter; @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> authenticationUserDetailsService() { return (AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken>) new AppUserDetailsService(); } @Bean public PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() { PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider(); provider. setPreAuthenticatedUserDetailsService(authenticationUserDetailsService()); provider. setUserDetailsChecker(new AccountStatusUserDetailsChecker()); return provider; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.authenticationProvider( preAuthenticatedAuthenticationProvider() ); } @Bean public AbstractPreAuthenticatedProcessingFilter preAuthenticatedProcessingFilter() throws Exception { AppPreAuthenticatedFilter filter = new AppPreAuthenticatedFilter(); filter.setAuthenticationManager(authenticationManager()); return filter; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/", "/public","/error").permitAll() .antMatchers("/admin").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilter(preAuthenticatedProcessingFilter()); } }
action/*Action.java
通常?のspring bootの @Controller です。 今回の認証用に特別な処理は行っていません。
action/PrivateAction.java
package jp.end0tknr.action; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class PrivateAction { @RequestMapping(value = "/private", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("message", "Hello Springboot"); return "private"; } }
templates/private.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>SPRING SECURITY PRIVATE PAGE</title> <meta charset="utf-8" /> </head> <body> <h1>SPRING SECURITY PRIVATE PAGE</h1> <p> HELLO !!! </p> </body> </html>
action/AdminAction.java
package jp.end0tknr.action; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class AdminAction { @RequestMapping(value = "/admin", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("message", "Hello Springboot"); return "admin"; } }
templates/admin.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>SPRING SECURITY ADMIN's PAGE</title> <meta charset="utf-8" /> </head> <body> <h1>SPRING SECURITY ADMIN's PAGE</h1> <p> HELLO !!! </p> </body> </html>
action/PublicAction.java
package jp.end0tknr.action; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @Controller public class PublicAction { @RequestMapping(value = "/public", method = RequestMethod.GET) public String index(Model model) { model.addAttribute("message", "Hello Springboot"); return "public"; } }
templates/public.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>SPRING SECURITY PUBLIC PAGE</title> <meta charset="utf-8" /> </head> <body> <h1>SPRING SECURITY PUBLIC PAGE</h1> <p> HELLO !!! </p> </body> </html>
action/ErrorAction.java
認証エラーが発生すると、/error へ redirectするようですが、 error画面の表示方法を知らなかったので、取り敢えず実装しました。
403だけでなく、404や500エラーも同じ/errorで表示されるはずです。
package jp.end0tknr.action; import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class ErrorAction implements ErrorController { @Override public String getErrorPath() { return "/error"; } @RequestMapping(value = "/error") public String index(Model model) { model.addAttribute("message", "Hello Springboot"); return "error"; } }
templates/error.html
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <title>SPRING SECURITY ERROR PAGE</title> <meta charset="utf-8" /> </head> <body> <h1>SPRING SECURITY ERROR PAGE</h1> <p> HELLO !!! </p> </body> </html>
filter/AppPreAuthenticatedFilter.java
認証情報取得のみ、記載すれば、後は spring securityに任せのようです。
以下では、ユーザIDを固定文字列(aaaaやcccc等)で渡していますが、
SpringBoot for java で、http headerやcookieを取得 - end0tknr's kipple - web写経開発
を参考に、http headerやcookie から取得して下さい。
package jp.end0tknr.filter; import javax.servlet.http.HttpServletRequest; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; public class AppPreAuthenticatedFilter extends AbstractPreAuthenticatedProcessingFilter{ @Override protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { //リクエストからユーザーID等のユーザー情報を抽出 //return "hoge"; //return "aaaa"; return "cccc"; } @Override protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { //リクエストから証明書情報を抽出。DB等にある場合もある? return ""; } }
model/User.java
通常の @Entity では、エラーとなった為、@Proxy(lazy = false) を追加しています。
package jp.end0tknr.domain.model; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import org.hibernate.annotations.Proxy; import lombok.Data; @Entity @Table(name = "usr") @Proxy(lazy = false) @Data public class User { @Id private String userId; private String password; private String firstName; private String lastName; private String roleName; }
myspring=> \d usr Table "public.usr" Column | Type | Collation | Nullable | Default ------------+------------------------+-----------+----------+--------- user_id | character varying(255) | | not null | first_name | character varying(255) | | not null | last_name | character varying(255) | | not null | password | character varying(255) | | not null | role_name | character varying(255) | | not null | Indexes: "usr_pkey" PRIMARY KEY, btree (user_id) Referenced by: TABLE "reservation" CONSTRAINT "fk_recqnfjcp370rygd9hjjxjtg" FOREIGN KEY (user_id) REFERENCES usr(user_id) myspring=> select * from usr; user_id | first_name | last_name | password | role_name -------------+------------+-----------+--------------------------------------+----------- taro-yamada | 太郎 | 山田 | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER aaaa | Aaa | Aaa | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER bbbb | Bbb | Bbb | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | USER cccc | Ccc | Ccc | $2a$10$oxSJl.keBwxms<ないしょ>UTqfTK | ADMIN (4 rows)
repository/UserRepository.java
package jp.end0tknr.domain.repository; import org.springframework.data.jpa.repository.JpaRepository; import jp.end0tknr.domain.model.User; public interface UserRepository extends JpaRepository<User, String> { }
service/AppUserDetails.java
spring security が参照する為、model/User.java を内包し、 spring security の UserDetails が必要とするmethodを追加しています。
package jp.end0tknr.domain.service; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.UserDetails; import jp.end0tknr.domain.model.User; // spring security 認証で使用する user定義。 // jp.end0tknr.domain.model.User を内包。 // Spring徹底入門 14章も参照。 // https://www.shoeisha.co.jp/book/detail/9784798142470 public class AppUserDetails implements UserDetails { private final User user; public AppUserDetails(User user) { this.user = user; } public User getUser() { return user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { // spring securityでは「ROLE_ADMIN」のように「ROLE_」の接頭語が必要な為 // https://qiita.com/yokobonbon/items/7d729bd8085f3fb898bb // https://docs.spring.io/spring-security/site/docs/current/reference/html5 return AuthorityUtils.createAuthorityList("ROLE_" + this.user.getRoleName()); } @Override public String getPassword() { return this.user.getPassword(); } @Override public String getUsername() { return this.user.getUserId(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
service/AppUserDetailsService.java
implements AuthenticationUserDetailsService なserviceクラスです。
package jp.end0tknr.domain.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; import org.springframework.stereotype.Service; import jp.end0tknr.domain.model.User; import jp.end0tknr.domain.repository.UserRepository; @Service public class AppUserDetailsService implements AuthenticationUserDetailsService<PreAuthenticatedAuthenticationToken> { @Autowired UserRepository userRepository; @Override public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) throws UsernameNotFoundException { String userName=(String)token.getPrincipal(); // Object credentials = token.getCredentials(); User user; try { user = userRepository.getOne(userName); } catch (Exception e) { System.out.println("NOT FOUND USER A"); throw new UsernameNotFoundException(userName + " is not found."); } if (user == null) { System.out.println("NOT FOUND USER B"); throw new UsernameNotFoundException(userName + " is not found."); } System.out.println("FOUND USER"); return new AppUserDetails(user); } }
resources/application.properties
spring.jpa.database=POSTGRESQL spring.datasource.url=jdbc:postgresql://192.168.63.3:5432/myspring spring.datasource.username=ないしょ spring.datasource.password=ないしょ