Spring Security を使用して既存の REST API プロジェクトに Basic 認証を追加する。

# はじめに
Spring Security を使用して既存の REST API プロジェクトに Basic 認証を追加することを目的としています。
[Spring Boot を使用して REST API を作成する](https://qiita.com/ryu0114h/items/b0c82af9d8a05fdbf1e0)という記事の続きとなっております。

# エンドポイント
今回作成するアプリケーションのエンドポイントは以下のようになります。

| メソッド  |  URL  | 処理内容 | 認証 | 
| ---- | ---- | ---- | ---- | 
| POST | /register | 従業員の作成 | ❌ | 
| POST | /login | ログイン | ❌ |
| GET | /users | ユーザー一覧を取得 | ⭕️ |
| GET | /users/:id | ユーザーを取得 | ⭕️ |
| POST | /users | ユーザーを作成 | ⭕️ |
| PATCH | /users/:id | ユーザーを変更 | ⭕️ |
| DELETE | /users/:id | ユーザーを削除 | ⭕️ |

# 認証認可の実装
## pom.xmlにspring-securityを追加
```:pom.xml
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-security</artifactId>
	</dependency>
```

## 認証させるEmployeeクラスを作成
```:Employee.java
package com.example.springBootRestApi.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.validation.constraints.Email;
import lombok.Data;

@Entity
@Table(name = "employee")
@Data
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Email
    private String email;

    private String password;
}

```

## Configファイル
- Basic認証を実現するためにWebSecurityConfigurerAdapterを実装したクラスを作成します。
  - /login と /register は誰でもアクセスできるようにしており、それ以外はBasic認証を必要としています。
  - パスワードをエンコードするためのサービスインターフェースをBean定義して、DIで使用できるようにしています。
  - authenticationManager（Authentication リクエストを処理するためのインスタンス）をBean定義して、DIで使用できるようにしています。

```:WebSecurityConfig.java
package com.example.springBootRestApi.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/login", "/register").permitAll()
                .anyRequest().authenticated()
                .and()
                .httpBasic();
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

```

## Repository
Employeeに関するRepositoryを作成します。
findByEmailメソッドとexistsByEmailメソッドを追加します。

```:EmployeeRepository.java

package com.example.springBootRestApi.repository;

import com.example.springBootRestApi.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {

    Employee findByEmail(String email);

    boolean existsByEmail(String email);
}
```

## UserDetailsServiceの実装クラスの作成
ユーザー固有のデータを読み込むコアインターフェースであるUserDetailsServiceの実装クラスを作成します。
インターフェースに必要な読み取り専用メソッドは 1 つだけです。
これにより、新しいデータアクセス戦略のサポートが簡素化されます。

```:UserDetailsServiceImpl.java
package com.example.springBootRestApi.security;

import com.example.springBootRestApi.model.Employee;
import com.example.springBootRestApi.repository.EmployeeRepository;
import java.util.ArrayList;
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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    private final EmployeeRepository employeeRepository;

    public UserDetailsServiceImpl(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Employee employee = employeeRepository.findByEmail(username);

        if (employee == null) {
            throw new RuntimeException("従業員が見つかりません。");
        }

        return new org.springframework.security.core.userdetails.User(
                employee.getEmail(), employee.getPassword(), new ArrayList<>()
        );
    }
}
```
## Serviceの作成
```:EmployeeService.java
package com.example.springBootRestApi.service;

import com.example.springBootRestApi.model.Employee;
import java.util.List;

public interface EmployeeService {

    List<Employee> getEmployees();

    Employee getEmployee(Long id);

    Employee createEmployee(Employee employee);

    Employee updateEmployee(Long id, Employee employee);

    void deleteEmployee(Long id);
}
```

```:EmployeeServiceImpl.java
package com.example.springBootRestApi.service;

import com.example.springBootRestApi.model.Employee;
import com.example.springBootRestApi.repository.EmployeeRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    private final PasswordEncoder passwordEncoder;

    private final EmployeeRepository employeeRepository;

    public EmployeeServiceImpl(PasswordEncoder passwordEncoder,
                               EmployeeRepository employeeRepository) {
        this.passwordEncoder = passwordEncoder;
        this.employeeRepository = employeeRepository;
    }

    @Override
    public List<Employee> getEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    public Employee getEmployee(Long id) {
        Optional<Employee> optionalEmployee = employeeRepository.findById(id);

        if (optionalEmployee.isEmpty()) {
            throw new RuntimeException("従業員が存在しません。");
        }

        return optionalEmployee.get();
    }

    @Override
    public Employee createEmployee(Employee employee) {
        if (employeeRepository.existsByEmail(employee.getEmail())) {
            throw new RuntimeException("このメールアドレスは既に登録されています");
        }

        Employee newEmployee = new Employee();
        newEmployee.setEmail(employee.getEmail());
        newEmployee.setPassword(passwordEncoder.encode(employee.getPassword()));
        return employeeRepository.save(newEmployee);
    }

    @Override
    public Employee updateEmployee(Long id, Employee requestBody) {
        Employee employee = getEmployee(id);
        employee.setEmail(requestBody.getEmail() == null ? employee.getEmail() : requestBody.getEmail());
        employee.setPassword(requestBody.getPassword() == null ? employee.getPassword() : requestBody.getEmail());
        return employeeRepository.save(employee);
    }

    @Override
    public void deleteEmployee(Long id) {
        getEmployee(id);
        employeeRepository.deleteById(id);
    }
}
```
## Controllerの作成
/register と /login のエンドポイントを作成します。
- register
  - Employeeを作成します。
- login
  - SecurityContextHolderにSecurityContextを設定します。Spring Securityはこの情報を使って認可を行います。

```:EmployeeController.java
package com.example.springBootRestApi.controller;

import com.example.springBootRestApi.model.Employee;
import com.example.springBootRestApi.service.EmployeeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmployeeController {

    private final EmployeeService employeeService;

    private final UserDetailsService userDetailsService;

    private final AuthenticationManager authenticationManager;

    public EmployeeController(EmployeeService employeeService,
                              UserDetailsService userDetailsService,
                              AuthenticationManager authenticationManager) {
        this.employeeService = employeeService;
        this.userDetailsService = userDetailsService;
        this.authenticationManager = authenticationManager;
    }

    @PostMapping("/register")
    public ResponseEntity<Employee> register(@Validated @RequestBody Employee employee) {
        return new ResponseEntity<Employee>(employeeService.createEmployee(employee), HttpStatus.CREATED);
    }

    @PostMapping("/login")
    public ResponseEntity<HttpStatus> login(@Validated @RequestBody Employee employee) {
        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(employee.getEmail(), employee.getPassword())
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        return new ResponseEntity<HttpStatus>(HttpStatus.OK);
    }
}
```

# 参考
https://docs.spring.io/spring-security/reference/

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/core/userdetails/UserDetailsService.html

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/authentication/AuthenticationManager.html

https://spring.pleiades.io/spring-security/site/docs/current/api/org/springframework/security/crypto/password/PasswordEncoder.html