[Spring Boot]JWT를 이용한 로그인

한상욱·2024년 7월 8일
0

Spring Boot

목록 보기
14/17
post-thumbnail

들어가며

이 글은 Spring Boot를 공부하며 정리한 글입니다.

JWT를 이용한 로그인

이전에는 JWT를 이용한 인증 인가 및 프로젝트 설정만 하였습니다. 이번에는 JwtTokenProvider를 통하여 실제로 토큰을 발급해보도록 하겠습니다.

회원의 권한

우리가 만드는 서비스는 회원의 권한을 JWT의 Payload에 삽입하고 해당 Payload에서 정보를 추출하여 인증 인가를 구현하게 됩니다. 따라서, 회원가입을 성공적으로 진행하면 회원의 권한도 함께 저장해야 합니다.

Role

우리는 회원의 권한을 쉽게 이해할 수 있도록 Enum 클래스로 관리하도록 하겠습니다.

enum class Role {
    MEMBER
}

현재는 MEMBER라는 권한만 존재하지만 상황에 따라서 다양한 권한을 설정해도 됩니다. 이제 이러한 권한을 사용자의 정보에 함께 저장해야 합니다.

MemberRole

import ...

@Entity
@Table(
    uniqueConstraints = [UniqueConstraint(name = "uk_member_email", columnNames = ["email"])]
)
class Member(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id : Long?,

    @Column(nullable = false, length = 100, updatable = false)
    val email : String,

    @Column(nullable = false, length = 100)
    val password : String,

    @Column(nullable = false, length = 10)
    val name : String,

    @Column(nullable = false, length = 30)
    @Temporal(TemporalType.DATE)
    val birthday : LocalDate,

    @Column(nullable = false, length = 5)
    @Enumerated(EnumType.STRING)
    val gender : Gender,
) {
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "member")
    val role : List<MemberRole>? = null

    fun toResponse() = MemberResponseDto(
        email = email,
        name = name,
        birthday = birthday,
        gender = gender,
    )
}

@Entity
class MemberRole(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    val id : Long?,

    @Column(nullable = false, length = 30)
    @Enumerated(EnumType.STRING)
    val role : Role,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(foreignKey = ForeignKey(name = "fk_member_role_member_id"))
    val member : Member,
)

위에 따르면 사용자의 권한이 여러개가 존재할 수 있겠습니다. 따라서, 1:N 관계로 Entity를 제작하겠습니다.

LoginDto

import com.fasterxml.jackson.annotation.JsonProperty
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank

data class LoginDto(

    @field:NotBlank(message = "이메일을 입력하세요!")
    @field:Email(message = "올바르지 않은 이메일형식입니다!")
    @JsonProperty("email")
    private val _email : String?,


    @field:NotBlank(message = "비밀번호를 입력하세요!")
    @JsonProperty("password")
    private val _password : String?,
) {
    val email : String
        get() = _email!!
    val password : String
        get() = _password!!
}

이제 사용자에게 로그인 정보를 받을 LoginDto에 대해서 알아보겠습니다. 사실, 회원가입때 충분한 Validation 과정을 거쳤다면, LoginDto에서는 Validation을 구축하지 않아도 큰 문제는 없을겁니다. 하지만, 형식상 Validation을 구축했습니다.

CustomUserDetail

import ...

@Service
class CustomUserDetailsService(
    private val memberRepository: MemberRepository,
    private val passwordEncoder: PasswordEncoder,
) : UserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        return memberRepository.findByEmail(username)
            ?.let { createUserDetails(it) }
            ?: throw UsernameNotFoundException("해당하는 유저는 존재하지 않습니다!")
    }

    private fun createUserDetails(member : Member) : UserDetails {
        return User(
            member.email,
            passwordEncoder.encode(member.password),
            member.role!!.map { SimpleGrantedAuthority("ROLE_${it.role}") }
        )
    }
}

우리는 권한정보를 사용자를 생성할 때 같이 넣어주여야 합니다. 따라서, CustomUserDetailsService를 제작하여 이러한 로직을 작성하였습니다. 권한 정보는 ROLE_를 붙여주어야 본래의 권한이 추출됩니다.

MemberService

import ...

@Transactional
@Service
class MemberService(
    private val memberRepository: MemberRepository,
    private val memberRoleRepository: MemberRoleRepository,
    private val jwtTokenProvider: JwtTokenProvider,
    private val authenticationManagerBuilder: AuthenticationManagerBuilder,
) {
	
    /**
     * 회원가입
     */
    fun signUp(memberRequestDto: MemberRequestDto) : String {
        var member : Member? = memberRepository.findByEmail(memberRequestDto.email)
        if (member != null) {
            throw InvalidEmailException(fieldName = "email", message = "이미 가입한 이메일입니다!")
        }
        member = memberRequestDto.toEntity()
        memberRepository.save(member)
        val memberRole = MemberRole(
            id = null,
            role = Role.MEMBER,
            member = member,
        )
        memberRoleRepository.save(memberRole)
        return "회원가입에 성공했습니다!"
    }
    
    /**
     * 로그인
     */    
    fun login(loginDto: LoginDto) : TokenInfo {
        val authenticationToken = UsernamePasswordAuthenticationToken(loginDto.email, loginDto.password)
        val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
		return jwtTokenProvider.createToken(authentication)
...
}

이제, 회원가입과 로그인을 새롭게 구현해주었습니다. 회원가입에서는 사용자에게 권한 정보를 추가해주었고, 로그인에서는 권한 정보를 생성한 후, 토큰을 생성하여 반환하게 됩니다.

MemberController

import ...

@RestController
@RequestMapping("/api/member")
class MemberController(
    private val memberService: MemberService
) {
    /**
     * 회원가입 Api
     */
    @PostMapping("/join")
    private fun signUp(@Valid @RequestBody memberRequestDto: MemberRequestDto)
    : ResponseEntity<BaseResponse<String>> {
        val result = memberService.signUp(memberRequestDto)
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(BaseResponse(data = result))
    }

    /**
     * 로그인 Api
     */
    @PostMapping("/login")
    private fun login(@Valid @RequestBody loginDto: LoginDto)
    : ResponseEntity<BaseResponse<TokenInfo>> {
        val tokenInfo = memberService.login(loginDto)
        return ResponseEntity.status(HttpStatus.OK)
            .body(BaseResponse(data = tokenInfo))
    }

이로써, JWT를 이용한 인증인가 서비스를 구축하였습니다.

profile
자기주도적, 지속 성장하는 모바일앱 개발자가 되기 위해

0개의 댓글