Spring Boot Kotlin - Security
ជំពូកនេះនឹងពន្យល់អ្នកពី Spring Security's Dependency ជាអ្វីហើយអាចធ្វើអ្វីបានខ្លះនៅលើកម្មវិធី Spring Boot។
Spring Boot Security
Spring Security ជា servlet filters ដែលជួយអ្នកបន្ថែមនូវ ការផ្ទៀងផ្ទាត់(authentication) និង ការអនុញ្ញាតកម្មវិធីគេហទំព័ររបស់អ្នក(authorization)។
វាក៏អាចរួមបញ្ចូលយ៉ាងល្អជាមួយ frameworks ដទៃដូចជា Spring Web MVC (ឬ Spring Boot) ក៏ដូចជា OAuth2 ឬ SAML ផងដែរ។ ហើយវាបង្កើត ទំព័រចូល/ចេញ (login/logout pages) ដោយស្វ័យប្រវត្តិ និងការពារប្រឆាំងនឹងការហែកចូលគេហទំព័រទូទៅដូចជា CSRF ជាដើម។
បើសិនជាអ្នកចង់ប្រើ dependency មួយនេះ អ្នកត្រូវបន្ថែមនូវ dependency មួយនេះនៅក្នុងឯកសារ build.gradle.kts:
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-security")
  testImplementation("org.springframework.security:spring-security-test")
}
បន្ទាប់ពីអ្នកបន្ថែមនូវ spring-boot-starter-security's dependency រួចរាល់គឺអ្នកត្រូវបន្ថែមនូវ dependency មួយទៀតដើម្បីអាចប្រើ jwt:
dependencies {
  implementation("io.jsonwebtoken:jjwt:0.9.1")
}
សម្រាប់ក្នុងមេរៀននេះគឺនឹងនាំអ្នកទៅប្រើប្រាស់ Spring Boot Security ជាមួយនឹង JWT។
package com.springboot.security.config
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.http.SessionCreationPolicy
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SpringSecurityConfig {
  @Bean
  @Throws(Exception::class)
  fun filterChain(http: HttpSecurity): SecurityFilterChain? {
    // basic http configurer to use with token service
    http
      .httpBasic().disable()
      .cors().and()
      .csrf().disable()
      .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
    // authorization request filters
    http
      .authorizeRequests {
        it.anyRequest().permitAll()
      }
    return http.build()
  }
  @Bean
  fun getPasswordEncoder(): PasswordEncoder {
    return BCryptPasswordEncoder(10)
  }
}
package com.springboot.security.modules.security
import com.springboot.security.model.entity.UserEntity
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class UserAuthDetails @Autowired constructor(
    private val user: UserEntity,
) : UserDetails {
  fun getUser() = user
  override fun getAuthorities(): MutableCollection<out GrantedAuthority> {
    return this.user.roles?.map { role -> SimpleGrantedAuthority("ROLE_${role.name}") }?.toMutableList()!!
  }
  override fun getPassword(): String = this.user.password ?: ""
  override fun getUsername(): String = this.user.username ?: ""
  override fun isAccountNonExpired(): Boolean = true
  override fun isAccountNonLocked(): Boolean = true
  override fun isCredentialsNonExpired(): Boolean = true
  override fun isEnabled(): Boolean = this.user.enabledUser ?: false
}
package com.springboot.security.modules.security.util
import com.springboot.security.modules.security.exception.*
import io.jsonwebtoken.*
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.util.*
import javax.servlet.http.HttpServletRequest
object JwtUtils {
  private const val AUTHORIZATION_HEADER: String = "Authorization"
  private const val AUTHORIZATION_TYPE: String = "Bearer "
  
  private var secretKey: String = "blog123"
  private var tokenExpiredInMillis: Long = 604800000 // 1 week
  private var passwordStrength: Int = 10
  private var passwordEncoder: PasswordEncoder? = null
  private var userDetailsService: UserDetailsService? = null
  
  // setters
  fun setUserDetailsService(_userDetailsService: UserDetailsService) = apply {
    this.userDetailsService = _userDetailsService
  }
  
