SpringBootを基づいてのSpringSecurityの導入(管理者、ユーザ分けログイン画面)（第三章）

#０　事前に修正
###0.1 ユーザテーブルの修正
***PASSWORD項目の追加***
***修正後のユーザテーブル***
![19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/8afd5ca2-4c8b-f66d-8bd2-f2ff06c9b5c1.png)

###0.2 その他の修正
***※PASSWORD項目により、下記の対象一覧の修正が必要になる***
***主にPASSWORD項目に対して修正なので、略します***

| 対象一覧         |
|:-----------------|
| UserModel.java   |
| UserForm.java    |
| UserMapper.java  |
| UserService.java |
| NewUser.html     |
| UserList.html    |

#１　Spring Securityの導入
***セッション管理やURL管理の為に導入した***
###1.1 pom.xmlにSpring Security用のdependencyの追加

```ruby:pom.xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
```

###1.2 管理者用ログイン機能追加

#####1.2.1 管理者用ログイン画面作成
```ruby:LoginAdmin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>管理者ログイン画面</title>
  </head>
  <body>
    <h1>管理者ログイン</h1>
    <form action="#" th:action="@{/admin/login}" th:object="${loginForm}" method="post">
      <table>
        <tr>
          <th class="cell_title">ID</th>
          <td><input type="text" th:field="*{Id}"></td>
        </tr>
        <tr>
          <th class="cell_title">パスワード</th>
          <td><input type="password" th:field="*{Password}"></td>
        </tr>
        <tr><td><input type="hidden" name="Admin" value="Admin"></td></tr>
      </table>
      <button type="submit">ログイン</button>
      <input type="button" value="終了" onclick="window.open('about:blank', '_self').close()">
    </form>
  </body>
</html>
```

#####1.2.2 共通用ログインフォームの作成
```ruby:LoginForm.java
package com.example.demo.dto;

import javax.validation.constraints.NotBlank;

import lombok.Data;

@Data
public class LoginForm {
	@NotBlank
	private String Id;

	@NotBlank
	private String Password;

	@NotBlank
	private String Admin;
}
```

#####1.2.3 管理者用ログイン用のWebSecurityConfigurerAdapter仕組の導入
***アクセス制限、ログイン、ログアウト等が設定可能の場所***

```ruby:AdminLoginSecurityConfig.java
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.example.demo.config.loginSecurity.AdminAuthenticationProvider;
import com.example.demo.config.loginSecurity.LoginAuthenticationFilter;

@Configuration
@EnableWebSecurity
@Order(1)
public class AdminLoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	AdminAuthenticationProvider authenticationProvider;

	@Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/images/**", "/css/**", "/javascript/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		http
		.antMatcher("/admin/**")
		.authorizeRequests()
		.anyRequest()
        .hasRole("ADMIN")
			.and()
		.logout()
			.logoutUrl("/admin/logout").permitAll()
			.logoutSuccessUrl("/admin/login");

		http.addFilterBefore(createAdminFilter(), LoginAuthenticationFilter.class);
    }

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

    private LoginAuthenticationFilter createAdminFilter() throws Exception {
    	LoginAuthenticationFilter filter = new LoginAuthenticationFilter();
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/admin/login", "POST"));
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/admin/loginFail"));
        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/admin/loginSuccess"));
        return filter;
    }

}
```

#####1.2.4 共通用のUsernamePasswordAuthenticationFilter仕組の導入
***ログイン情報を保存する場所***

```ruby:LoginAuthenticationFilter.java
package com.example.demo.config.loginSecurity;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.example.demo.dto.LoginForm;

public class LoginAuthenticationFilter extends UsernamePasswordAuthenticationFilter  {

	@Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        LoginForm loginInfo = new LoginForm();
        loginInfo.setId(request.getParameter("Id"));
        loginInfo.setPassword(request.getParameter("Password"));
        loginInfo.setAdmin(request.getParameter("Admin"));

        String password = obtainPassword(request);

        UsernamePasswordAuthenticationToken authRequest =
        		new UsernamePasswordAuthenticationToken(loginInfo, password);

        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
```

#####1.2.5 共通用のAuthenticationProvider仕組の導入
***ログイン成功や権限設定の場所***

