본문 바로가기
Programming/Spring Boot

[실습] 스프링부트(Spring Boot)로 비밀번호 관리 기능 구현하기 - REST API 구현하기

by 돌방로그 2023. 2. 27.


오늘의 실습 목표는 "비밀번호 관리 기능 만들어보기!" 입니다.
본 게시글에서 다루는 사항은 비밀번호 관리 기능의 Backend 단인 API를 구현하는 과정입니다.


비밀번호 관리 구현하기 - API (Contoller,  Service, Repository, Domain)

사전 준비

아래 사항에 대해서 사전 준비가 완료되지 않으신 분들은 아래 링크를 참조하여 사전 준비를 진행합니다.

 

혹시라도 아래 개념이 잘 기억나지 않으시는 분들은 관련 링크를 참조하시기 바랍니다.

 


파일 구조

테이블을 생성하고 CRUD 기능을 테스트하기 위해서 작업해야 할 파일은 총 4개입니다.

아래 폴더/파일 트리 구조와 호출 흐름도는 전체 구현이 완료된 후 갖춰질 구조입니다.

작업할 파일에 대한 간략한 설명은 아래를 참고하시면 됩니다.

  • AdminUserPassword
    • 목적: AdminUserPassword 테이블과 매핑되는 객체 정의 파일
    • 경로: src\main\java\...\user\domain
  • AdminUserPasswordRepository
    • 목적: AdminUserPassword 에 대한 메소드 정의 파일
    • 경로: src\main\java\...\user\repository
  • AdminUserPasswordService
    • 목적: REST API의 호출 결과를 처리하기 위한 비즈니스 로직이 담겨져 있는 파일
    • 경로: src\main\java\...\user\service
  • AdminUserInformationController
    • 목적: FE(Front-end)의 뷰와 연결되는 최상위 파일로 REST API와 매핑되는 함수가 구현되어 있는 파일
      • UserInformation 하위로 UserPassword가 포함되는 개념이라 AdminUserPasswordController는 별도로 생성하지 않
    • 경로: src\main\java\...\user\controller

테이블 구조

[실습] 스프링부트(Spring Boot)로 비밀번호 기능 구현하기 - DB, API 기획/설계하기를 참조해주세요.

 


소스 코드

AdminUserPasswordKey

2개 이상의 PK를 두기 위해 복합키(Composite Primary Key) 개념을 적용하였습니다.

복합키(Composite Primary Key) 적용 방법 중 @IdClass 어노테이션을 사용하여 구현하였습니다.

(복합키 적용 방법 2가지에 대해서는 별도의 글로 포스팅할 예정입니다. )

 

본 클래스 파일은 복합키 관련 정의 파일입니다.

package com.logsjejustone.webapiserver.user.domain;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

import java.io.Serializable;

@Getter
@Setter
@Data
public class AdminUserPasswordKey implements Serializable {
    private String employeeNo;
    private Integer no;
}

 

AdminUserPassword

위 '테이블 구조'에 링크된 페이지에서 정의한 바와 같이 각 컬럼의 타입과 길이, 속성을 지정합니다.

package com.logsjejustone.webapiserver.user.domain;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.ColumnDefault;

import java.time.LocalDateTime;

@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name="AdminUserPasswordHistory")
@IdClass(AdminUserPasswordKey.class)
public class AdminUserPassword {
    @Id
    @NotNull
    @Column(length = 6)
    private String employeeNo;

    @Id
    @NotNull
    private Integer no;

    @NotNull
    @Column(length = 6)
    private String registerEmployeeNo;

    @NotNull
    private LocalDateTime registerDatetime;

    @NotNull
    @Column(length = 6)
    private String updateEmployeeNo;

    @NotNull
    private LocalDateTime updateDatetime;

    @NotNull
    @Column(columnDefinition = "TEXT")
    private String currentPw;

    @Column(columnDefinition = "TEXT")
    private String previousPw1;

    @Column(columnDefinition = "TEXT")
    private String previousPw2;

    @NotNull
    @Column(length = 1)
    @ColumnDefault("0")
    private String pwTrialState;

    @NotNull
    @Column(length = 1)
    @ColumnDefault("'N'")
    private String temporaryPwState;
}

 

AdminUserPasswordRepository

JpaRepository를 상속받아 기본적인 메소드들은 별도로 정의하지 않고 이용할 수 있습니다.

