前回、Spring boot を使って、かんたんなプロジェクトを作成し、MySQL と MyBatis を使ってデータベースとの連携などについてやってみたわけですが、今回は、Spring Secuity を使ってログインやログアウトの機能を実装してみようと思います。

 

 

Spring Security の説明の前に・・

すでに、過去に掲載した記事の内容に沿って、プロジェクトが作成されて問題なく動作していることが前提になります。


 

Spring Security の準備

 

Security に関連する依存関係を追加

プロジェクトの pom.xml<dependencies>...</dependency> の間に以下の依存関係 (dependency) 追加します。

一つは、Spring Security の Spring boot 向けのパッケージです。もう一つは、Thymeleaf の SpringSecurity 向けのパッケージ。

<!-- Springboot Security -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>

Spring Boot 向けの Spring Security は、オートコンフィギュレーションに対応しているので、これらの依存関係を追加すると、プロジェクトを実行した段階で Spring Boot の「ログイン画面」が表示されます。

Spring Security 標準のログイン画面

Spring Security 標準のログイン画面

 

この段階で、FirstbootappApplication.java@SpringBootApplication アノテーションに、exclude = {SecurityAutoConfiguration.class} を追加することで、Spring Security のオートコンフィグを一時的に無効にすることができます。

@SpringBootApplication(exclude = {SecurityAutoConfiguration.class})
public class FirstbootappApplication {

    public static void main(String[] args) {
        SpringApplication.run(FirstbootappApplication.class, args);
    }
}

ただし、この後いろいろ設定を放り込んでいくと、これだけでは無効にできない場合もあるので注意してください。

 

Java Config で Spring Security を設定する

Spring Security の設定は、従来の XML でも可能ですが、Spring Boot では Java コンフィグ (Java Configuration) で行うのが城跡のようです。

実際には、WebSecurityConfigrerAdapter を継承したクラスで設定します。

設定例は次のとおりです。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    // (1)
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        auth
            .inMemoryAuthentication()
            .withUser("user")
            .password(encoder.encode("password"))
            .roles("USER")
            .and()
            .withUser("admin")
            .password(encoder.encode("password"))
            .roles("USER", "ADMIN");
    }
    // (2)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
            .antMatchers("/admin/**").hasRole("ADMIN")
            .antMatchers("/user/**").authenticated()
            .antMatchers("/loginForm").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/loginForm")
            .usernameParameter("userName")
            .passwordParameter("password")
            .loginProcessingUrl("/login")
            .defaultSuccessUrl("/welcome", true)
            .failureUrl("/loginForm?error=true");
    }
}

 

(1) Authentication Manager

ここでは、シンプルな InMemoryUserDetailsManager を使って Authentication Manager を構築していて、ユーザー名、パスワード、役割 (ロール) をそれぞれ設定しています。

パスワードエンコーダーは、PasswordEncoderFactories.createDelegatingPasswordEncoder() で取得しています。

 

(2) HttpSecurity

HttpSecurity は 特定の HTTP リクエスト に Web ベースのセキュリティを構成します。

 

authorizeRequests()

ここでは次のようなアクセス許可を構成しています。ちなみに、ここでの antMatchers(path) では、パスの形式を Ant形式で記述できます。

antMatchers("/admin/**").hasRole("ADMIN")
リクエストパスが、/admin で始まるパスは、ユーザーが認証されて、なおかつ ADMIN の役割 (ロール) を持っている必要があります。
antMatchers("/user/**").authenticated()
リクエストパスが、/user で始まるパスは、ユーザーが認証されている場合にのみアクセスできます。
antMatchers("/loginForm").permitAll()
リクエストパスが /loginForm の場合、誰でもアクセスができます。
anyRequest().authenticated()
上で指定していないリクエストパスは、ユーザーが認証されている場合にのみアクセスできます。

注意すべき点 としては、より個別なルールほど最初の方に書き、一般的なルールは下の方に書く、ということです。

最後の、anyRequest() ですが、この行より上のルールで指定されていないその他のリクエスト、という意味になり、仮に .permitAll() とすれば、この行より上のルールで指定されていないその他のリクエストは、誰でもアクセスできる、という意味になります。

また、.anonymous() とした場合は、この行より上で指定されていないその他のリクエストは、認証されていない匿名のユーザーのみ アクセスできます。この場合、ユーザーが認証されている場合はアクセスできません

 

formLogin()

formLogin() では、カスタムのログインページとその動作に関する設定をします。

loginPage()
カスタムログインページのビュー名を指定します。このメソッドでビューを指定しない場合、Spring Security のデフォルトのログインページが適用されます。
usernameParameter()
passwordParameter()
フォーム上の、ユーザー名とパスワードの要素のパラメータ名を指定します。要するに、それぞれの input 要素の name 属性の値になります。
loginProcessingUrl()
ログイン画面でフォームを送信する際の URL です。要するに、form 要素の action 属性の値になります。
defaultSuccessUrl()
ログイン認証が成功したときに遷移する先の URL です。ここでは、引数に パスと、true を指定していますが、これは「ユーザーが認証の前に、保護されたページに訪れようとしていたとしても、指定したパスにリダイレクトされる」、と Javadoc にはあります。false を指定した場合は、もしかすると認証成功後に 認証前に訪れようとされていたページにリダイレクトされるという意味なのかもしれません。
failureUrl()
ログイン認証に失敗した場合にリダイレクトするパスを指定します。ここでは、リクエストパス /loginFormerror=true というリクエストパラメータを指定しています。

 

