Book/스프링부트 핵심가이드

스프링 시큐리티(Spring Security) 구현(of. 회원가입, 로그인)

블로그 주인장 2023. 12. 8.

회원가입과 로그인 구현

스프링 시큐리티 설정 후에 회원가입과 로그인 구현하는 것에 대해 알아보겠습니다.


회원가입 구현


SignService 인터페이스 구현

public interface SignService {

    /**
     * 회원 가입
     */
    SignUpResultDto signUp(String id, String password, String name, String role);

    /**
     * 로그인
     */
    SignUpResultDto signIn(String id, String password) throws RuntimeException;
}

 

SignServiceImpl 클래스 (회원 가입)

@Override
public SignUpResultDto signUp(String id, String password, String name, String role) {
    LOGGER.info("[getSignUpResult] 회원 가입 정보 전달");
    User user;
    if (role.equalsIgnoreCase("admin")) {
        user = User.builder()
                .uid(id)
                .name(name)
                .password(passwordEncoder.encode(password))
                .roles(Collections.singletonList("ROLE_ADMIN"))
                .build();
    } else {
        user = User.builder()
                .uid(id)
                .name(name)
                .password(passwordEncoder.encode(password))
                .roles(Collections.singletonList("ROLE_USER"))
                .build();
    }

    User savedUser = userRepository.save(user);
    SignInResultDto signInResultDto = new SignInResultDto();

    LOGGER.info("[getSignUpResult] userEntity 값이 들어왔는지 확인 후 결과값 주입");
    if (!savedUser.getName().isEmpty()) {
        LOGGER.info("[getSignUpResult] 정상 처리 완료");
        setSuccessResult(signInResultDto);
    } else {
        LOGGER.info("[getSignUpResult] 정상 처리 완료");
        setFailResult(signInResultDto);
    }

    return signInResultDto;
}
  • ADMIN과 USER로 권한을 구분한다.
  • signUp( ) 메서드는 role 객체를 확인하여 User 엔티티에 roles 권한을 추가해서 엔티티를 생성한다.
  • 패스워드는 암호화해야하기에 PasswordEncoder를 활용하여 인코딩을 수행한다.
  • 생성된 엔티티를 UserRespository를 통해 저장한다.

 

SignServiceImpl 클래스 (로그인)

@Override
public SignUpResultDto signIn(String id, String password) throws RuntimeException {
    LOGGER.info("[getSignInResult] signDataHandler 로 회원 정보 요청");
    User user = userRepository.getByUid(id);
    LOGGER.info("[getSignInResult] Id : {}", id);

    LOGGER.info("[getSignInResult] 패스워드 비교 수행");
    if (!passwordEncoder.matches(password, user.getPassword())) {
        LOGGER.info("[getSignInResult] 패스워드 불일치");
        throw new RuntimeException();
    }
    
    LOGGER.info("[getSignInResult] SignInResultDto 객체 생성");
    SignInResultDto signInResultDto = SignInResultDto.builder()
            .token(jwtTokenProvider.createToken(
                    String.valueOf(user.getUid()), user.getRoles()))
            .build();

    LOGGER.info("[getSignInResult] SignInResultDto 객체 주입");
    setSuccessResult(signInResultDto);
    
    return signInResultDto;
}
  • 로그인은 미리 저장되어 있는 계정 정보와 요청을 통해 전달된 계정 정보가 일치하는지 확인하는 작업이다.
  • 내부 로직 시퀀스
    1. id를 기반으로 UserRepository에서 User 엔티티를 가져온다.
    2. PasswordEncoder를 사용하여 데이터베이스에 저장되어 있던 패스워드와 입력받은 패스워드가 일치하는지 확인하는 작업을 수행한다.
    3. 패스워드가 일치해서 인증을 통과하면 JwtTokenProvider를 통해 id 와 role 값을 전달해서 토큰을 생성한 후 Response에 담아 전달한다.

 

