๐Ÿ”ฅ TIL - Day 72 Kotlin & Springboot 03 Spring Security ์ ์šฉํ•˜๊ธฐ (Jwt๋ฐฉ์‹ ์ธ์ฆ)

Kim Dae Hyunยท2021๋…„ 12์›” 14์ผ
3

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
83/93

์ „์ฒด์†Œ์Šค Github

Spring Security๋ณด๋‹ค๋Š” Kotlin์„ ์ด์šฉํ•ด๋ณด๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ด๋ฏ€๋กœ ๊ต‰์žฅํžˆ ๊ฐ„๋‹จํ•˜๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋‹ค.
(๊ถŒํ•œ ์—†์Œ)

๐Ÿ“Œ Entity

data ํด๋ž˜์Šค๋Š” ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— ์ ํ•ฉํ•˜์ง€ ์•Š๋‹ค๊ณ  ํŒ๋‹จ๋˜์–ด ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜๊ณ  setter์˜ ๊ฒฝ์šฐ ์ ‘๊ทผ์„ ๋ง‰๊ณ  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ฐ˜์˜ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ–ˆ๋‹ค.

@Entity
@Table(name = "MEMBRE_TBL")
class Member (
    username: String,
    password: String,
    ) : Timestamped() {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null

    @Column(nullable = false, unique = true)
    var username: String = username
        protected set // setter ์ ‘๊ทผ๊ธˆ์ง€ (๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ๋ฐ˜์˜ => updateUsername)

    @Column(nullable = false)
    var password: String = password
        protected set

    @OneToMany(mappedBy = "member")
    val boards: MutableList<Board> = ArrayList()

    fun updateMember(memberDto: MemberDto) {
        this.username = memberDto.username
        this.password = memberDto.password
    }
    
    fun updateUsername(username: String) {
        this.username = username
    }

    fun updatePassword(password: String) {
        this.password = password
    }
}

๐Ÿ“Œ Repository

์ดํ›„ SpringSecurity์˜ UserDetailsService์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•ด username์œผ๋กœ ์กฐํšŒํ•˜๋Š” ์ฟผ๋ฆฌ๋งŒ ํ•˜๋‚˜ ์ •์˜ํ•œ๋‹ค.

interface MemberRepository : JpaRepository<Member, Long> {
    fun findByUsername(username: String) : Member?
}

๐Ÿ“Œ WebSecurityConfig (SpringSecurity ์„ค์ •ํŒŒ์ผ ์ •์˜)

  • post, delete ๋“ฑ ์š”์ฒญ์„ ํ…Œ์ŠคํŠธํ•ด๋ณด๊ธฐ ์œ„ํ•ด csrf๋Š” disable ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • signup(ํšŒ์›๊ฐ€์ž…), signin(์ธ์ฆ) ๋งŒ ์ ‘๊ทผ์„ ํ—ˆ์šฉํ•˜๊ณ  ๋‹ค๋ฅธ API ๋ฐ ๋ฆฌ์†Œ์Šค๋Š” ๋ชจ๋‘ ์ธ์ฆ์„ ํ•„์š”๋กœ ํ•˜๋„๋ก ์„ค์ •ํ•œ๋‹ค.
  • ํ† ํฐ๋ฐฉ์‹ ์ธ์ฆ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ session์€ STATELESS ๋กœ ์„ค์ •ํ•œ๋‹ค.
  • jwtํ† ํฐ์„ ์ˆ˜ํ–‰ํ•˜๋Š” JwtFilter๋ฅผ ๋งŒ๋“ค๊ณ  UsernamePasswordAuthenticationFilter ์ด์ „์— ๋ฐฐ์น˜ํ•˜์—ฌ ๋จผ์ € ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • authenticationManagerBean ๋กœ AuthenticationManager๋ฅผ ์ฃผ์ž…๋ฐ›์•„์„œ Jwtํ† ํฐ์ด ์œ ํšจํ•œ ๊ฒฝ์šฐ ์ž„์œผ๋กœ ์ธ์ฆ๋˜์ง€ ์•Š์€ AuthenticationToken์— ๋Œ€ํ•ด ์ธ์ฆ์ฒ˜๋ฆฌ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.
@Configuration
@EnableWebSecurity
class WebSecurityConfig(
    private val userDetailsService: UserDetailsServiceImpl,
    private val jwtFilter: JwtFilter
    ) : WebSecurityConfigurerAdapter() {

    override fun configure(http: HttpSecurity) {

        http.csrf().disable()

        http.authorizeRequests()
            .antMatchers("/api/members/signup", "/api/members/signin").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter::class.java)
    }

    @Bean
    fun passwordEncoder() = BCryptPasswordEncoder()

    @Bean
    override fun authenticationManagerBean(): AuthenticationManager {
        return super.authenticationManagerBean()
    }
}

