Springで、ログイン画面に入力されたID・パスワードをDBに確認し、ユーザーの権限で特定のURLへのアクセスを禁止する機能を簡単に作成しましょう〜♪
SpringSecurityの認証・認可
-
ログイン機能での認証
- ログインに成功したユーザーだけが、ログイン後の画面に遷移することができる
- Springでは、パスワードの暗号化・復号も簡単
- CSRF対策も可能
-
ログイン機能での認可
- 権限による機能制限
- 権限によって画面表示項目を変更することも可能
ログイン機能・認証機能の内容
- 順番に以下の実装をします
- ログイン画面から入力したユーザーID、パスワードをDBに検索し、登録されていれば、ログインできる
- ユーザー登録の際に、パスワードを暗号化して保存する
- 一般ユーザーはアドミン権限専用画面
http://localhost:8080/admin
にアクセスできない - 一般ユーザーのホーム画面には、アドミン権限専用画面へのリンクを表示しない
アドミン専用画面へのコントローラーを作成
HomeController.java
//省略(全文は下記参考)
@Controller
public class HomeController {
@Autowired
UserService userService;
//省略(全文は下記参考)
/**
* アドミン権限専用画面のGET用メソッド.
* @param model Modelクラス
* @return 画面のテンプレート名
*/
@GetMapping("/admin")
public String getAdmin(Model model) {
//コンテンツ部分にユーザー詳細を表示するための文字列を登録
model.addAttribute("contents", "login/admin :: admin_contents");
//レイアウト用テンプレート
return "login/homeLayout";
}
}
ログイン設定 (直リンク禁止)
- まずは直リンクの禁止から実装します
- ログインしていない状態では、ホーム画面やユーザー管理画面にはアクセスできないようにする(ログイン画面と、ユーザー登録画面以外)
- もし直接アクセスしようとしたら、403エラーで、共通エラーページに遷移
- webjarsやcssなどの静的リソースをセキュリティの対象外とする
セキュリティ設定用クラス
- @EnableWebSecurityを付ける
-
WebSecurityConfigurerAdapterクラスを継承
- 各種メソッドをオーバーライドすることで、セキュリティの設定を行う
- セキュリティ用にBean定義を行うので@Configurationアノテーションを付ける
静的リソースを除外
- webjarsやcssなどの静的リソースには、誰でもアクセスできるようにする
-
web.ignoring().antMatchers("/webjars/∗∗", "/css/∗∗");
-
/∗∗
は、正規表現でいずれかのファイルという意味 - つまり、/webjars配下と/css配下のファイルはセキュリティの対象外
-
直リンクの禁止
- **http.authorizeRequests()**にメソッドチェーンでリンク禁止先の条件を追加
- メソッドチェーンとは、
.
でメソッドを連続して呼び出すこと
- メソッドチェーンとは、
-
antMatchers("<リンク先>").permitAll()
- antMatchersメソッドの引数に、リンク先をセットすると、そのリンク先に対する設定ができる
- リンク先にpermitAllメソッドを使うことで、ログインしてないユーザーでもリンク先にアクセスすることができる=直リンクができる
-
anyRequest().authenticated()
- anyRequestメソッドで、全てのリンク先が対象になり、authenticatedメソッドで、認証しないとアクセスできないように設定する
- permitAllしたリンク先以外の直リンクを禁止できる
- 🌟メソッドチェーンでは上から順番に設定がされていくので
anyRequest.authenticated()
を一番最初に設定すると、そのあとにantMatchers("<リンク先>").permitAll()
を設定しても、全てのリクエストで認証が必要になってしまうので注意
SecurityConfig.java
package com.example.demo;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
//静的リソースへのアクセスには、セキュリティを適用しない
web.ignoring().antMatchers("/webjars/∗∗", "/css/∗∗");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// ログイン不要ページの設定
http
.authorizeRequests()
.antMatchers("/webjars/**").permitAll() //webjarsへアクセス許可
.antMatchers("/css/**").permitAll() //cssへアクセス許可
.antMatchers("/login").permitAll() //ログインページは直リンクOK
.antMatchers("/signup").permitAll() //ユーザー登録画面は直リンクOK
.anyRequest().authenticated(); //それ以外は直リンク禁止
//CSRF対策を無効に設定(一時的)
http.csrf().disable();
}
起動してログイン画面へのアクセスを確認!
- http://localhost:8080/login
- ログイン画面とユーザー登録画面はログインしなくても表示される
- http://localhost:8080/home へアクセス
- 404エラーが帰ってきたので、ホーム画面への直リンクを禁止できました〜^^
- 次回ログイン機能を実装していきます!
(参考)コード全文
HomeController.java
package com.example.demo.login.controller;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.demo.login.domain.model.SignupForm;
import com.example.demo.login.domain.model.User;
import com.example.demo.login.domain.service.UserService;
@Controller
public class HomeController {
@Autowired
UserService userService;
// 結婚ステータスのラジオボタン用変数
private Map<String, String> radioMarriage;
/**
* ラジオボタンの初期化メソッド(ユーザー登録画面と同じ).
*/
private Map<String, String> initRadioMarrige() {
Map<String, String> radio = new LinkedHashMap<>();
// 既婚、未婚をMapに格納
radio.put("既婚", "true");
radio.put("未婚", "false");
return radio;
}
/**
* ホーム画面のGET用メソッド
*/
@GetMapping("/home")
public String getHome(Model model) {
//コンテンツ部分にユーザー詳細を表示するための文字列を登録
model.addAttribute("contents", "login/home :: home_contents");
return "login/homeLayout";
}
/**
* ユーザー一覧画面のGETメソッド用処理.
*/
@GetMapping("/userList")
public String getUserList(Model model) {
//コンテンツ部分にユーザー一覧を表示するための文字列を登録
model.addAttribute("contents", "login/userList :: userList_contents");
//ユーザー一覧の生成
List<User> userList = userService.selectMany();
//Modelにユーザーリストを登録
model.addAttribute("userList", userList);
//データ件数を取得
int count = userService.count();
model.addAttribute("userListCount", count);
return "login/homeLayout";
}
/**
* ユーザー詳細画面のGETメソッド用処理.
*/
@GetMapping("/userDetail/{id:.+}")
public String getUserDetail(@ModelAttribute SignupForm form,
Model model,
@PathVariable("id") String userId) {
// ユーザーID確認(デバッグ)
System.out.println("userId = " + userId);
// コンテンツ部分にユーザー詳細を表示するための文字列を登録
model.addAttribute("contents", "login/userDetail :: userDetail_contents");
// 結婚ステータス用ラジオボタンの初期化
radioMarriage = initRadioMarrige();
// ラジオボタン用のMapをModelに登録
model.addAttribute("radioMarriage", radioMarriage);
// ユーザーIDのチェック
if (userId != null && userId.length() > 0) {
// ユーザー情報を取得
User user = userService.selectOne(userId);
// Userクラスをフォームクラスに変換
form.setUserId(user.getUserId()); //ユーザーID
form.setUserName(user.getUserName()); //ユーザー名
form.setBirthday(user.getBirthday()); //誕生日
form.setAge(user.getAge()); //年齢
form.setMarriage(user.isMarriage()); //結婚ステータス
// Modelに登録
model.addAttribute("signupForm", form);
}
return "login/homeLayout";
}
/**
* ユーザー更新用処理.
*/
@PostMapping(value = "/userDetail", params = "update")
public String postUserDetailUpdate(@ModelAttribute SignupForm form,
Model model) {
System.out.println("更新ボタンの処理");
//Userインスタンスの生成
User user = new User();
//フォームクラスをUserクラスに変換
user.setUserId(form.getUserId());
user.setPassword(form.getPassword());
user.setUserName(form.getUserName());
user.setBirthday(form.getBirthday());
user.setAge(form.getAge());
user.setMarriage(form.isMarriage());
try {
//更新実行
boolean result = userService.updateOne(user);
if (result == true) {
model.addAttribute("result", "更新成功");
} else {
model.addAttribute("result", "更新失敗");
}
} catch(DataAccessException e) {
model.addAttribute("result", "更新失敗(トランザクションテスト)");
}
//ユーザー一覧画面を表示
return getUserList(model);
}
/**
* ユーザー削除用処理.
*/
@PostMapping(value = "/userDetail", params = "delete")
public String postUserDetailDelete(@ModelAttribute SignupForm form,
Model model) {
System.out.println("削除ボタンの処理");
//削除実行
boolean result = userService.deleteOne(form.getUserId());
if (result == true) {
model.addAttribute("result", "削除成功");
} else {
model.addAttribute("result", "削除失敗");
}
//ユーザー一覧画面を表示
return getUserList(model);
}
/**
* ログアウト用処理.
*/
@PostMapping("/logout")
public String postLogout() {
//ログイン画面にリダイレクト
return "redirect:/login";
}
/**
* ユーザー一覧のCSV出力用処理.
*/
@GetMapping("/userList/csv")
public ResponseEntity<byte[]> getUserListCsv(Model model) {
//ユーザーを全件取得して、CSVをサーバーに保存する
userService.userCsvOut();
byte[] bytes = null;
try {
//サーバーに保存されているsample.csvファイルをbyteで取得する
bytes = userService.getFile("sample.csv");
} catch (IOException e) {
e.printStackTrace();
}
//HTTPヘッダーの設定
HttpHeaders header = new HttpHeaders();
header.add("Content-Type", "text/csv; charset=UTF-8");
header.setContentDispositionFormData("filename", "sample.csv");
//sample.csvを戻す
return new ResponseEntity<>(bytes, header, HttpStatus.OK);
}
/**
* アドミン権限専用画面のGET用メソッド.
* @param model Modelクラス
* @return 画面のテンプレート名
*/
@GetMapping("/admin")
public String getAdmin(Model model) {
//コンテンツ部分にユーザー詳細を表示するための文字列を登録
model.addAttribute("contents", "login/admin :: admin_contents");
//レイアウト用テンプレート
return "login/homeLayout";
}
}