Tech Hotoke Blog

IT観音とは私のことです。

Vue×SpringでSPA作成2【ログイン機能のカスタマイズ~バックエンド編~】

f:id:TechHotoke:20211216134128p:plain

まえがき

こちらの記事の続編です。 techhotoke.hatenablog.com

目的

VueとSpringで作成したプロジェクトの構築手順の備忘録。

備忘録のため、詳細な説明を省略している部分があります。

前提

基本的なJavaの知識やSpring、Vueの知識があること。

環境

  • Java 11
  • Spring Boot2.5.6
  • Gradle 7.1.1 * Vue3
  • Vue 2.6(Vuetifyが対応していなかったのでバージョン下げました。確認不足・・・)
  • IDESTS

やること

  • 前回はSpring Securityのデフォルト機能しか使っていなかったので、独自認証処理の実装。
  • バックエンド側の処理

実装

まずは独自認証用のUserFormクラスを作成します。

UserForm

public class UserForm {
        private String emailAddress;
        private String password;
     
        public void encrypt(PasswordEncoder encoder){
            this.password = encoder.encode(password);
        }
     
        @Override
        public String toString() {
            return "UserForm{" +
                    "emailAddress='" + emailAddress + '\'' +
                    ", password='" + password + '\'' +
                    '}';
        }
}

今回はemailAddressをユーザーIDとして扱います。

次に、独自認証用の設定を行なっていきます。

SpaAuthenticationFilter

@SuppressWarnings("unused")
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("emailAddress");
        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.getEmailAddress(),
                            userForm.getPassword(),
                            new ArrayList<>())
            );
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

WebSecurityConfig

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
 
    @Autowired
    private DataSource dataSource;
 
    //ユーザIDとパスワードを取得するSQL文
    // 使用可否は全てTRUEで設定
    private static final String USER_SQL = "SELECT "
            + "tmpl_mail AS username, "
            + "tmpl_password AS password, "
            + "true "
            + "FROM tbl_temple "
            + "WHERE tmpl_mail = ?";
 
    private static final String ROLE_SQL = "SELECT "
            + "tmpl_mail AS username, "
            + "role "
            + "FROM tbl_temple "
            + "WHERE tmpl_mail = ?";
 
    @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("/frontend/**").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());
    }
}
  • ログインは/api/loginで実施する。

  • vueは/frontend/配下に配置して認証を必要としない。

  • APIは/api/配下のURLとして認証を必要とする。

  • 認証はCookieを利用する。

  • CSRF対策用にX-CSRF-TOKENをCookieに含める。

次に今回はHTML5 History APIを利用したルーティング機能を使用していきたいので、それに関する設定を行なっていきます。 Springのデフォルト設定での静的リソースへのアクセスはトップページ以外はファイル名を拡張子まで指定しなければアクセスできません。 そのため、トップページ以外へ直接アクセスした場合や、画面リフレッシュ時に正しくページが表示できなくなるため、別途設定が必要です。

Html5HistoryModeResourceConfig

/**
 * 対応しないURLの場合、固定ページを返す。
 */
@Configuration
public class Html5HistoryModeResourceConfig implements WebMvcConfigurer {
 
    private final Resources resourceProperties = new Resources();
 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**") // 全パスをこのリソースハンドラーの処理対象にする
                .addResourceLocations(resourceProperties.getStaticLocations()) // 静的リソース配置先のパスを指定する
                .resourceChain(resourceProperties.getChain().isCache()) // 開発時はfalse、本番はtrueが望ましい。trueにしておくとメモリ上にキャッシュされるためI/Oが軽減される
                .addResolver(new SpaPageResourceResolver()); // 拡張したPathResourceResolverを読み込ませる
    }
 
    public static class SpaPageResourceResolver extends PathResourceResolver {
        @Override
        protected Resource getResource(String resourcePath, Resource location) throws IOException {
            Resource resource = super.getResource(resourcePath, location); // まずはPathResourceResolverで静的リソースを取得する
            return resource != null ? resource : super.getResource("/frontend/index.html", location); // 取得できなかった場合は、index.htmlを返す
        }
    }
}

最後に、ログインフォームからログインが成功した時に返却されるAPIを実装して置きます。 Controllerパッケージ配下にHomeController.javaを作成し、下記のように記述します。

@RestController
@RequestMapping("/api")
public class HomeController {
    
    @GetMapping("/home")
    @ResponseBody
    public String getHomeInfomation(){
        
        String text = "Home画面です";
        
        return text;
    }
}

一旦ここまでの設定を行い、http://localhost:8081/frontendにアクセスしてみるとvueのソースへのアクセスなので、設定を変更したため、今は直接アクセスすることが可能になってるはずです。

一旦今回はここまでで、次回はフロントエンド側の実装を行なっていきます。

ここまで読んで頂きありがとうございます。