๐Ÿ“Œ UserDetails & UserDetailsService ์ปค์Šคํ…€

SpringSecurity๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” principal ๊ฐ์ฒด์ธ UserDetails ๋ฅผ ์šฐ๋ฆฌ์˜ Member๊ฐ€ ๋˜๋„๋ก ์ปค์Šคํ…€ํ•˜๊ณ  UserDetailsService ๋˜ํ•œ Member ํƒ€์ž…์„ ์ธ์ฆํ•˜๋„๋ก ํ•˜๊ธฐ ์œ„ํ•จ์ด๋‹ค.

UserDetails

UserDetails๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  Member ์—”ํ‹ฐํ‹ฐ๋ฅผ ์ƒ์„ฑ์ž๋กœ ๋ฐ›์•„์„œ Member์˜ ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•˜๋„๋ก ํ•œ๋‹ค.
์—ฌ๊ธฐ์„œ Member๋Š” UserDetailsService์—์„œ ๊ฒ€์ฆ(DB์กฐํšŒ) ํ›„์— ๋„˜์–ด์˜ค๋Š” ๊ฐ์ฒด์ด๋‹ค.

class UserDetailsImpl(val member: Member) : UserDetails {

    var enabled: Boolean = true

    override fun getAuthorities(): MutableCollection<out GrantedAuthority> = AuthorityUtils.createAuthorityList()

    override fun getPassword(): String = member.password

    override fun getUsername(): String = member.password

    override fun isAccountNonExpired(): Boolean = enabled

    override fun isAccountNonLocked(): Boolean = enabled

    override fun isCredentialsNonExpired(): Boolean = enabled

    override fun isEnabled(): Boolean = enabled
}

UserDetailsService

MemberRepository๋ฅผ DI๋ฐ›์•„ username์— ๋Œ€ํ•œ ๊ฒ€์ฆ(DB์กฐํšŒ)์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ๊ทธ ๊ฒฐ๊ณผ๋ฅผ UserDetails๋กœ ๋„˜๊ฒจ์ค€๋‹ค.

@Service
class UserDetailsServiceImpl(private val memberRepository: MemberRepository) : UserDetailsService {

    override fun loadUserByUsername(username: String): UserDetails {
        val member: Member = memberRepository.findByUsername(username) ?: throw UsernameNotFoundException("์กด์žฌํ•˜์ง€ ์•Š๋Š” username ์ž…๋‹ˆ๋‹ค.")

        return UserDetailsImpl(member)
    }
}

๐Ÿ“Œ JwtUtils ํด๋ž˜์Šค (Jwt ์ƒ์„ฑ ๋ฐ ๊ฒ€์ฆ์„ ์œ„ํ•œ ์œ ํ‹ธ ํด๋ž˜์Šค)

@Component
class JwtUtils(private val userDetailsService: UserDetailsServiceImpl) {

    val EXP_TIME: Long = 1000L * 60 * 3
    val JWT_SECRET: String = "secret"
    val SIGNATURE_ALG: SignatureAlgorithm = SignatureAlgorithm.HS256

    // ํ† ํฐ์ƒ์„ฑ
    fun createToken(username: String): String {
        val claims: Claims = Jwts.claims();
        claims["username"] = username

        return Jwts.builder()
            .setClaims(claims)
            .setExpiration(Date(System.currentTimeMillis()+ EXP_TIME))
            .signWith(SIGNATURE_ALG, JWT_SECRET)
            .compact()
    }
	
    // ํ† ํฐ๊ฒ€์ฆ
    fun validation(token: String) : Boolean {
        val claims: Claims = getAllClaims(token)
        val exp: Date = claims.expiration
        return exp.after(Date())
    }
    
    // ํ† ํฐ์—์„œ username ํŒŒ์‹ฑ
    fun parseUsername(token: String): String {
        val claims: Claims = getAllClaims(token)
        return claims["username"] as String
    }
    
    // username์œผ๋กœ Authentcation๊ฐ์ฒด ์ƒ์„ฑ
    fun getAuthentication(username: String): Authentication {
        val userDetails: UserDetails = userDetailsService.loadUserByUsername(username)

        return UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities)
    }
    
    
    // ๋ชจ๋“  Claims ์กฐํšŒ
    private fun getAllClaims(token: String): Claims {
        return Jwts.parser()
            .setSigningKey(JWT_SECRET)
            .parseClaimsJws(token)
            .body
    }
}

๐Ÿ“Œ Jwt ๊ฒ€์ฆํ•„ํ„ฐ ๊ตฌํ˜„

