작고 안전한 웹 어플리케이션

어플리케이션의 제품 정보와 사용자는 데이터베이스에 저장되고, 각 사용자의 암호는 BCrypt를 사용하여 암호화된다.

이 작은 프로젝트의 구현은 다음과 같다

  1. 데이터베이스

  2. 사용자 관리

  3. 인증 논리 구현

  4. 주 페이지

  5. 실행 및 테스트

사용자 관리

사용자는 데이터베이스에 저장되며, 사용자의 암호는 BCrypt를 사용하여 암호화된다.

PasswordEncoder 등록

@Configuration
class UserManagementConfig {
    @Bean
    fun bCryptPasswordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    @Bean
    fun sCryptPasswordEncoder(): PasswordEncoder = SCryptPasswordEncoder()

}

User 등록

USER ENTITY
@Entity
class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,
    val username: String,
    val password: String,

    @Enumerated(EnumType.STRING)
    val algorithm: EncryptAlgorithm,

    @OneToMany
    val authorities: List<Authority>
) {

    enum class EncryptAlgorithm {
        BCRYPT, SCRYPT
    }
}

interface UserRepository: JpaRepository<User, Long> {

    fun findByUsername(username: String): User?
}
AUTHORITY ENTITY
@Entity
class Authority(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0L,
    val name: String,
    @ManyToOne
    @JoinColumn(name = "user")
    val user: User
) {

}

interface AuthorityRepository: JpaRepository<Authority, Long> {
}

인증 논리 구현

UserDetails
class CustomUserDetails(
    private val user: User
): UserDetails {
    override fun getAuthorities(): List<GrantedAuthority> = user.authorities
        .map { authority -> GrantedAuthority { authority.name } }
        .toList()

    override fun getPassword(): String = user.password

    override fun getUsername(): String = user.username

    override fun isAccountNonExpired(): Boolean = true

    override fun isAccountNonLocked(): Boolean = true

    override fun isCredentialsNonExpired(): Boolean = true

    override fun isEnabled(): Boolean = true
}
UserDetailsService
@Service
class CustomUserDetailService(
    private val userRepository: UserRepository
) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails = userRepository.findByUsername(username)
        ?.let { CustomUserDetails(it) }
        ?: throw UsernameNotFoundException("User not found")
}
AuthenticationProvider
@Service
class CustomAuthenticationProvider(
    private val userDetailService: CustomUserDetailService,
) : AuthenticationProvider {
    private val sCryptPasswordEncoder: SCryptPasswordEncoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
    private val bCryptPasswordEncoder: BCryptPasswordEncoder = BCryptPasswordEncoder()

    override fun authenticate(authentication: Authentication): Authentication {
        val name = authentication.name
        val password = authentication.credentials.toString()
        val user: CustomUserDetails = userDetailService.loadUserByUsername(name)

        return when (user.getUser.algorithm) {
            BCRYPT -> {
                bCryptPasswordEncoder.matches(password, user.password)
            }
            SCRYPT -> {
                sCryptPasswordEncoder.matches(password, user.password)
            }
        }
            .takeIf { it }
            ?.let { return UsernamePasswordAuthenticationToken(user.username, user.password, user.authorities) }
            ?: throw BadCredentialsException("Bad credentials")
    }

    override fun supports(authentication: Class<*>): Boolean = UsernamePasswordAuthenticationToken::class.java.isAssignableFrom(authentication)
}
SecurityConfiguration
@Configuration
@EnableWebSecurity
class SecurityConfiguration(
    private val authenticationProvider: CustomAuthenticationProvider
) {


    @Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http
        .formLogin { it.loginPage("/login")
            .successHandler { _, response, _ -> response.sendRedirect("/hello") }
            .failureHandler { _, response, _ -> response.sendRedirect("/error") }
        }
        .authorizeHttpRequests { it.anyRequest().authenticated() }
        .authenticationProvider(authenticationProvider)
        .build()

}

Last updated