内容
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 |