  fun setPasswordStrength(_passwordStrength: Int) = apply {
    this.passwordStrength = _passwordStrength
  }
  fun setSecretKey(_secretKey: String) = apply {
    this.secretKey = _secretKey
  }
  fun setTokenExpiredInMillis(_tokenExpiredInMillis: Long) = apply {
    this.tokenExpiredInMillis = _tokenExpiredInMillis
  }
    
  // getters
  fun getUserDetailsService(): UserDetailsService {
    if (this.userDetailsService == null)
      throw JwtNotImplementException()
    return this.userDetailsService!!
  }
  fun getPasswordEncoder(): PasswordEncoder {
    if (this.passwordEncoder == null)
      this.passwordEncoder = BCryptPasswordEncoder(this.passwordStrength)
    return this.passwordEncoder!!
  }
  private fun getSecretKey(): String = Base64.getEncoder().encodeToString(secretKey.toByteArray())
  private fun getTokenExpiredInMillis(): Long = this.tokenExpiredInMillis
  fun extractToken(request: HttpServletRequest): String? {
    val headerToken = request.getHeader(AUTHORIZATION_HEADER)?.toString() ?: ""
    val isBearerToken = headerToken.trim().lowercase().startsWith(AUTHORIZATION_TYPE.lowercase())
    if (!isBearerToken)
      return null
    val token = headerToken.substring(AUTHORIZATION_TYPE.length)
    val isValidJwtThreePart = token.split(".").size == 3
    if (!isValidJwtThreePart)
      return null
    return token
  }
  private fun validateTokenExpired(claims: Claims): Boolean {
    if (claims.expiration.after(Date()))
      return true
    return false
  }
  fun resolveUserFromToken(token: String?): UsernamePasswordAuthenticationToken? {
      val claims = this.decryptToken(token) ?: return null
      val isTokenExpired = this.validateTokenExpired(claims)
    
      if (!isTokenExpired)
        return null
      val username = claims.subject
      val user = this.getUserDetailsService().loadUserByUsername(username) ?: return null
      if (!user.isEnabled)
        throw UserNotEnabledException("User is disabled!")
      return resolveAuthFromUser(user)
  }
  private fun resolveAuthFromUser(user: UserDetails): UsernamePasswordAuthenticationToken {
    val auth = UsernamePasswordAuthenticationToken(user.username, user.password, user.authorities)
    auth.details = user
    return auth
  }
  fun encryptToken(user: UserDetails): String {
    val currentDateMillisecond = Date().time + this.getTokenExpiredInMillis()
    val expireDate = Date(currentDateMillisecond)
    return Jwts.builder()
        .setSubject(user.username)
        .setIssuedAt(Date())
        .setExpiration(expireDate)
        .signWith(SignatureAlgorithm.HS256, this.getSecretKey())
        .compact()
  }
  private fun decryptToken(token: String?): Claims? {
    token ?: return null
    val secretKey = this.getSecretKey()
    return try {
        Jwts.parser()
            .setSigningKey(secretKey)
            .parse(token)
            .body as? Claims
    } catch (ex: SignatureException) {
      throw SignatureTokenException("Invalid JWT Signature")
    } catch (ex: MalformedJwtException) {
      throw MalformedJwtTokenException("Invalid JWT token")
    } catch (ex: ExpiredJwtException) {
      throw ExpiredJwtTokenException("Expired JWT token")
    } catch (ex: UnsupportedJwtException) {
      throw UnsupportedJwtTokenException("Unsupported JWT exception")
    } catch (ex: IllegalArgumentException) {
      throw EmptyJwtClaimsException("Jwt claims string is empty")
    }
  }
}
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class EmptyJwtClaimsException(
  message: String? = "",
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class ExpiredJwtTokenException(
  message: String? = "",
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class JwtNotImplementException(
  message: String? = "User details service not implement yet!"
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class MalformedJwtTokenException(
  message: String? = "",
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class SignatureTokenException(
  message: String? = "",
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class UnsupportedJwtTokenException(
  message: String? = "",
) : BaseException(message)
package com.springboot.security.modules.security.exception
import com.springboot.security.exception.BaseException
class UserNotEnabledException(
  message: String? = "",
) : BaseException(message)