이 글은 Spring Boot를 공부하며 정리한 글입니다.
이전에는 JWT를 이용한 인증 인가 및 프로젝트 설정만 하였습니다. 이번에는 JwtTokenProvider를 통하여 실제로 토큰을 발급해보도록 하겠습니다.
우리가 만드는 서비스는 회원의 권한을 JWT의 Payload에 삽입하고 해당 Payload에서 정보를 추출하여 인증 인가를 구현하게 됩니다. 따라서, 회원가입을 성공적으로 진행하면 회원의 권한도 함께 저장해야 합니다.
우리는 회원의 권한을 쉽게 이해할 수 있도록 Enum 클래스로 관리하도록 하겠습니다.
enum class Role {
MEMBER
}
현재는 MEMBER라는 권한만 존재하지만 상황에 따라서 다양한 권한을 설정해도 됩니다. 이제 이러한 권한을 사용자의 정보에 함께 저장해야 합니다.
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를 제작하겠습니다.
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을 구축했습니다.
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_를 붙여주어야 본래의 권한이 추출됩니다.
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)
...
}
이제, 회원가입과 로그인을 새롭게 구현해주었습니다. 회원가입에서는 사용자에게 권한 정보를 추가해주었고, 로그인에서는 권한 정보를 생성한 후, 토큰을 생성하여 반환하게 됩니다.
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를 이용한 인증인가 서비스를 구축하였습니다.