SignServiceImpl 클래스 (성공 및 실패 결과)

  • 회원가입과 로그인 메서드에서 사용할 수 있는 데이터를 설정하는 메서드이다.
  • 각 메서드는 DTO를 전달받아 값을 설정한다.
private void setFailResult(SignInResultDto signInResultDto) {
    signInResultDto.setSuccess(false);
    signInResultDto.setCode(CommonResponse.FAIL.getCode());
    signInResultDto.setMsg(CommonResponse.FAIL.getMsg());
}

private void setSuccessResult(SignInResultDto signInResultDto) {
    signInResultDto.setSuccess(true);
    signInResultDto.setCode(CommonResponse.SUCCESS.getCode());
    signInResultDto.setMsg(CommonResponse.SUCCESS.getMsg());
}

 

CommonResponse 클래스 구현

@Getter
@AllArgsConstructor
public enum CommonResponse {
    SUCCESS(0, "Success") ,
    FAIL(-1, "Fail");

    final int code;
    final String msg;
}

 

PasswordEncoderConfiguration 클래스 구현

  • PasswordEncoder는 별도의 @Configuration 클래스를 생성하고 @Bean 객체로 등록하도록 구현
@Configuration
public class PasswordEncoderConfiguration {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

 

 

SignUpResultDto 클래스 구현

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {
    private boolean success;
    private int code;
    private String msg;
}

 

SignInResultDto 클래스 구현

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@EqualsAndHashCode(callSuper = true)
public class SignInResultDto extends SignUpResultDto {

    private String token;

    @Builder
    public SignInResultDto(boolean success, int code, String msg, String token) {
        super(success, code, msg);
        this.token = token;
    }
}

 

SignController 클래스 구현

@RestController
@RequestMapping("/sign-api")
@RequiredArgsConstructor
public class SignController {
    private final Logger LOGGER = LoggerFactory.getLogger(SignController.class);
    private final SignService signService;

    @PostMapping(value = "/sign-in")
    public SignInResultDto signIn(
            @ApiParam(value = "ID", required = true) @RequestParam String id,
            @ApiParam(value = "Password", required = true) @RequestParam String password
    ) throws RuntimeException {

        LOGGER.info("[signIn] 로그인을 시도하고 있습니다. id : {}, pw : ****", id);
        SignInResultDto signInResultDto = signService.signIn(id, password);

        if (signInResultDto.getCode() == 0) {
            LOGGER.info("[signIn] 정상적으로 로그인되었습니다. id : {}, token : {}" ,
                    id, signInResultDto.getToken());
        }

        return signInResultDto;
    }

    @PostMapping(value = "/sign-up")
    public SignUpResultDto signUp(
            @ApiParam(value = "ID", required = true) @RequestParam String id,
            @ApiParam(value = "비밀번호", required = true) @RequestParam String password,
            @ApiParam(value = "이름", required = true) @RequestParam String name,
            @ApiParam(value = "권한", required = true) @RequestParam String role
    ) {
        LOGGER.info("[signUp] 회원가입을 수행합니다. id : {}, 비밀번호 : ****," +
                " name : {}, role : {}", id, name, role);

        SignUpResultDto signUpResultDto = signService.signUp(id, password, name, role);

        LOGGER.info("[signUp] 회원가입을 완료했습니다. id : {}", id);
        return signUpResultDto;
    }

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> ExceptionHandler(RuntimeException e) {
        HttpHeaders responseHeaders = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

        LOGGER.error("ExceptionHandler 호출, {}, {}", e.getCause(), e.getMessage());

        Map<String, String> map = new HashMap<>();
        map.put("error-type", httpStatus.getReasonPhrase());
        map.put("code", "400");
        map.put("message", "에러 발생");
        
        return new ResponseEntity<>(map, responseHeaders, httpStatus);
    }
}
  • 클라이언트는 계정을 생성하고 로그인 과정을 거쳐 토큰값을 전달받음으로써 애플리케이션에서 제공하는 API 서비스를 사용할 준비를 한다.
반응형

댓글