Spring Securityで独自のパスワード暗号化を使う

#Spring Securityで独自パスワード暗号化を使う方法

Spring Securityでパスワード暗号化をしようとした場合、大抵はSpring Securityで備えられている`PasswordEncoder`クラスを使う方法がよく紹介されています。が、この`PasswordEncoder`クラスはすぐに使えて便利な一方復号化ができないので、DBにこのクラスを使ってパスワードを保存したあと復号化したい際は困ります。

そんなときに独自のパスワード暗号化方法を使うやり方がいまいちヒットしなかったので、備忘録的にもメモを残しておこうと思います。

##PasswordEncoderクラスを実装した独自暗号化クラスを作成
やり方はいたって簡単な`PasswordEncoder`を実装したクラスを使う方法を紹介します。

```OriginalPasswordEncrypter.java
import java.nio.charset.StandardCharsets;
import java.security.spec.KeySpec;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.springframework.security.crypto.password.PasswordEncoder;

public class OriginalPasswordEncrypter implements PasswordEncoder {
	public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
    private KeySpec ks;
    private SecretKeyFactory skf;
    private Cipher cipher;
    byte[] arrayBytes;
    private String myEncryptionKey;
    private String myEncryptionScheme;
    SecretKey key;

//ここのメソッドは使用していないので、overrideしたままです。
    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        return false;
    }

//以下で独自の暗号化メソッドを宣言します。
//暗号化前のパスワードを引数で受け取って暗号化後のパスワードをStringで返します。

    @Override
    public String encode(CharSequence rawPassword) {
    	String encryptedString = null;
        try {
            cipher.init(Cipher.ENCRYPT_MODE, key);
            byte[] plainText = rawPassword.toString().getBytes(StandardCharsets.UTF_8);
            byte[] encryptedText = cipher.doFinal(plainText);
            encryptedString = new String(Base64.encodeBase64(encryptedText));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return encryptedString;
    }

//暗号化前のパスワードと暗号化したパスワードの比較方法メソッド
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.equals(decrypt(encodedPassword));
    }

    public OriginalPasswordEncrypter() throws Exception {
        myEncryptionKey = "ThisIsSpartaThisIsSparta";
        myEncryptionScheme = DESEDE_ENCRYPTION_SCHEME;
        arrayBytes = myEncryptionKey.getBytes(StandardCharsets.UTF_8);
        ks = new DESedeKeySpec(arrayBytes);
        skf = SecretKeyFactory.getInstance(myEncryptionScheme);
        cipher = Cipher.getInstance(myEncryptionScheme);
        key = skf.generateSecret(ks);
    }

    public String decrypt(String encryptedString) {
        String decryptedText=null;
        try {
            cipher.init(Cipher.DECRYPT_MODE, key);
            byte[] encryptedText = Base64.decodeBase64(encryptedString);
            byte[] plainText = cipher.doFinal(encryptedText);
            decryptedText= new String(plainText);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return decryptedText;
    }
}
```

ちなみに暗号化方法は色々とあると思うので、暗号化メソッドは[stackoverflow](https://stackoverflow.com/questions/10303767/encrypt-and-decrypt-in-java)からお借りしました。

次に、SpringSecurityのセキュリティメソッドを実装しているクラスで設定します。

```SecurityConfig.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.basiauth.basiauth.service.LoginUserDetailsServiceImpl;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled=true,securedEnabled=true,jsr250Enabled=true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	@Qualifier("SuccessHandler")
	private AuthenticationSuccessHandler successHandler;

    @Autowired
    private LoginUserDetailsServiceImpl userService;

//DaoAuthenticationProviderをBeanで設定し、ここで先程の独自のパスワード暗号化メソッドを設定します。
    @Bean
    public DaoAuthenticationProvider authProvider() throws Exception {
    	DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
    	authProvider.setUserDetailsService(userService);
    	authProvider.setPasswordEncoder(new OriginalPasswordEncrypter());
    	return authProvider;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    	auth.authenticationProvider(authProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .authorizeRequests()
                .antMatchers("/login").permitAll() 
                .antMatchers("/register").permitAll()
                .antMatchers("/error/session").permitAll()
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll()
                .antMatchers("/webjars/*").permitAll()
                .antMatchers("/admin").hasAnyAuthority("ROLE_ADMIN")
                .antMatchers("/user").hasAnyAuthority("ROLE_ADMIN","ROLE_GENERAL")
                .anyRequest().authenticated(); 


        http
            .formLogin()
                .loginProcessingUrl("/login")
                .loginPage("/login")
                .failureUrl("/login?error")
                .usernameParameter("userId")
                .passwordParameter("password")
                .defaultSuccessUrl("/home", true)
                .successHandler(successHandler); //成功時のハンドラー追加

        http
            .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutUrl("/logout")
                .logoutSuccessUrl("/login");

    }
}
```

あとはDBなどのパスワードを保存する側に予め暗号化したパスワードを格納しておけばOKです。

意外と簡単にできました。