기본적으로 사용할 수 있는 메소드는 'JpaRepository' 링크를 참조해주세요.

package com.logsjejustone.webapiserver.user.repository;

import com.logsjejustone.webapiserver.user.domain.AdminUserPassword;
import com.logsjejustone.webapiserver.user.domain.AdminUserPasswordKey;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AdminUserPasswordRepository extends JpaRepository<AdminUserPassword, AdminUserPasswordKey> {

    AdminUserPassword findFirstByEmployeeNoOrderByNoDesc(String employeeNo);
}
  • 기본적으로 제공하지는 않지만 JPA Query Creation 기능을 이용하여 쿼리 메소드를 추가하였습니다.
    • findFirstByEmployeeNoOrderByNoDesc: 특정 직원의 최신 비밀번호 이력을 추출하는  API입니다.

 

AdminUserPasswordService

비즈니스 로직을 처리하는 구문으로 테이블에서 데이터를 가져오기 전과 가져온 후에 대한 처리를 하는 코드입니다.

전체 코드가 궁금하신 분들은 아래 더보기를 클릭하여 확인해주시길 바랍니다.

더보기
package com.logsjejustone.webapiserver.user.service;

import com.logsjejustone.webapiserver.user.domain.AdminUserPassword;
import com.logsjejustone.webapiserver.user.repository.AdminUserPasswordRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.Map;


@Service
public class AdminUserPasswordService {

    @Autowired
    private AdminUserPasswordRepository adminUserPasswordRepository;

    private final LocalDateTime localDateTime = LocalDateTime.now();

    // CREATE
    public ResponseEntity<AdminUserPassword> InsertAdminUserPw(Map<String, String> adminUserPw) {
        ResponseEntity<AdminUserPassword> responseEntity;
        String employeeNo = adminUserPw.get("employeeNo");
        String currentPw = adminUserPw.get("employeePw");

        AdminUserPassword adminUserPwLatest = this.adminUserPasswordRepository.findFirstByEmployeeNoOrderByNoDesc(employeeNo);
        System.out.println("adminUserPwLatest:" + adminUserPwLatest);

        String latestCurrentPw = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getCurrentPw();
        String latestPreviousPw1 = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getPreviousPw1();
        String latestPreviousPw2 = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getPreviousPw2();
        Integer latestNo = (adminUserPwLatest == null) ? 0 : adminUserPwLatest.getNo();

        System.out.println("latestCurrentPw: " + latestCurrentPw + " | latestPreviousPw1: " + latestPreviousPw1 + " | latestPreviousPw2: " + latestPreviousPw2 + " | latestNo: " + latestNo);
        if(currentPw.equals(latestCurrentPw) || currentPw.equals(latestPreviousPw1) || currentPw.equals(latestPreviousPw2)) {
            System.out.println("[ERROR/InsertAdminUserPw] 이전에 사용한 비밀번호로 교체할 수 없습니다.");
            responseEntity = new ResponseEntity<>(HttpStatus.CONFLICT);
        }
        else {
            AdminUserPassword adminUserPwNew = new AdminUserPassword();

            // Copy/Paste
            adminUserPwNew.setEmployeeNo(employeeNo);
            adminUserPwNew.setPreviousPw2(latestPreviousPw1);
            adminUserPwNew.setPreviousPw1(latestCurrentPw);

            // New data
            adminUserPwNew.setRegisterEmployeeNo(adminUserPw.get("registerEmployeeNo"));
            adminUserPwNew.setRegisterDatetime(localDateTime);
            adminUserPwNew.setUpdateEmployeeNo(adminUserPw.get("updateEmployeeNo"));
            adminUserPwNew.setUpdateDatetime(localDateTime);

            adminUserPwNew.setNo(latestNo + 1);
            adminUserPwNew.setCurrentPw(currentPw);
            adminUserPwNew.setPwTrialState(adminUserPw.get("pwTrialState"));
            adminUserPwNew.setTemporaryPwState(adminUserPw.get("temporaryPwState"));

            this.adminUserPasswordRepository.save(adminUserPwNew);
            responseEntity = new ResponseEntity<>(HttpStatus.OK);
        }

        System.out.println("responseEntity:" + responseEntity);
        return responseEntity;
    }