コントローラとビューの作成

それでは、ここで先程のセキュリティの設定に沿って、コントローラとビューの作成をします。

 

コントローラ

何のひねりもないシンプルなコントローラです。特に説明の必要ないですね。

LoginController.java

@Controller
public class LoginController {
    @GetMapping("/loginForm")
    public String loginForm() {
        return "loginForm";
    }
}

このコントローラの loginForm() メソッドはリクエストパスが、/loginForm、リクエストメソッドが GET の時に resources/template にある loginForm.html をビューとして返します。

 

UserController.java

@Controller
@RequestMapping("/user")
public class UserController {
    @GetMapping("")
    public String user() {
        return "user/user";
    }
}

user() メソッドはリクエストパスが、/user、リクエストメソッドが GET の時に resources/template/ にある user/user.html をビューとして返します。

 

AdminController.java

@Controller
@RequestMapping("/admin")
public class AdminController {
    @GetMapping("")
    public String admin() {
        return "admin/admin";
    }
}

admin() メソッドはリクエストパスが、/admin、リクエストメソッドが GET の時に resources/template/ にある admin/admin.html をビューとして返します。

 

WelcomeController.java

@Controller
public class WelcomeController {
    @GetMapping("/welcome")
    public String welcome() {
        return "welcome";
    }
}

welcome() メソッドはリクエストパスが、/welcome、リクエストメソッドが GET の時に resources/template/ にある welcome.html をビューとして返します。

 

ビュー

 

ログインフォーム

ログインフォームを作成します (下のコード参照)。

loginForm.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ログイン</title>
</head>
<body>
<div class="main">
  <div class="col-md-6 col-sm-12">
    <div class="login-form">
        <form th:action="@{/login}" method="POST" id="loginForm">
          <div class="form-group">
              <label for="userId">ユーザーID:</label>
              <input type="text" id="username" name="username" class="form-control" placeholder="ユーザーIDを入力"/>
          </div>
          <div class="form-group">
              <label for="passwd">パスワード:</label>
              <input type="password" id="password" name="password" class="form-control" placeholder="パスワードを入力"/>
          </div>
          <div class="form-group form-check">
              <label class="form-check-label">
                  <input class="form-check-input" type="checkbox" name="remenberMe" id="rememberMe"/>忘れないで!
              </label>
          </div>
          <button type="submit" class="btn btn-danger">Login</button>
          <button type="submit" class="btn btn-blue">Register</button>
      </form>
      <p th:if="${param.error}">エラー</p>
    </div>
  </div>
</div>
</body>
</html>

form 要素の action 属性は、 th:action="@{/login}" のように、前述の Java Config の .loginProcessingUrl("/login") のパスと一致します。

また、ユーザー名とパスワードについても、各 input 要素の name 属性の値は、

.usernameParameter("userName")
.passwordParameter("password")

の値と一致するように注意してください。

後半にある、<p th:if="${param.error}">エラー</p> は、リクエストパラメータ error が存在する場合に、メッセージを出力するようにしている部分です。

なお、このフォームは、Bootstrap4 があると、より綺麗に表示されるように作っていますので、必要な場合は、<head>...</head> 内に次のHTMLコードを追加してください。

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

 

その他のフォーム

残りのフォームです。必要最小限にしています。

admin.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>ADMIN ページ</title>
</head>
<body>
  <h1>Admin</h1>
  <p>
    ここは Admin ページ
  </p>
</body>
</html>

 

user.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>USER ページ</title>
</head>
<body>
  <h1>User</h1>
  <p>
    ここは User ページ
  </p>
</body>
</html>

 

welcome.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Welcome ページ</title>
</head>
<body>
  <h1>Welcome</h1>
  <p>
    ここは Welcome ページ
  </p>
</body>
</html>

 

動作確認

それでは、プロジェクトエクスプローラのプロジェクト名を右クリックして、「Run As」→ 「Spring boot app」で実行し、localhost:8080 にアクセスして次のようなページが表示されたら OK です。

もし、エラーが発生する場合は、コンソールのログを見たり、内容をさらに見直したりしてみてください。

 

Spring Security: ログインフォーム

Spring Security: ログインフォーム

 

ユーザー名とパスワードを ワザ と間違えてみましょう。

Spring Security: ログインエラー

Spring Security: ログインエラー

このように、エラーが表示されます。

 

ログインに成功した場合、次のように Welcome ページが表示されます。

Spring Security: ログインに成功した場合

Spring Security: ログインに成功した場合

 

user でログインして、/admin にアクセスすると、次のように 403 Forbidden になります。

Spring Security: アクセス禁止ページにアクセス

Spring Security: アクセス禁止ページにアクセス

 

まとめ

今回は、Spring Boot と Spring Security によるログイン画面と処理の実装を行ってみました。

Struts などでは、いちいち面倒くさかったログイン画面とログイン処理の実装も、ほとんどコードを書くことなくできるのは魅力的ですね。

でも、もう少し疑問が残ります。

  • ログイン後にプログラマブルにリダイレクト先を指定できないのか?
  • ログインに失敗した場合に独自の処理を追加したい場合は?
  • ログアウト処理は?

これらについては、次回やってみたいと思います。


 

 

zaturendo

中小企業社内SE。

0件のコメント

コメントを残す

アバタープレースホルダー

メールアドレスが公開されることはありません。 が付いている欄は必須項目です