프로젝트에서 Spring Security와 같은 프레임워크를 사용하지 않고 인증 프로세스를 구현하였다.
회원가입은 회원정보를 이용해 회원을 가입시키고 이에 대한 인증토큰을 발급하는 프로세스로 구성하였다.
@Configuration
class AuthConfig {
@Bean
fun passwordEncoder(): PasswordEncoder{
return BCryptPasswordEncoder()
}
}
@Service
@Transactional
class UserService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
private val authenticationService: AuthenticationService,
) {
fun signupUser(request: UserSignupRequest): UserSignUpResponse{
val encodedPassword = passwordEncoder.encode(request.password)
val newUser = User.of(request.email, encodedPassword, request.userName)
userRepository.saveUser(newUser)
requireNotNull(newUser.id)
val token = authenticationService.generateToken(newUser)
return UserSignUpResponse(newUser.id, request.email, request.userName,
loginInfo = Login(token))
}
}
$2a$10$VvVu/IuIlFCzdZv0o3WY5esiM5P9CQF25qzzRCG1eo.OhnnONDc9W
@Service
@Transactional
class UserService(
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
private val authenticationService: AuthenticationService,
) {
fun loginUser(request: UserLoginRequest): UserLoginResponse{
val user: User = findValidUserByEmail(request.email)
if(!passwordEncoder.matches(request.password, user.password))
throw SecurityException(ErrorCode.BAD_CREDENTIALS_ERROR)
user.updateLastLoginTime()
requireNotNull(user.id)
check(userRepository.updateUser(userId = user.id, lastLoginAt = user.lastLoginAt)){"update last login time fail"}
val token = authenticationService.generateToken(user)
return UserLoginResponse(loginInfo = Login(token))
}
@Transactional(readOnly = true)
fun findValidUserByEmail(email: String): User {
val user: User = userRepository.findByUserEmail(email) ?: throw SecurityException(ErrorCode.USER_NOT_FOUND)
check(!user.isWithdrawal()) { "The user(${user.email}) is withdraw" }
return user
}
}
@Transactional
@Service
class AuthenticationService(
private val refreshTokenRepository: RefreshTokenRepository,
private val userRepository: UserRepository,
private val passwordEncoder: PasswordEncoder,
private val tokenUtil : TokenUtil
) : Log {
fun generateToken(user: User): Token {
requireNotNull(user.id)
val token = tokenUtil.generateToken("test", user.id, user.email)
val refreshToken = RefreshToken(user.email, passwordEncoder.encode(token.refreshToken))
refreshTokenRepository.save(refreshToken)
return token
}
}
Authorization
으로 정하고 value는 Bearer
접두사를 붙인다.@Component
class TokenVerifyInterceptor(
private val tokenUtil: TokenUtil,
private val userService: UserService
) : HandlerInterceptor, Log {
companion object {
const val USER_KEY = "userProfile"
}
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
val method = request.method
if(method == HttpMethod.OPTIONS.name())
return true
try {
val token: String = tokenUtil.getToken(request)
tokenUtil.verifyToken(token)
val userEmail = tokenUtil.getEmail(token)
val user = userService.findValidUserByEmail(userEmail)
request.setAttribute(USER_KEY, user)
} catch (e: Exception) {
when (e) {
is java.lang.SecurityException,
is MalformedJwtException,
is UnsupportedJwtException,
is IllegalArgumentException -> {
log.debug("token is expired")
throw SecurityException(ErrorCode.BAD_CREDENTIALS_ERROR)
}
is ExpiredJwtException -> {
log.debug("token is expired")
throw SecurityException(ErrorCode.TOKEN_EXPIRED)
}
is SecurityException -> {
log.warn("user cannot access cause : {}", e.errorCode)
throw e
}
else -> {
log.error("unexpected error occurs on verifying token. request uri : {}, token - {}, msg - {}",
request.requestURI,
request.getHeader("Authorization"),
e.message, e)
throw SecurityException(ErrorCode.BAD_CREDENTIALS_ERROR)
}
}
}
return true
}
}
@Configuration
class WebConfig(
private val tokenVerifyInterceptor: TokenVerifyInterceptor,
private val loginArgumentResolver: UserArgumentResolver,
private val authorizationInterceptor: AuthorizationInterceptor
) : WebMvcConfigurer {
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(tokenVerifyInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/employee/name/**")
.excludePathPatterns("/user/*")
.excludePathPatterns("/auth/*")
.excludePathPatterns("/h2-console/*")
.excludePathPatterns("/favicon.ico")
.excludePathPatterns("/test/**")
}
}
@RestController
@RequestMapping("/employee")
class EmployeeController(
private val employeeService: EmployeeService,
private val updateValidator: EmployeeUpdateValidator
) {
@GetMapping("/{empNo}")
fun getEmployee(
@LoginUser user: User,
@PathVariable empNo: Int): CommonApiResponse<Employee> {
val employee = employeeService.findEmployeeByEmpNo(empNo)
return CommonApiResponse(
success = true,
data = employee
)
}
ArgumentResolver
를 이용해 인증이 완료된 사용자 정보를 가져온다.@Component
class UserArgumentResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.hasParameterAnnotation(LoginUser::class.java)
}
override fun resolveArgument(
parameter: MethodParameter,
mavContainer: ModelAndViewContainer?,
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): User {
return webRequest.getNativeRequest(HttpServletRequest::class.java)
?.getAttribute(TokenVerifyInterceptor.USER_KEY) as User?
?: throw SecurityException(ErrorCode.USER_NOT_FOUND)
}
}
@Configuration
class WebConfig(
private val tokenVerifyInterceptor: TokenVerifyInterceptor,
private val loginArgumentResolver: UserArgumentResolver,
private val authorizationInterceptor: AuthorizationInterceptor
) : WebMvcConfigurer {
override fun addArgumentResolvers(resolvers: MutableList<HandlerMethodArgumentResolver>) {
resolvers.add(loginArgumentResolver)
}
}
ArgumentResolver
는 WebConfig에 등록해준다.