    // READ
    public AdminUserPassword GetLatestPwHistory(String employeeNo) {
        return this.adminUserPasswordRepository.findFirstByEmployeeNoOrderByNoDesc(employeeNo);
    }
}

 

CREATE

public ResponseEntity<AdminUserPassword> InsertAdminUserPw(Map<String, String> adminUserPw) {
    ResponseEntity<AdminUserPassword> responseEntity;
    String employeeNo = adminUserPw.get("employeeNo");
    String currentPw = adminUserPw.get("employeePw");

    AdminUserPassword adminUserPwLatest = this.adminUserPasswordRepository.findFirstByEmployeeNoOrderByNoDesc(employeeNo);
    System.out.println("adminUserPwLatest:" + adminUserPwLatest);

    String latestCurrentPw = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getCurrentPw();
    String latestPreviousPw1 = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getPreviousPw1();
    String latestPreviousPw2 = (adminUserPwLatest == null) ? "" : adminUserPwLatest.getPreviousPw2();
    Integer latestNo = (adminUserPwLatest == null) ? 0 : adminUserPwLatest.getNo();

    System.out.println("latestCurrentPw: " + latestCurrentPw + " | latestPreviousPw1: " + latestPreviousPw1 + " | latestPreviousPw2: " + latestPreviousPw2 + " | latestNo: " + latestNo);
    if(currentPw.equals(latestCurrentPw) || currentPw.equals(latestPreviousPw1) || currentPw.equals(latestPreviousPw2)) {
        System.out.println("[ERROR/InsertAdminUserPw] 이전에 사용한 비밀번호로 교체할 수 없습니다.");
        responseEntity = new ResponseEntity<>(HttpStatus.CONFLICT);
    }
    else {
        AdminUserPassword adminUserPwNew = new AdminUserPassword();

        // Copy/Paste
        adminUserPwNew.setEmployeeNo(employeeNo);
        adminUserPwNew.setPreviousPw2(latestPreviousPw1);
        adminUserPwNew.setPreviousPw1(latestCurrentPw);

        // New data
        adminUserPwNew.setRegisterEmployeeNo(adminUserPw.get("registerEmployeeNo"));
        adminUserPwNew.setRegisterDatetime(localDateTime);
        adminUserPwNew.setUpdateEmployeeNo(adminUserPw.get("updateEmployeeNo"));
        adminUserPwNew.setUpdateDatetime(localDateTime);

        adminUserPwNew.setNo(latestNo + 1);
        adminUserPwNew.setCurrentPw(currentPw);
        adminUserPwNew.setPwTrialState(adminUserPw.get("pwTrialState"));
        adminUserPwNew.setTemporaryPwState(adminUserPw.get("temporaryPwState"));

        this.adminUserPasswordRepository.save(adminUserPwNew);
        responseEntity = new ResponseEntity<>(HttpStatus.OK);
    }

    System.out.println("responseEntity:" + responseEntity);
    return responseEntity;
}

 

  • 비밀번호이력(AdminUserPasswordHistory) 테이블에 전달받은 직원번호로 조회되는 데이터가 없는 경우와 있는 경우로 로직을 분리하여 처리합니다. 
  • 기 사용했던 비밀번호(현재, 1번째 전, 2번째 전)인 경우, CONFLICT 오류를 발생시킵니다.

 

READ

public AdminUserPassword GetLatestPwHistory(String employeeNo) {
    return this.adminUserPasswordRepository.findFirstByEmployeeNoOrderByNoDesc(employeeNo);
}
  • 있는 경우에만 데이터를 조회하며, 없는 경우에는 null을 리턴합니다.
    • 관리자 포털 사용자로 추가한 경우 이력 테이블에 무조건 데이터가 있어야 정상적인 로직입니다.

 

AdminUserInformationService

기존에 AdminUserInformation 테이블 기능 구현시 만든 함수들 중, Password 관련 로직을 분리하는 작업을 진행하였습니다. 

UpdateAdminUserInformation 함수에서 Password 관련 로직은 제거하였으며, 제거된 코드는 신규로 생성한 UpdateAdminUserPassword 함수에서 수행하도록 분리하였습니다. 