```ruby:LoginAuthenticationProvider.java
package com.example.demo.config.loginSecurity;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import com.example.demo.dto.LoginForm;
import com.example.demo.service.UserService;

@Configuration
public class LoginAuthenticationProvider implements AuthenticationProvider {

	@Autowired
	private UserService userService;

	@Override
    public Authentication authenticate(Authentication auth) throws AuthenticationException {

		LoginForm loginInfo = (LoginForm)auth.getPrincipal();
        String password = (String)auth.getCredentials();

        Collection<GrantedAuthority> authorityList = getAuthority(loginInfo);

        if(authorityList.size() == 0) {
            throw new BadCredentialsException("Authentication Error");
        }

        return new UsernamePasswordAuthenticationToken(loginInfo, password, authorityList);
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    private Collection<GrantedAuthority> getAuthority(LoginForm loginInfo) {
    	Collection<GrantedAuthority> authorityList = new ArrayList<>();

    	if(userService.login(loginInfo.getId(), loginInfo.getPassword())) {
    		if (loginInfo.getAdmin().equals("Admin")) {
            	authorityList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
            } else if (loginInfo.getAdmin().equals("User")) {
            	authorityList.add(new SimpleGrantedAuthority("ROLE_USER"));
            }
    	}

    	return authorityList;
    }
}
```
#####1.2.6 ログイン機能のテーブル接続の作成
***ユーザサービスクラスに追加***

```ruby:UserService.java
    public boolean login(String Id, String Password) {
        return dao.login(Id, Password).size() == 1;
    }
```

***ユーザマッパークラスに追加***

```ruby:UserMapper.java
    @Select("SELECT * FROM USER WHERE ID=#{Id} AND PASSWORD=#{Password}")
    List<UserModel> login(String Id, String Password);
```


#####1.2.7 ログイン用のコントローラーを作成
```ruby:LoginController.java
package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.dto.LoginForm;

@Controller
public class LoginController {

    @RequestMapping("/admin/login")
    public String loginAdmin(LoginForm loginForm) {
        return "LoginAdmin.html";
    }

    @RequestMapping("/admin/loginSuccess")
    public String loginSuccessAdmin(LoginForm loginForm) {
        return "redirect:/admin/userlist";
    }
}
```

#####1.2.8 ユーザコントローラーのURLを修正（アクセス制限により）
```ruby:UserController.java
package com.example.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.UncategorizedSQLException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.dto.LoginForm;
import com.example.demo.dto.UserForm;
import com.example.demo.model.UserModel;
import com.example.demo.service.UserService;

@Controller
public class UserController {
	private final UserService userService;

    @Autowired
    public UserController(UserService userService) {
        this.userService = userService;

    }

    @RequestMapping("/admin/new")
    public String addUser(UserForm userForm) {
        return "admin/NewUser.html";
    }

    @PostMapping("/admin/new")
    public String create(@Validated @ModelAttribute UserForm userForm, BindingResult errorResult, Model model) {
		if (errorResult.hasErrors()) {
			return "admin/NewUser.html";
		}

    	try {
    		userService.insert(userForm);
    		return "redirect:userlist";

    	} catch(UncategorizedSQLException e) {
    		FieldError fieldError = new FieldError(errorResult.getObjectName(), "Id", "ユーザ新規作成に失敗");
    		errorResult.addError(fieldError);
    		return "admin/NewUser.html";
    	}
    }

    @GetMapping("/admin/userlist")
    public String displayUsers(Model model) {
    	List<UserModel> users = userService.selectAll();
    	model.addAttribute("users", users);
        return "admin/UserList.html";
    }
}
```

#####1.2.9 ユーザ一覧画面トユーザ新規画面を管理者専用にすることと伴い、ファイルパスの修正
***修正後***
![20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/11cf5607-32b7-1c26-0a71-d2fc9caba90a.png)


###1.3 ユーザ用ログイン機能追加

#####1.2.1 ユーザ用ログイン画面作成
```ruby:LoginUser.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>ユーザログイン画面</title>
  </head>
  <body>
    <h1>ユーザログイン</h1>
    <form action="#" th:action="@{/user/login}" th:object="${loginForm}" method="post">
      <table>
        <tr>
          <th class="cell_title">ID</th>
          <td><input type="text" th:field="*{Id}"></td>
        </tr>
        <tr>
          <th class="cell_title">パスワード</th>
          <td><input type="password" th:field="*{Password}"></td>
        </tr>
        <tr><td><input type="hidden" name="Admin" value="User"></td></tr>
      </table>
      <button type="submit">ログイン</button>
      <input type="button" value="終了" onclick="window.open('about:blank', '_self').close()">
    </form>
  </body>
</html>
```

#####1.3.2 共通用ログインフォームの作成（1.2.2と同様になるので、割合）

#####1.3.3 ユーザ用ログイン用のWebSecurityConfigurerAdapter仕組の導入
***アクセス制限、ログイン、ログアウト等が設定可能の場所***

