본문 바로가기
Dev/SpringBoot

24. [SpringBoot] RESTFul한 로그인 구현 예제 - 2

by VIPeveloper 2020. 10. 6.
반응형

1. 서론

지난 포스팅에서는 하드코딩된 하나의 유저로 로그인 하는 과정을 학습해보았습니다. 이 방법은 실무에 적합하지 않습니다.

이번 포스팅에서는 여러 유저의 정보를 통해 회원가입을 하고, 회원 가입된 유저를 로그인하는 방법에 대해 학습하겠습니다.

2. 본론

1. User

- 우리는 유저 정보를 저장하기 위해 JPA를 이용할 것입니다. 그러기 위해서 가장 먼저 필요한 것이 도메인 설정입니다.

- 유저에는 UserDetails를 implement 해줍니다. 관련 설정들을 모두 오버라이딩 해줍니다.

import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

@Entity
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    @Column(name = "email", unique = true)
    private String email;

    @Column(name = "password")
    private String password;

    @Column(name = "auth")
    private String auth;

    @Builder
    public User(String email, String password, String auth) {
        this.email = email;
        this.password = password;
        this.auth = auth;
    }

    // 사용자의 권한을 콜렉션 형태로 반환
    // 단, 클래스 자료형은 GrantedAuthority를 구현해야함
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> roles = new HashSet<>();
        for (String role : auth.split(",")) {
            roles.add(new SimpleGrantedAuthority(role));
        }
        return roles;
    }

    // 사용자의 id를 반환 (unique한 값)
    @Override
    public String getUsername() {
        return email;
    }

    // 사용자의 password를 반환
    @Override
    public String getPassword() {
        return password;
    }

    // 계정 만료 여부 반환
    @Override
    public boolean isAccountNonExpired() {
        // 만료되었는지 확인하는 로직
        return true; // true -> 만료되지 않았음
    }

    // 계정 잠금 여부 반환
    @Override
    public boolean isAccountNonLocked() {
        // 계정 잠금되었는지 확인하는 로직
        return true; // true -> 잠금되지 않았음
    }

    // 패스워드의 만료 여부 반환
    @Override
    public boolean isCredentialsNonExpired() {
        // 패스워드가 만료되었는지 확인하는 로직
        return true; // true -> 만료되지 않았음
    }

    // 계정 사용 가능 여부 반환
    @Override
    public boolean isEnabled() {
        // 계정이 사용 가능한지 확인하는 로직
        return true; // true -> 사용 가능
    }
}

2. UserRepository

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
  Optional<User> findByEmail(String email);
}

3. JwtUserDetailsService

- 이 부분을 수정할 것입니다. 하드코딩되어있었던 유저 정보 대신, DB에서 정보를 가져와 비교해보는 검증 과정을 코딩하여봅시다. 또한 회원가입을 위해 정보를 삽입할 수 있는 서비스 계층을 만들어 봅시다.

import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;

@Service
@RequiredArgsConstructor
public class JwtUserDetailsService implements UserDetailsService {

	//	@Override
//	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//		if ("user_id".equals(username)) {
//			return new User("user_id", "$2a$10$jCvWm3NXDRFs/EfuI4h4.u0ZxNocv.ZkgEy6qbjVXrfQ5.KzLfhAe",
//					new ArrayList<>());
//		} else {
//			throw new UsernameNotFoundException("User not found with username: " + username);
//		}
//	}
	private final UserRepository userRepository;

	/**
	 * Spring Security 필수 메소드 구현
	 *
	 * @param email 이메일
	 * @return UserDetails
	 * @throws UsernameNotFoundException 유저가 없을 때 예외 발생
	 */
	@Override // 기본적인 반환 타입은 UserDetails, UserDetails를 상속받은 UserInfo로 반환 타입 지정 (자동으로 다운 캐스팅됨)
	public User loadUserByUsername(String email) throws UsernameNotFoundException { // 시큐리티에서 지정한 서비스이기 때문에 이 메소드를 필수로 구현
		return userRepository.findByEmail(email)
				.orElseThrow(() -> new UsernameNotFoundException((email)));
	}
	/**
	 * 회원정보 저장
	 *
	 * @param infoDto 회원정보가 들어있는 DTO
	 * @return 저장되는 회원의 PK
	 */
	@Transactional
	public Long save(UserDto infoDto) {
		BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
		infoDto.setPassword(encoder.encode(infoDto.getPassword()));

		return userRepository.save(User.builder()
				.email(infoDto.getEmail())
				.auth(infoDto.getAuth())
				.password(infoDto.getPassword()).build()).getId();
	}
}

4. UserDto

- json내부 정보를 감싸기 위해 Dto를 설정합니다.

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserDto {
  private String email;
  private String password;

  private String auth;
}

5. UserController

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
public class UserController {

    private final JwtUserDetailsService userService;

    @PostMapping("/signup")
    public Response signup(@RequestBody UserDto infoDto) { // 회원 추가
        Response response = new Response();

        try {
            userService.save(infoDto);
            response.setResponse("success");
            response.setMessage("회원가입을 성공적으로 완료했습니다.");
        } catch (Exception e) {
            response.setResponse("failed");
            response.setMessage("회원가입을 하는 도중 오류가 발생했습니다.");
            response.setData(e.toString());
        }
        return response;
    }

}

6. Response

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class Response {
    private String response;
    private String message;
    private Object data;

    public Response(String response, String message, Object data) {
        this.response = response;
        this.message = message;
        this.data = data;
    }

}

7. application.yml

- h2 데이터베이스를 사용할 것입니다.

spring:
  jwt:
    secret: lemorninglemorninglemorninglemorninglemorninglemorninglemorninglemorninglemorninglemorning
  datasource:
#    url: jdbc:h2:tcp://localhost/~/testDB
    url: jdbc:h2:mem:testDB
    username: sa
    password:
    driver-class-name: org.h2.Driver
  devtools:
    livereload:
      enabled: true   # 이 옵션은 정적 파일 변동 시 자동 반영될 수 있도록 돕는다.
  jpa:
    hibernate:
      ddl-auto: create  # 이 옵션은 애플리케이션 실행 시점에 테이블을 drop 하고, 다시 생성한다
    properties:
      hibernate:
        # show_sql: true
        format_sql: true
  thymeleaf:
    prefix: classpath:/templates/
    suffix: .html
logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace   # sql query 에 들어오는 파라메타 값 추적

8. test

- 이제 서버를 실행시켜보고, 회원가입을 진행해봅시다.

- 주의해야할 것이 있습니다. 회원 가입 시, controller단에서 userDto를 객체로 받기 때문에, 인증시 사용되는 username,password를 이용하면 안됩니다. 커스터마이징한 email,password로 지정해야합니다. 이것을 프론트엔드 개발자와 협의하는 과정이 필요합니다.

성공!

그 다음 로그인 시에는 username, password를 이용합니다.

성공!

 

3. 결론

- DB에 여러명의 유저를 저장하고(회원가입), 로그인하는 API를 만들어보았습니다.

반응형