public ResponseEntity<AdminUserInformation> UpdateAdminUserPw(Map<String, String> employee) {
    ResponseEntity<AdminUserInformation> responseEntity;
    String employeeNo = employee.get("employeeNo");

    Optional<AdminUserInformation> optAdminUserInfo = this.adminUserInformationRepository.findById(employeeNo);
    if(optAdminUserInfo.isEmpty()) {
        System.out.println("[ERROR/UpdateAdminUserPw] The employee No(" + employeeNo + ") does NOT exist.");

        responseEntity = new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
    else {
        AdminUserInformation afterAdminUserInfo = optAdminUserInfo.get();

        afterAdminUserInfo.setUpdateDatetime(localDateTime);
        afterAdminUserInfo.setUpdateEmployeeNo(employee.get("updateEmployeeNo"));

        afterAdminUserInfo.setEmployeePw(employee.get("employeePw"));
        int pwExpDate = employee.get("temporaryPwState").equals("Y") ? 7 : 60;
        afterAdminUserInfo.setEmployeePwExpDate(localDateTime.plusDays(pwExpDate).format(DateTimeFormatter.ofPattern("yyyyMMdd")));

        responseEntity = new ResponseEntity<>(HttpStatus.OK);

        this.adminUserInformationRepository.save(afterAdminUserInfo);
    }

    return responseEntity;
}

public ResponseEntity<AdminUserInformation> UpdateAdminUserInformation(AdminUserInformation adminUserInformation,
                                                                       String targetEmpNo) {
        ...

//            비밀번호의 경우 별도의 함수(UpdateAdminUserPw)에서 처리함

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

 

AdminUserInformationController

비밀번호이력(AdminUserPasswordHistory) 테이블에 대한 처리 로직을 호출하는 REST API가 매핑되는 코드입니다.

Password 관련하여 총 3개 API가 수정 및 추가되어 있습니다.

  • CREATE: (POST: ~/admin-web/user)
  • READ: (GET: ~/admin-web/user/password)
  • UPDATE: (PUT: ~/admin-web/user/update)

 

전체 코드를 확인하고 싶으신 분들은 아래 더보기를 클릭해주세요.

더보기
package com.logsjejustone.webapiserver.user.controller;

import com.logsjejustone.webapiserver.user.domain.AdminUserInformation;
import com.logsjejustone.webapiserver.user.domain.AdminUserPassword;
import com.logsjejustone.webapiserver.user.service.AdminUserInformationService;
import com.logsjejustone.webapiserver.user.service.AdminUserPasswordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/admin-web/user")
@CrossOrigin(origins = "http://localhost:8081")
public class AdminUserInformationController {

    @Autowired
    private AdminUserInformationService adminUserInformationService;

    @Autowired
    private AdminUserPasswordService adminUserPasswordService;


    // CREATE
    @PostMapping("")
    public ResponseEntity<AdminUserInformation> AddAdminUser(@RequestBody Map<String, String> employee) {
        System.out.println("[AdminUserInformation:AddAdminUser]" + employee);

        ResponseEntity<AdminUserInformation> responseEntity;
        HttpStatusCode statusCode = this.adminUserPasswordService.InsertAdminUserPw(employee).getStatusCode();
        if(statusCode.is2xxSuccessful()) {
            responseEntity = this.adminUserInformationService.AddAdminUser(employee);
        }
        else {
            responseEntity = new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
        }

        return responseEntity;
    }


    // READ
    @GetMapping("/all")
    public List<AdminUserInformation> GetAllAdminUsers() {
        System.out.println("[AdminUserInformation:GetAllAdminUsers]");

        return this.adminUserInformationService.GetAllAdminUsers();
    }

    @GetMapping("/available")
    public List<AdminUserInformation> GetAvailableAdminUsers() {
        System.out.println("[AdminUserInformation:GetAvailableAdminUsers]");

        return this.adminUserInformationService.GetAvailableAdminUsers();
    }

    @GetMapping("/password")
    public AdminUserPassword GetLatestPwHistory(@RequestBody Map<String, String> employee) {
        System.out.println("[AdminUserPassword:GetLatestPwHistory] employee:" + employee);

        return this.adminUserPasswordService.GetLatestPwHistory(employee.get("employeeNo"));
    }

    // UPDATE
    @PutMapping("/update/{targetEmpNo}")
    public ResponseEntity<AdminUserInformation> UpdateAdminUserInformation(@RequestBody AdminUserInformation adminUserInformation,
                                                                           @PathVariable String targetEmpNo) {
        System.out.println("[AdminUserInformation:UpdateAdminUserInformation]" + adminUserInformation + "\n Target:" + targetEmpNo);

        return this.adminUserInformationService.UpdateAdminUserInformation(adminUserInformation, targetEmpNo);
    }

    @PutMapping("/update")
    public ResponseEntity<AdminUserInformation> UpdateAdminUserPw(@RequestBody Map<String, String> employee) {
        System.out.println("[AdminUserInformation:UpdateAdminUserPw]" + employee);

        ResponseEntity<AdminUserInformation> responseEntity;
        HttpStatusCode statusCode = this.adminUserPasswordService.InsertAdminUserPw(employee).getStatusCode();
        if(statusCode.is2xxSuccessful()) {
            responseEntity = this.adminUserInformationService.UpdateAdminUserPw(employee);
        }
        else {
            responseEntity = new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
        }

        return responseEntity;
    }

    // DELETE
    @PutMapping("/delete/{targetEmpNo}")
    public ResponseEntity<AdminUserInformation> DeleteAdminUser(@RequestBody AdminUserInformation adminUserInformation,
                                                                @PathVariable String targetEmpNo) {
        System.out.println("[AdminUserInformation:DeleteAdminUser]" + adminUserInformation + "\n Target:" + targetEmpNo);

        return this.adminUserInformationService.DeleteAdminUser(adminUserInformation, targetEmpNo);
    }

}

 

[CREATE] 사용자 신규 추가하는 경우

기존에 구현했던 API에서 '비밀번호 변경 이력 추가' 로직이 추가되었습니다.

@PostMapping("")
public ResponseEntity<AdminUserInformation> AddAdminUser(@RequestBody Map<String, String> employee) {
    System.out.println("[AdminUserInformation:AddAdminUser]" + employee);

    ResponseEntity<AdminUserInformation> responseEntity;
    HttpStatusCode statusCode = this.adminUserPasswordService.InsertAdminUserPw(employee).getStatusCode();
    if(statusCode.is2xxSuccessful()) {
        responseEntity = this.adminUserInformationService.AddAdminUser(employee);
    }
    else {
        responseEntity = new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
    }

    return responseEntity;
}

 

추가적으로 기존 API와 달라진 점이 있습니다.

  • Parameter로 Class 객체가 아닌 Map<String, String>으로 전달받도록 수정
    • →  Information 외 Password 관련 정보도 함께 전달하기 위함
  • 비밀번호 이력 추가 작업을 선 진행하여 사전 조건 확인 로직을 수행하도록 함

 

[READ] 비밀번호 최신 이력 조회하는 경우

신규 추가된 API로 특정 직원의 최신 비밀번호 이력 정보를 획득하기 위한 API입니다.

@GetMapping("/password")
public AdminUserPassword GetLatestPwHistory(@RequestBody Map<String, String> employee) {
    System.out.println("[AdminUserPassword:GetLatestPwHistory] employee:" + employee);

    return this.adminUserPasswordService.GetLatestPwHistory(employee.get("employeeNo"));
}

 

[UPDATE] 비밀번호 변경 혹은 임시발급 하는 경우

신규 추가된 API로 특정 직원의 비밀번호를 사용자 본인이 직접 변경하거나 관리자가 임시 비밀번호를 발급해주는 경우에 호출되는 API입니다.

@PutMapping("/update")
public ResponseEntity<AdminUserInformation> UpdateAdminUserPw(@RequestBody Map<String, String> employee) {
    System.out.println("[AdminUserInformation:UpdateAdminUserPw]" + employee);

    ResponseEntity<AdminUserInformation> responseEntity;
    HttpStatusCode statusCode = this.adminUserPasswordService.InsertAdminUserPw(employee).getStatusCode();
    if(statusCode.is2xxSuccessful()) {
        responseEntity = this.adminUserInformationService.UpdateAdminUserPw(employee);
    }
    else {
        responseEntity = new ResponseEntity<>(HttpStatus.EXPECTATION_FAILED);
    }

    return responseEntity;
}

 


테스트

테스트는 별도의 포스팅으로 작성해둔 [실습] 스프링부트(Spring Boot)로 비밀번호 관리 기능 구현하기 - REST API 테스트하기를 참조해주세요.

 


 

댓글