目標物
- usernameとpasswordでログイン
- ログインができたら、helloに遷移
- helloではログインusernameを表示する
- ログアウトボタンでログアウトする
今回はvalidationやユーザの新規登録は実装しません。
ログイン⇒ログイン後画面⇒ログアウトのみの簡単な実装となります。
対象者
-
MyBatisをある程度使える(わからない方は↓を参考にしてください)
https://qiita.com/sumichan/items/bdc2a0e909416ae55162 -
SpringSecurityを初めて実装する人
-
簡単なものから初めてみたい人
使っている環境
■OS : Windows10
■IDE : Eclipse
■Maven
■SpringBoot
■DB : MySQL8.0.22
pom.xml
楽ちんなのでlombokを使っています。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/XXXXX?serverTimezone=JST
spring.datasource.username=XXXXXX
spring.datasource.password=XXXXXX
spring.datasource.schema=classpath:schema.sql
spring.datasource.data=classpath:data.sql
spring.datasource.initialization-mode=always
schem.sql
CREATE TABLE IF NOT EXISTS loginUser(
userId BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
password VARCHAR(128) NOT NULL
);
data.sql
パスワードの変換は こちら を使いました。
- username hoge
- password 12345
※今回は簡単なusername,passwordですが、本当に使うときはセキュリティ強化してください。
INSERT INTO loginUser(username, password) VALUES('hoge', '$08$yI3HNSrpzBKPbSUaAtTxB.DgkicwoYkLFoIsupYMV6BqJ1x9zeyHO');
SecurityConfig.java
WebSecurityConfigurerAdapterを継承すると、security関係の設定ができるクラスになります。
- BCryptでパスワードを暗号化します
- セキュリティの設定をします
- 認証方法の設定をします
詳細はコメントアウトの通りです。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Bean //パスワードのエンコード
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override //securityの設定をする
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()//ルール、アクセスポリシーの設定
.antMatchers("/login").permitAll()//loginは認証なしでaccessできる
.anyRequest().authenticated()//↑以外のすべてのURLリクエストをloginしないと見れない
.and()
.formLogin()//ログインの設定
.loginPage("/login")
.defaultSuccessUrl("/hello", true)//ログインが成功したら/helloにいく
.and()
.logout()//ログアウトの設定
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"));//logoutのURLを/logoutにする
}
@Override //認証方法の設定を行う
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
//userDetailsServiceを使って、認証を行う
.userDetailsService(userDetailsService);
}
}
MyUser.java
UserDetailsはインターフェースなので、処理は何も書いていないメソッドだけのクラスです。
なので実装する必要があります。
package com.example.demo.securityUser;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Getter;
import lombok.Setter;
public class MyUser implements UserDetails{
@Getter
@Setter
private String username;
@Getter
@Setter
private String password;
private Collection<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public boolean isAccountNonExpired() {
// TODO 自動生成されたメソッド・スタブ
return true;
}
@Override
public boolean isAccountNonLocked() {
// TODO 自動生成されたメソッド・スタブ
return true;
}
@Override
public boolean isCredentialsNonExpired() {
// TODO 自動生成されたメソッド・スタブ
return true;
}
@Override
public boolean isEnabled() {
// TODO 自動生成されたメソッド・スタブ
return true;
}
}
今回はusernameとpasswordだけしか使わないので、このふたつのgetter,setterを書きます。
その他は今回は使いませんが、実装は必要なのでtrueにしておいてください。
(実装されていないメソッドの追加をすると、勝手に書いてくれます。)
インターフェース UserDetails公式説明
#UserDetailsServiceImpl
UserDetailsServiceを実装したクラスを作る。
必ずloadUserByUsername(String username)
が必要になります。
入力したusernameがDBにあるか探して、UserDetailsに結果を返します。
package com.example.demo.SecurityUserDetails;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.demo.securityMapper.UserMapper;
import com.example.demo.securityUser.MyUser;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired //mapperをインスタンス化。DB接続をするMapperクラスを参照
private UserMapper userMapper;
//loadUserByUsernameは実装が必要
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//findByUsernameで見つけてきたユーザ情報をmyUserに入れる
MyUser myUser = userMapper.findByUsername(username);
//UserDetailsにreturn
return myUser;
}
}
UserMapper.java
Mapperクラスなのでinterfaceにします。
入力されたusernameで、一致したレコードを取ってきています。
package com.example.demo.securityMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.example.demo.securityUser.MyUser;
@Mapper
public interface UserMapper {
//select文。userテーブルから、usernameが一致しているものを検索
@Select("select * from loginuser where username = #{username}")
public MyUser findByUsername(String username);//識別する
}
SecurityController.java
独自のログイン画面に行く指示と、ログインが成功したらhello.htmlに行く指示をしています。
helloに行くときはusernameを表示できるように、Authenticationに入っているログイン情報を"username"に詰めます。
package com.example.demo.securityController;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class SecurityController {
@GetMapping("/login") //configのloginPageでloginに来るように指定したので
public String login() {
return "login";//自作のloginページを表示
}
@GetMapping("/hello") //configのdefaultSuccessUrlでhelloに来るように指定したので
public String hello(Authentication loginUser,Model model) {
/*AuthenticationでログインUserの情報を使うことができるので
modelを使って、"username"にusernameを詰める*/
model.addAttribute("username",loginUser.getName());
return "hello";//helloを表示。usernameをhelloで表示できる
}
}
#login.html
なんでもいいのでform画面を作ります。
th:action="@{/login}" method="post"
は絶対postにしてください。
デフォルトだとgetになってしまっているので、SpringSecurityの認証機能が使えなくなります。
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<div class="wrapper">
<form th:action="@{/login}" method="post" name="Login_Form"
class="form-signin">
<h2 class="form-signin-heading">ログインしてください</h2>
<hr class="colorgraph">
<br>
<input type="text" name="username" placeholder="username" /><br>
<input type="password" name="password" placeholder="password" /> <br>
<button name="Submit" value="Login" type="Submit">Login</button>
</form>
<br>
</div>
</body>
</body>
</html>
#hello.html
なんでもいいのでログイン成功後の画面を作ります。
controllerでusernameに名前を詰めたので、th:text="${username}"
で表示できます。
th:action="@{/logout}"
と書けばログアウトしてくれます。(勝手にやってくれる、すごい)
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Hello</title>
</head>
<body>
<h1>ログイン成功</h1>
<h2><span th:text="${username}" ></span> さんHello!!!!</h2>
<form th:action="@{/logout}" method="post">
<button>Logout</button>
</form>
</body>
</html>
#http://localhost:8080
を実行する
port番号を指定していたら変えてください。
無事にログイン⇒ログアウトできれば成功です!
お疲れ様でした!
#ソースコード
https://github.com/IwaiSumire/trial/tree/main/Qiita210216
#参考
YouTube
書籍:Spring Boot 2 入門: 基礎から実演まで
書籍:Spring解体新書
公式チュートリアル
#はまったところ
- Userクラスをimplementsしたクラスを作っていなかった。(作る意味が分からなかった)
- login.htmlが th:action="@{/login}になっておらず、CSRF対策用になっていなかった(thがなかった。formに書いておくと勝手にCSRF対策してくれるようになる。)
CSRF・・・ログイン情報を保持したまま、悪意のある人がURLをクリックした場合、意図せず情報・リクエスト送信されてしまうこと。
#最後に
スッキリしたログイン機能を作ってみました。
SpringSecurity、裏で色々やってくれすぎて難しいですね。