はじめに
Spring Security を使用して既存の REST API プロジェクトに Basic 認証を追加することを目的としています。
Spring Boot を使用して REST API を作成するという記事の続きとなっております。
エンドポイント
今回作成するアプリケーションのエンドポイントは以下のようになります。
メソッド | 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);
}
}
参考