@Component
class JwtFilter(private val jwtUtils: JwtUtils) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        // ํ—ค๋”์— Authorization์ด ์žˆ๋‹ค๋ฉด ๊ฐ€์ ธ์˜จ๋‹ค.
        val authorizationHeader: String? = request.getHeader("Authorization") ?: return filterChain.doFilter(request, response)
        // Bearerํƒ€์ž… ํ† ํฐ์ด ์žˆ์„ ๋•Œ ๊ฐ€์ ธ์˜จ๋‹ค.
        val token = authorizationHeader?.substring("Bearer ".length) ?: return filterChain.doFilter(request, response)

        // ํ† ํฐ ๊ฒ€์ฆ
        if (jwtUtils.validation(token)) {
            // ํ† ํฐ์—์„œ username ํŒŒ์‹ฑ
            val username = jwtUtils.parseUsername(token)
            // username์œผ๋กœ AuthenticationToken ์ƒ์„ฑ
            val authentication: Authentication = jwtUtils.getAuthentication(username)
            // ์ƒ์„ฑ๋œ AuthenticationToken์„ SecurityContext๊ฐ€ ๊ด€๋ฆฌํ•˜๋„๋ก ์„ค์ •
            SecurityContextHolder.getContext().authentication = authentication
        }

        filterChain.doFilter(request, response)
    }
}

๐Ÿ“Œ Service ๊ณ„์ธต ๊ตฌํ˜„ (๋กœ๊ทธ์ธ ์ฒ˜๋ฆฌ)

ํ† ํฐ์ด ์—†๋Š” ์ƒํƒœ๋กœ ์ตœ์ดˆ ๋กœ๊ทธ์ธ์„ ํ•˜๊ฒŒ ๋˜๋ฉด username๊ณผ password๋ฅผ ๋“ค๊ณ  ๋“ค์–ด์˜ฌ ๊ฒƒ์ด๋‹ค.
ํด๋ผ์ด์–ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ›์€ username๊ณผ password๋กœ ์ธ์ฆ์„ ์‹œ๋„ํ•˜๊ณ  ์ธ์ฆ์— ์„ฑ๊ณตํ–ˆ๋‹ค๋ฉด ํ† ํฐ์„ ์ƒ์„ฑํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ตํ•œ๋‹ค.

@Service
class MemberService(
    private val memberRepository: MemberRepository,
    private val passwordEncoder: PasswordEncoder,
    private val authenticationManager: AuthenticationManager,
    private val jwtUtils: JwtUtils
) {

    @Transactional(readOnly = true)
    fun signin(memberDto: MemberDto): String {
        try {
            // ์ธ์ฆ์‹œ๋„
            authenticationManager.authenticate(
                UsernamePasswordAuthenticationToken(memberDto.username, memberDto.password, null)
            )
        } catch (e: BadCredentialsException) {
            throw BadCredentialsException("๋กœ๊ทธ์ธ ์‹คํŒจ")
        }
        // ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์ธ์ฆ์— ์„ฑ๊ณตํ•œ ๊ฒƒ. 
        // ํ† ํฐ ์ƒ์„ฑ
        val token = jwtUtils.createToken(memberDto.username)
       
        return token
    }
}

๐Ÿ“Œ Controller ๊ณ„์ธต ๊ตฌํ˜„

์ธ์ฆ์ •๋ณด(username, password)๋ฅผ ๋ฐ›์•„ service๊ณ„์ธต์˜ ์ธ์ฆ๋กœ์ง์„ ์ˆ˜ํ–‰ํ•˜๊ณ  ์˜ˆ์™ธ์—†์ด ๋ฆฌํ„ด๋˜๋ฉด ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์‘๋‹ต์œผ๋กœ ๋‚ด๋ ค์ค€๋‹ค.

@RequestMapping("/api/members")
@RestController
class MemberController(private val memberService: MemberService ) {

    @PostMapping("/signin")
    fun signin(@RequestBody memberDto: MemberDto) = ResponseEntity.ok().body(memberService.signin(memberDto))

}

๐Ÿ“Œ ํ…Œ์ŠคํŠธ

@Transactional
@AutoConfigureMockMvc
@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // BeforeAll
class MemberApiControllerTest {

    @Test
    @DisplayName("๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ")
    fun `๋กœ๊ทธ์ธ ํ…Œ์ŠคํŠธ jwt ํ† ํฐ๋ฐœ๊ธ‰ ํ…Œ์ŠคํŠธ` () {

        val signinDto: SigninDto = SigninDto("test", "test")
        val signinDtoJson = objectMapper.writeValueAsString(signinDto)

        mockMvc.post("/api/members/signin")
        {
            contentType = MediaType.APPLICATION_JSON
            content = signinDtoJson
        }
            .andExpect {
                status { isOk() }
            }
            .andDo {
                print()
            }
    }
}

Postman ํ…Œ์ŠคํŠธ

profile
์ข€ ๋” ์ฒœ์ฒœํžˆ ๊นŒ๋จน๊ธฐ ์œ„ํ•ด ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. ๐Ÿง

0๊ฐœ์˜ ๋Œ“๊ธ€