[Spring] 회원가입과 로그인 구현

WOOK JONG KIM·2022년 11월 9일
0
post-thumbnail

그전에 UserDetails 인터페이스의 구현체 클래스로 User 엔티티 생성

지금까지는 User 객체를 통해 인증하는 방법을 구현했는데, 여기선 회원가입을 구현하고 User 객체로 인증을 시도하는 로그인 구현

회원가입과 로그인의 도메인은 Sign으로 통합해서 표현할 예정, 각각 Sign-up, Sign-in으로 구분해서 기능을 구현

서비스 레이어 구현

public interface SignService {
    
    SignUpResultDto signUp(String id, String password, String name, String role);
    
    SignInResultDto signIn(String id, String password) throws  RuntimeException;
}

SignService 인터페이스 구현

@Service
public class SignServiceImpl implements SignService {

    private final Logger LOGGER = LoggerFactory.getLogger(SignServiceImpl.class);

    public UserRepository userRepository;
    public JwtTokenProvider jwtTokenProvider;
    public PasswordEncoder passwordEncoder;

    @Autowired
    public SignServiceImpl(UserRepository userRepository, JwtTokenProvider jwtTokenProvider,
                           PasswordEncoder passwordEncoder){
        this.userRepository = userRepository;
        this.jwtTokenProvider = jwtTokenProvider;
        this.passwordEncoder = passwordEncoder;
    }

    @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(password))
                    .build();
        } else{
            user = User.builder()
                    .uid(id)
                    .name(name)
                    .password(passwordEncoder.encode(password))
                    .roles(Collections.singletonList("ROLE_USER"))
                    .build();
        }

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

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

    @Override
    public SignInResultDto 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())){
            throw new RuntimeException();
        }

        LOGGER.info("[getSignInResult] 패스워드 일치");

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

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

    private void setFailResult(SignUpResultDto result){
        result.setSuccess(false);
        result.setCode(CommonResponse.FAIL.getCode());
        result.setMsg(CommonResponse.FAIL.getMsg());
    }
}

우선 초반에 회원가입과 로그인을 구현하기 위해 세가지 객체에 대한 의존성 주입을 받음

signUp(회원가입 구현)
-> ADMIN및 USER 권한으로 구분된 role 객체를 확인해 User 엔티티의 roles 변수에 추가해서 엔티티 생성

패스워드는 암호화해서 저장해야 하기 때문에 PasswordEncoder를 활용해 인코딩 수행

PassWord인코더는 별도의 @Configuration 클래스를 생성하고 @Bean 객체로 등록하도록 구현

@Configuration
public class PasswordEncoderConfiguration {
    
    @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

위 코드는 빈 객체를 등록하기 위해서 생성된 클래스이기에, SecurityConfiguration 클래스 같은 이미 생성된 @Configuration 클래스 내부에 passwordEncoder() 메서드를 정의해도 충분

이렇게 생성된 엔티티를 UserRepository를 통해 저장

이후 SignIn 메서드에서 로그인 메서드 구현
-> 로그인 : 미리 저장돼 있는 계정 정보와 요청을 통해 전달된 계정 정보가 일치하는지 확인하는 작업

SignIn 내부 로직
1. id를 기반으로 UserRepository에서 User엔티티를 가져옴
2. PassWordEncoder를 사용해 DB에 저장돼 있던 패스워드와 입력받은 패스워드가 일치하는지 확인하는 작업 수행, 위에선 RuntimeException을 사용했지만 별도의 커스텀 예외 만들기도 함
3. 패스워드가 일치해서 인증을 통과하면 JwtTokenProvider를 통해 id와 role 값을 전달해서 토큰을 생성한 후 Response에 담아 전달

코드 마지막 부분에 사용된 CommonResponse 열거체

package com.springboot.security.common;

public enum CommonResponse {
    
    SUCCESS(0, "Success"), FAIL(-1, "Fail");
    
    int code;
    String msg;
    
    CommonResponse(int code, String msg){
        this.code = code;
        this.msg = msg;
    }
    
    public int getCode(){
        return code;
    }
    
    public String getMsg(){
        return msg;
    }
}

이후 회원가입과 로그인을 API로 노출하는 컨트롤러 생성해야함
-> 사실상 서비스 레이어로 요청을 전달하고 응답하는 역할만 수행

SignController

@RestController
@RequestMapping("/sign-api")
public class SignController {

    private final Logger LOGGER = LoggerFactory.getLogger(SignController.class);
    private final SignService signService;

    @Autowired
    public SignController(SignService signService) {
        this.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 : {}, password : ****, name : {}, role : {}", id,
            name, role);
        SignUpResultDto signUpResultDto = signService.signUp(id, password, name, role);

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

    @GetMapping(value = "/exception")
    public void exceptionTest() throws RuntimeException {
        throw new RuntimeException("접근이 금지되었습니다.");
    }

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> ExceptionHandler(RuntimeException e) {
        HttpHeaders responseHeaders = new HttpHeaders();
        //responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/json");
        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 서비스를 사용할 준비를 마침

Response로 전달되는 SignUpResultDto와 SignInResultDto 클래스

@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
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;
    }

}
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class SignUpResultDto {

    private boolean success;

    private int code;

    private String msg;

}

여기까지 구현하면 정상적으로 스프링 시큐리티가 동작하는 애플리케이션 환경이 완성된 것

profile
Journey for Backend Developer

0개의 댓글