```ruby:UserLoginSecurityConfig.java
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import com.example.demo.config.loginSecurity.LoginAuthenticationFilter;
import com.example.demo.config.loginSecurity.LoginAuthenticationProvider;

@Configuration
@EnableWebSecurity
@Order(2)
public class UserLoginSecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	LoginAuthenticationProvider authenticationProvider;

	@Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/images/**", "/css/**", "/javascript/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		http
		.antMatcher("/user/**")
		.authorizeRequests()
			.anyRequest()
	        .hasRole("USER")
			.and()
		.logout()
			.logoutUrl("/user/logout").permitAll()
			.logoutSuccessUrl("/user/login");

		http.addFilterBefore(createUserFilter(), LoginAuthenticationFilter.class);
    }

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

    private LoginAuthenticationFilter createUserFilter() throws Exception {
    	LoginAuthenticationFilter filter = new LoginAuthenticationFilter();
        filter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/user/login", "POST"));
        filter.setAuthenticationManager(authenticationManagerBean());
        filter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler("/user/loginFail"));
        filter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/user/loginSuccess"));
        return filter;
    }
}
```

#####1.3.4 共通用のUsernamePasswordAuthenticationFilter仕組の導入（1.2.4と同様になるので、割合）

#####1.3.5 共通用のAuthenticationProvider仕組の導入（1.2.5と同様になるので、割合）

#####1.3.6 ログイン機能のテーブル接続の作成（1.2.6と同様になるので、割合）


#####1.3.7 ユーザログイン用のコントローラーをログインコントローラーに追加
```ruby:LoginController.java
    @RequestMapping("/user/login")
    public String loginUser(LoginForm loginForm) {
        return "LoginUser.html";
    }

    @RequestMapping("/user/loginSuccess")
    public String loginSuccessUser(LoginForm loginForm) {
        return "redirect:/user/userinfo";
    }
```

#####1.3.8 ユーザログイン成功後のコントローラーをユーザコントローラーに追加
```ruby:UserController.java
    @GetMapping("/user/userinfo")
    public String displayUserInfo(Model model) {
    	Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    	LoginForm loginInfo = (LoginForm)auth.getPrincipal();
    	UserModel user = userService.selectUser(loginInfo.getId());
    	model.addAttribute("user", user);
        return "user/UserInfo.html";
    }
```

#####1.3.9 ログインユーザの情報を表示する機能の作成
***ユーザ情報画面作成***
***パス:　template/user/UserInfo.html***

```ruby:UserInfo.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
  <head>
    <meta charset="UTF-8" />
    <title>ユーザ情報</title>
  </head>
  <body>
    <h1>ユーザ情報</h1>
    <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>名前</th>
        <th>メール</th>
      </tr>
    </thead>
    <tbody>
        <tr th:object="${user}">
            <td th:text="*{Id}"></td>
            <td th:text="*{Name}"></td>
            <td th:text="*{Email}"></td>
        </tr>
      </tbody>
  </table>
   <form action="#" th:action="@{/user/logout}" method="post">
      <button type="submit">ログアウト</button>
    </form>
  </body>
</html>
```

***ユーザサービスクラスにユーザ情報取得機能の追加***

```ruby:UserService.java
    public UserModel selectUser(String Id) {
        return dao.selectUser(Id);
    }
```

***ユーザマッパークラスにユーザ情報取得機能の追加***

```ruby:UserMapper.java
    @Select("SELECT * FROM USER WHERE ID=#{Id}")
    UserModel selectUser(String Id);    
```

#２　デモ
###2.1 管理者
#####2.1.1 ログイン
![22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/48f59b43-35f6-ad02-35b2-4bbd35d4ef51.png)
#####2.1.2 ログイン成功（ユーザ一覧表示）
![23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/3ab32954-5da6-d09e-f06c-2dcae77f51c6.png)
#####2.1.3 ログアウト
![24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/0e3fb0cf-9da0-f20a-6bfe-5554b326ad2f.png)

###2.2 ユーザ
#####2.2.1 ログイン
![25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/1e2ed97a-0561-8d0e-5c5c-13338d7dc0f0.png)
#####2.2.2 ログイン成功（ユーザ情報表示）
![26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/0f0ebd97-bdc3-fe47-851c-e3544d030b6f.png)
#####2.2.3 ログアウト
![27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/663377/f0e012eb-4dc1-d66e-1b79-f18707690c53.png)

#３　終わりに
ここまで、SpringSecurityについて、書きました。
前章と比較しながら参照すればよいと思います。