Location via proxy:
[ UP ]
[Report a bug]
[Manage cookies]
No cookies
No scripts
No ads
No referrer
Show this form
Submit Search
Spring bootでweb セキュリティ(ログイン認証)編
•
14 likes
•
32,928 views
なべ
Follow
Spring Securityを用いたログイン認証。
Read less
Read more
1 of 65
Download now
More Related Content
Spring bootでweb セキュリティ(ログイン認証)編
1.
セキュリティ(ログイン認証)編
2.
アジェンダ はじめに 準備
認証方式 ユーザー認証処理 ユーザー認証処理のカスタマイズ パスワードエンコード BCryptを使ったパスワードのハッシュ化 ロール ユニットテスト 静的コンテンツの配置 まとめ
3.
はじめに Spring Securityを用いたログイン認証について説明する
4.
はじめに 使用ライブラリ&バージョン ■Spring Security
4.0
5.
準備 Maven依存関係 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>
6.
準備 設定用クラスの作成 @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { } 設定クラスであることを @Configurationアノテーションで明記 WebMvcSecurityの有効化 既存のAdapterを拡張
7.
準備 設定用クラスの作成 @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { } このクラスに様々な設定を記述する
8.
認証方式・ユーザー認証処理 認証方式とユーザー認証処理を組み合わせる ■認証方式 BASIC認証、Form認証 ■ユーザー認証処理(何を基にユーザー認証をするか) インメモリ(プログラム内固定)、 データベース、LDAP など
9.
認証方式 設定メソッドの上書き @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { } } configureメソッドをオーバーライドし、 引数のHttpSecurityに対して 様々な設定を行う
10.
認証方式 アクセス出来るURLの範囲を決める @Override protected void
configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated(); } HttpSecurityから、 アクセス範囲を決めるクラスを取得 ルート「/」は全ユーザーが アクセス可能 他のURLは認証が必要
11.
認証方式 BASIC認証を使用する @Override protected void
configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated() .and() .httpBasic(); } BASIC認証を使用する
12.
認証方式 ここまでの状態を試す http://localhost:8080/test http://localhost:8080/sample その他のURLでは 全て認証を求められる BASIC認証をするよう実装したので BASIC認証の画面が出る http://localhost:8080/ ルートURLは全ユーザーが アクセス可能 (ただし今は未実装なので Not Foundエラーになる)
13.
認証方式 以下の画面遷移でForm認証を使用する ホーム画面 挨拶画面 【Click here】クリック 参考:Securing
a Web Application http://spring.io/guides/gs/securing-web/ URL: / URL: /hello
14.
認証方式 以下の画面遷移でForm認証を使用する ホーム画面 挨拶画面 【Click here】クリック 参考:Securing
a Web Application http://spring.io/guides/gs/securing-web/ URL: / URL: /hellohttp .authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated(); 下のアクセス制御により、ルート「/」は 全ユーザーがアクセス出来るので 問題なく表示される ただしルート「/」以外は 認証が必要になる。
15.
認証方式 以下の画面遷移でForm認証を使用する ホーム画面 挨拶画面 【Click here】クリック 参考:Securing
a Web Application http://spring.io/guides/gs/securing-web/ URL: / URL: /hello ログイン画面 そのため実際は 間にログイン画面が挟まる
16.
認証方式 ホーム画面を作成する <!DOCTYPE html> <html
xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security Example</title> </head> <body> <h1>Welcome!</h1> <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p> </body> </html> home.html
17.
認証方式 ホーム画面を作成する home.html
18.
認証方式 挨拶画面を作成する <!DOCTYPE html> <html
xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Hello World!</title> </head> <body> <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1> <form th:action="@{/logout}" method="post"> <input type="submit" value="Sign Out"/> </form> </body> </html> hello.html ログインしたユーザー名が 埋め込まれる
19.
認証方式 挨拶画面を作成する hello.html ログインしたユーザー名が 埋め込まれる
20.
認証方式 URLとビューを関連付ける @Configuration public class
MvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/home").setViewName("home"); registry.addViewController("/").setViewName("home"); registry.addViewController("/hello").setViewName("hello"); } } 本来はコントローラを作成してビューを制御するが、 今回は簡易版としてWebMvcConfigurerAdapterを 拡張してURLとビューのテンプレートをお手軽に関連付ける /home もしくは / は home.html を /hello は hello.html を表示する
21.
認証方式 設定メソッドを変更する @Override protected void
configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .anyRequest().authenticated() .and() // .httpBasic(); .formLogin() .loginPage("/login_page") .usernameParameter("username") .passwordParameter("password") .permitAll(); } BASIC認証をやめて Form認証を行う 認証のページは /login_page ユーザー名とパスワードの項目は username と password 全ユーザーのアクセスを許可する
22.
認証方式 URLとビューを関連付ける @Configuration public class
MvcConfig extends WebMvcConfigurerAdapter { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/home").setViewName("home"); registry.addViewController("/").setViewName("home"); registry.addViewController("/hello").setViewName("hello"); registry.addViewController("/login_page").setViewName("login_page"); } } ログインページ用の関連付けを追加
23.
認証方式 ログイン画面を作成する <!DOCTYPE html> <html
xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3"> <head> <title>Spring Security Example </title> </head> <body> <form th:action="@{/login_page}" method="post"> <div><label> User Name : <input type="text" name="username"/> </label></div> <div><label> Password: <input type="password" name="password"/> </label></div> <div><input type="submit" value="Sign In"/></div> </form></body></html> login_page.html ユーザー名とパスワードの項目は username と password
24.
認証方式 ログイン画面を作成する login_page.html
25.
認証方式 ユーザー認証処理に、インメモリ認証を使用する @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { ~~ 中略 ~~ } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { } } HttpSecurityを引数に受けるconfigureメソッドの他に AuthenticationManagerBuilderを受け取るメソッドがあり、 ユーザー認証処理はここに設定を記述する
26.
認証方式 ユーザー認証処理に、インメモリ認証を使用する @Override protected void
configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER").and() .withUser("admin").password("password").roles("USER", "ADMIN"); } } インメモリ認証を行う指定 ユーザーは、user と admin の2つを用意する。 パスワードはいずれも password で、 ロールは user が USERで、adminは USER と ADMIN。 (ロールについては後ほど)
27.
認証方式 動作確認 http://localhost:8080/ user /
password を入力 userが表示される admin / password を入力 admin が表示される
28.
ユーザー認証処理 データベース登録したにログイン情報で認証処理を試す
29.
ユーザー認証処理 接続先をapplication.ymlに記述する spring: datasource: driverClassName: com.mysql.jdbc.Driver url:
jdbc:mysql://xxx.xxx.xxx.xxx/database username: databaseusername password: xxx 接続先に応じて変更を!
30.
ユーザー認証処理 ユーザー認証処理をJDBC方式にする @Autowired private DataSource
dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth // .inMemoryAuthentication() // .withUser("user").password("password").roles("USER").and() // .withUser("admin").password("password").roles("USER", "ADMIN"); .jdbcAuthentication() .dataSource(dataSource); } } コンテナが管理している 接続先情報を取得 JDBC方式を指定する 接続先を指定する
31.
ユーザー認証処理 デフォルトのユーザー情報テーブル create table
users( username varchar_ignorecase(50) not null primary key, password varchar_ignorecase(50) not null, enabled boolean not null ); create table authorities ( username varchar_ignorecase(50) not null, authority varchar_ignorecase(50) not null, constraint fk_authorities_users foreign key(username) references users(username) ); create unique index ix_auth_username on authorities (username,authority); ■Spring Security Reference - 37.1 User Schema より http://docs.spring.io/spring-security/site/docs/4.0.3.CI-SNAPSHOT/reference/htmlsingle/#user-schema
32.
ユーザー認証処理 テスト用のユーザーを登録 insert into
users values('test_user', 'test', true); insert into authorities values('test_user', 'ROLE_USER');
33.
ユーザー認証処理 動作確認 http://localhost:8080/ test_user /
test を入力 test_userが 表示される
34.
ユーザー認証処理のカスタマイズ 自前の認証処理を実装する create table
user_info ( username varchar(50) not null primary key, email varchar(50) not null, password varchar(50) not null, enabled boolean not null, authority varchar(50) not null ); create unique index ix_user_info_email on user_info (email); 以下のテーブルに登録したユーザー情報で認証する テーブルを新規作成 メールアドレスを登録し、 この値とパスワードで 認証する insert into user_info values('test_user', 'test@user', 'test', TRUE, 'ROLE_USER'); サンプルデータ
35.
ユーザー認証処理のカスタマイズ ユーザー情報用のエンティティを作成する @Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name="user_info") public class
UserInfo { @Id @GeneratedValue private String username; @Column(nullable = false) private String email; @Column(nullable = false) private String password; @Column(nullable = false) private Boolean enabled; @Column(nullable = false) private String authority; Lombokを使用 user_infoテーブルの エンティティとして宣言 カラムに応じた フィールドを宣言
36.
ユーザー認証処理のカスタマイズ ユーザー情報用のエンティティを作成する @Data @NoArgsConstructor @AllArgsConstructor @Entity @Table(name="user_info") public class
UserInfo implements UserDetails { ~~ 中略 ~~ public Collection<GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(authority)); return authorities; } UserDetailsに定義された メソッドを実装 (他も同様に) Spring Securityの決まりに従って UserDetailsを実装
37.
ユーザー認証処理のカスタマイズ ユーザー情報用のリポジトリを作成する public interface
UserInfoRepository extends JpaRepository<UserInfo, String> { public UserInfo findByEmail(String email); } メールアドレスで検証する メソッドを宣言 (実装はJPAに任せる) 先ほどのエンティティを扱うリポジトリ
38.
ユーザー認証処理のカスタマイズ 自前の認証処理を実装したServiceクラスを作成する @Service public class
UserInfoService implements UserDetailsService { @Override public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { return null; } } UserDetailsServiceを 実装する UserDetailsServiceに 定義されているメソッドを実装する InMemoryUserDetailsManagerなど 既存の認証処理はこのインターフェースを 実装している
39.
ユーザー認証処理のカスタマイズ 自前の認証処理を実装したServiceクラスを作成する @Override public UserDetails
loadUserByUsername(String s) throws UsernameNotFoundException { if (s==null || "".equals(s)) { throw new UsernameNotFoundException("Username is empty"); } UserInfo userInfo = userInfoRepository.findByEmail(s); if (userInfo == null) { throw new UsernameNotFoundException( "User not found for name: " + s); } return userInfo; } 未入力はエラー 入力されたメールアドレスで検索 検索できなかった場合はエラー 検索できたらそれを返す
40.
ユーザー認証処理のカスタマイズ Serviceクラスを使用して認証するよう設定する @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserInfoService userInfoService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(userInfoService); // .jdbcAuthentication() // .dataSource(dataSource); } 作成したServiceクラスを宣言 JDBC認証から、 作成したServiceクラスでの 認証に切り替える
41.
ユーザー認証処理のカスタマイズ 動作確認 http://localhost:8080/ test@user /
test を入力 test_userが 表示される 認証はメールアドレス で行う ログイン後は ユーザー名を表示
42.
パスワードエンコード パスワードを平文のまま保存するのは危険なのでエンコードして保存し、 認証時に入力されたパスワードをエンコードしてチェックする ログイン画面 平文パスワード エンコード 処理 エンコード済み パスワード データベース 読み込み 処理 エンコード済み パスワード 認証 エンコード済みの パスワードを格納
43.
パスワードエンコード ハッシュ値が入るようテーブルを変更 create table
user_info ( username varchar(50) not null primary key, email varchar(50) not null, password varchar(60) not null, enabled boolean not null, authority varchar(50) not null ); create unique index ix_user_info_email on user_info (email); 以下のテーブルに登録したユーザー情報で認証する insert into user_info values('test_user', 'test@user', '$2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu', TRUE, 'ROLE_USER'); サンプルデータ パスワードのカラムを拡張 「test」のハッシュ値
44.
パスワードエンコード 認証プロバイダを変更 @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserInfoService userInfoService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .authenticationProvider(createAuthProvider()); // .userDetailsService(userInfoService); // .jdbcAuthentication() // .dataSource(dataSource); } 作成したServiceクラスでの認証から、 後述する認証プロバイダに変更する
45.
パスワードエンコード 認証プロバイダを変更 auth .authenticationProvider(createAuthProvider()); // .userDetailsService(userInfoService); //
.jdbcAuthentication() // .dataSource(dataSource); } private AuthenticationProvider createAuthProvider() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userInfoService); authProvider.setPasswordEncoder(new BCryptPasswordEncoder()); return authProvider; } 認証プロバイダの作成 認証処理を実装した Serviceクラスを指定する パスワードのエンコードに BCryptエンコーダを使用する
46.
BCryptを使った パスワードのハッシュ化 BCryptのハッシュ値 $2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu BCryptのハッシュ値 insert into
user_info values('test_user', 'test@user', '$2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu', TRUE, 'ROLE_USER'); サンプルデータ BCryptでハッシュ化したパスワード ヘッダ ソルト値 22文字 ハッシュ値 32文字
47.
BCryptを使った パスワードのハッシュ化 BCryptのヘッダ $2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu ヘッダ ソルト値
22文字 ハッシュ値 32文字 2a → 10 → バージョン番号。 主流は「2a」。 ストレッチング(ハッシュ値の計算を繰り返す)回数の指定。 累乗数を指定する。 10の場合、210なので1,024回繰り返す。
48.
BCryptを使った パスワードのハッシュ化 BCryptのソルト値 $2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu ヘッダ ソルト値
22文字 ハッシュ値 32文字 指定のソルト値とパスワードを基に 指定のストレッチ回数で求めたハッシュ値。
49.
BCryptを使った パスワードのハッシュ化 BCryptのハッシュ値 $2a$10$fms1eItee/kPxD8FSdnk/uO9bC.CBweUrQE.CLRgZgzIGcFJVcLRu ヘッダ ソルト値
22文字 ハッシュ値 32文字 ハッシュ値を求める際に自動生成されるソルト値。
50.
BCryptを使った パスワードのハッシュ化 Spring SecurityでのBCryptのハッシュ値の生成方法 String
hashedPassword = BCrypt.hashpw( rawPassword, BCrypt.gensalt() ); ハッシュ化前のパスワード ソルト値の生成 ハッシュ化したパスワード BCryptクラスを使用
51.
BCryptを使った パスワードのハッシュ化 Spring SecurityでのBCryptのハッシュ値の生成方法 String
hashedPassword = BCrypt.hashpw( “testpassword”, BCrypt.gensalt() ); 上記コードを3回実行した結果 $2a$10$UmuUZJ4D6IaJMm5.UQdsAef2fz6QQuRyRDYrSFab4uNv2SzNEHFW2 $2a$10$.XMgSGCnfecWXh25jKDoZOojlyaMWODJxpoCcOh0YorGM53Vcxq22 $2a$10$1Cc9rpBWu5NenpYameu2f.nHohPeo0HfRLn7dAwKnhQMLkNdibOd6 「testpassword」のハッシュ値を取得するコード ソルト値もハッシュ値も違う 3つの値が生成された
52.
ロール Spring Securityのユーザごとの権限に応じたアクセス制御 UserDetails GrantedAuthority 1 0..* ユーザー名等の情報を保持 そのユーザーの 権限情報を保持 権限情報は 複数持てる
権限情報に応じた アクセス制御を行う
53.
ロール サンプルのテーブル構成と実装 create table
user_info ( username varchar(50) not null primary key, email varchar(50) not null, password varchar(50) not null, enabled boolean not null, authority varchar(50) not null ); 以下のテーブルに登録したユーザー情報で認証する サンプルのテーブルは 1ユーザー1権限 権限を保持するカラム public class UserInfo implements UserDetails { ~~ 中略 ~~ public Collection<GrantedAuthority> getAuthorities() { List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority(authority)); return authorities; } 標準モデルに合わせて リストで返却
54.
ロール サンプルユーザー insert into
user_info values('test_user', 'test@user', 'xxx', TRUE, 'ROLE_USER'); insert into user_info values('admin_user', 'admin@user', 'xxx', TRUE, 'ROLE_ADMIN'); サンプルのユーザー 一般権限ユーザー 管理者権限ユーザー
55.
ロール アクセス出来るURLの範囲を変更する @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .anyRequest().authenticated(); ~~中略~~ 「/admin/」内のURLは 管理者権限ユーザーのみが アセス出来る
56.
ロール 動作確認 管理者ユーザーで ログイン 管理者のみのURLへ アクセス可能 一般ユーザーで ログイン 管理者ではないので 「403
Forbidden」 (閲覧禁止)が返ってくる
57.
ユニットテスト MockMVCを利用してテストを行う @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @SpringApplicationConfiguration(classes =
App.class) public class AppTest { @Autowired private WebApplicationContext context; private MockMvc mockMvc; @Before public void setup() { mockMvc = MockMvcBuilders .webAppContextSetup(context) .build(); }
58.
ユニットテスト MockMVCにSpring Securityの利用を通知 import
static org.springframework.security.test.web.servlet.setup. SecurityMockMvcConfigurers.*; ~~中略~~ public void setup() { mockMvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); } MockMVCに通知
59.
ユニットテスト ログインしないでアクセスするテスト @Test public void
ログインせずにルートページへアクセス() throws Exception { mockMvc.perform(get("/")) .andExpect(status().isOk()); } @Test public void ログインせずにハローページへアクセス() throws Exception { mockMvc.perform(get("/hello")) .andExpect(status().is3xxRedirection()); } ログインせずにルートへアクセスする場合は OKステータスが返ってくる ログインせずにハローページへアクセスする場合は 300番台(移動通知)のどれか (正確には302:Moved Temporarily)が 返ってくる
60.
ユニットテスト ログインしてアクセスするテスト @Test @WithMockUser(username="test@user") public void
ログインしてハローページへアクセス() throws Exception { mockMvc.perform(get("/hello")) .andExpect(status().isOk()); } ログインするユーザを 「WithMockUser」アノテーションで指定 ログインした後なら OKステータスが返ってくる
61.
ユニットテスト 権限を指定してアクセスするテスト @Test @WithMockUser(username="user@user", roles="USER") public
void 一般ユーザーで管理者ページへアクセス() throws Exception { mockMvc.perform(get("/admin/adminPage")) .andExpect(status().is4xxClientError()); } @Test @WithMockUser(username="admin@user", roles="ADMIN") public void 管理者ーザーで管理者ページへアクセス() throws Exception { mockMvc.perform(get("/admin/adminPage")) .andExpect(status().isOk()); } 「WithMockUser」アノテーションに 権限を記述 一般ユーザーには権限がないので 400番台(処理失敗)のどれか (正確には403:Forbidden)が返ってくる 管理者ユーザーならアクセス可能
62.
ユニットテスト Formログインにアクセスするテスト @Test public void
ログインページのテスト() throws Exception { mockMvc.perform( formLogin() .user("username", "test@user") .password("password", "test") .loginProcessingUrl("/login_page") ).andExpect(status().isFound()) .andExpect( authenticated() .withUsername("test_user") .withRoles("USER") ); } Formログインをする 認証に成功し、 ユーザー名とロールが想定通りか チェックする 認証情報(今回はメアド)、 パスワード、ログインURLを指定
63.
静的コンテンツの配置 ログイン前でも画像・CSS・JavaScript等は参照可能とする <div><input type="submit"
value="Sign In"/></div> </form> <img src="/img/test.png"/> ログイン前は何も設定しないと、 静的コンテンツにアクセス出来ない 静的コンテンツは resources/static に配置
64.
静的コンテンツの配置 ログイン前でも画像・CSS・JavaScript等は参照可能とする @Configuration @EnableWebMvcSecurity public class
WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/img/**"); } 除外リストに静的コンテンツを記述 ログイン前でも静的コンテンツへの アクセスが行えるようになった
65.
まとめ 参考 ■Spring Security
Reference http://docs.spring.io/spring-security/site/docs/4.0.3.CI- SNAPSHOT/reference/htmlsingle/ ■Spring Bootでユーザ認証 | memorandum http://ksoichiro.blogspot.jp/2015/03/spring-boot.html ■Spring Boot でログイン画面 + 一覧画面 + 登録画面の Webアプリケーションを作る ( その10 )( ロ グイン画面作成3 ) - かんがるーさんの日記 http://ksby.hatenablog.com/entry/2015/02/08/030648#LoginControllerTest.java ■BCrypt(Blowfish暗号)について調べたので文書化してみました http://www.kamiya54.info/post/100503173956/bcryptblowfish%E6%9A%97%E5%8F %B7%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E8%AA%BF%E3%81%B 9%E3%81%9F%E3%81%AE%E3%81%A7%E6%96%87%E6%9B%B8%E5%8C%96% E3%81%97%E3%81%A6%E3%81%BF%E3%81%BE%E3%81%97%E3%81%9F
Download