0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Posted at

はじめに

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);
    }
}

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?