内容
Vue.js + Spring Boot + HerokuでSPAを作成しました。
Vue.jsとSpring Bootは同じリポジトリ上で管理し、ビルドも同時に実施します。
それにより、最低限のソースコード管理とHerokuへのデプロイ時にまとめてビルドする状態を実現しました。
本記事は第三弾として、Spring Securityを用いた認証認可を紹介します。
Vue.js + Spring Boot + Heroku(はじめに 兼 まとめ)
Vue.js + Spring Boot + Heroku(プロジェクト作成~ビルド)
Vue.js + Spring Boot + Heroku(Herokuへデプロイ〜環境毎プロパティファイル作成)
Vue.js + Spring Boot + Heroku(認証作成 Spring Boot編)
Vue.js + Spring Boot + Heroku(認証作成 Vue編)
Vue.js + Spring Boot + Heroku(サンプルAPI作成)
Spring Securityをmavenに設定
pom.xmlにSpring Securityを設定する。
1 2 3 4 5 6 7 8 9 |
(略) <dependencies> (略) <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> (略) |
IntelliJ右側のMavenタブを開き、Mavenのグルグルマーク(Reload All Maven Projects)をクリック
Spring Securityを設定
Spring Securityの設定をするため、UserForm.java, SpaAuthenticationFilter.javaを作成し、SecurityConfig.javaを設定
・DBはSpringで環境変数毎に異なるDBを設定する。の通り。
・APIは/api/配下のURLとし認証を必要とする。
・vueは/vue/配下に配置して認証を必要としない。
・ログインは/api/loginで実施する。
・認証はCookieを利用する。
・csrf対策用にX-CSRF-TOKENをCookieに含める。
1 2 3 4 5 6 7 |
src/main/java/com/example/demo/ ├── BaseController.java ├── DemoApplication.java ├── Html5HistoryModeResourceConfig.java ├── SecurityConfig.java ├── SpaAuthenticationFilter.java └── UserForm.java |
UserForm.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import lombok.Data; import org.springframework.security.crypto.password.PasswordEncoder; @Data public class UserForm { private String id; private String password; public void encrypt(PasswordEncoder encoder){ this.password = encoder.encode(password); } @Override public String toString() { return "UserForm{" + "id='" + id + '\'' + ", password='" + password + '\'' + '}'; } } |
SpaAuthenticationFilter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; import java.io.IOException; import java.util.ArrayList; public class SpaAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private static final Logger LOGGER = LoggerFactory.getLogger(SpaAuthenticationFilter.class); private AuthenticationManager authenticationManager; private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private DataSource dataSource; public SpaAuthenticationFilter(AuthenticationManager authenticationManager, BCryptPasswordEncoder bCryptPasswordEncoder) { this.authenticationManager = authenticationManager; this.bCryptPasswordEncoder = bCryptPasswordEncoder; // ログイン用のpathを変更する setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/api/login", "POST")); // ログイン用のID/PWのパラメータ名を変更する setUsernameParameter("id"); setPasswordParameter("password"); // ログイン後にリダイレクトのリダイレクトを抑制 this.setAuthenticationSuccessHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK)); // ログイン失敗時のリダイレクト抑制 this.setAuthenticationFailureHandler((req, res, ex) -> res.setStatus(HttpServletResponse.SC_UNAUTHORIZED)); } // 認証の処理 @Override public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) throws AuthenticationException { try { // requestパラメータからユーザ情報を読み取る UserForm userForm = new ObjectMapper().readValue(req.getInputStream(), UserForm.class); return authenticationManager.authenticate( new UsernamePasswordAuthenticationToken( userForm.getId(), userForm.getPassword(), new ArrayList<>()) ); } catch (IOException e) { LOGGER.error(e.getMessage()); throw new RuntimeException(e); } } } |
SecurityConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import javax.servlet.http.HttpServletResponse; import javax.sql.DataSource; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } @Value("${env}") private String env; @Autowired private DataSource dataSource; //ユーザIDとパスワードを取得するSQL文 // 使用可否は全てTRUEで設定 private static final String USER_SQL = "SELECT " + "id AS username, " + "password AS password, " + "true " + "FROM usertbl " + "WHERE id = ?"; private static final String ROLE_SQL = "SELECT " + "id AS username, " + "role " + "FROM usertbl " + "WHERE id = ?"; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery(USER_SQL) .authoritiesByUsernameQuery(ROLE_SQL) .passwordEncoder(bCryptPasswordEncoder()); } @Override protected void configure(HttpSecurity http) throws Exception { http.logout().logoutUrl("/api/logout") .deleteCookies("JSESSIONID") .invalidateHttpSession(true) .logoutSuccessHandler((req, res, auth) -> res.setStatus(HttpServletResponse.SC_OK)) ; http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/api/login").permitAll() .antMatchers("/vue/**").permitAll() .antMatchers("/api/**").authenticated() ; // Spring Securityデフォルトでは、アクセス権限(ROLE)設定したページに未認証状態でアクセスすると403を返すので、 // 401を返すように変更 http.exceptionHandling().authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)); http.addFilter(new SpaAuthenticationFilter(authenticationManager(), bCryptPasswordEncoder())); http.csrf().ignoringAntMatchers("/api/login").csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); } } |
CORSを追加したい場合は下記を追加。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; (略) @Override protected void configure(HttpSecurity http) throws Exception { (略) System.out.println("環境:" + env); if(env.equals("heroku")) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedMethod(CorsConfiguration.ALL); corsConfiguration.addAllowedHeader(CorsConfiguration.ALL); corsConfiguration.addAllowedOrigin("[ドメイン]"); UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource(); corsSource.registerCorsConfiguration("/**", corsConfiguration); // すべてのパスを対象にする http.cors().configurationSource(corsSource); } } |
プロパティファイルでSecure属性を指定
本番環境のみCookieにSecure属性を指定するため、application-heroku.propertiesに追記。
※プロパティファイルの構成はSpringで環境変数毎に異なるDBを設定する。の通りです。
1 |
server.servlet.session.cookie.secure=true |
試験
Spring Boot起動中の場合は一旦終了(■マーク)し、Spring Bootを起動(▶︎マーク)。ターミナルからcurlコマンドでCookieが払い出されることを確認します。
※Springで環境変数毎に異なるDBを設定する。のデータであれば「user1, password」「user2, password」でログインできます。
1 |
$ curl -v -X POST -H "Content-Type: application/json" -d '{"id":"user1", "password":"password"}' localhost:8080/api/login |
コマンド例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
$ curl -v -X POST -H "Content-Type: application/json" -d '{"id":"user1", "password":"password"}' localhost:8080/api/login Note: Unnecessary use of -X or --request, POST is already inferred. * Trying ::1... * TCP_NODELAY set * Connected to localhost (::1) port 8080 (#0) > POST /api/login HTTP/1.1 > Host: localhost:8080 > User-Agent: curl/7.64.1 > Accept: */* > Content-Type: application/json > Content-Length: 37 > * upload completely sent off: 37 out of 37 bytes < HTTP/1.1 200 < Set-Cookie: XSRF-TOKEN=c9d516e8-c353-4ff4-8d98-dddf62fc3494; Path=/ < X-Content-Type-Options: nosniff < X-XSS-Protection: 1; mode=block < Cache-Control: no-cache, no-store, max-age=0, must-revalidate < Pragma: no-cache < Expires: 0 < X-Frame-Options: DENY < Set-Cookie: JSESSIONID=7021A4DBAD2979190FD1D20D45EDB3A4; Path=/; HttpOnly < Content-Length: 0 < Date: Sun, 06 Sep 2020 04:09:28 GMT < * Connection #0 to host localhost left intact * Closing connection 0 |