Spring Security๋ณด๋ค๋ Kotlin์ ์ด์ฉํด๋ณด๋ ๊ฒ์ด ๋ชฉ์ ์ด๋ฏ๋ก ๊ต์ฅํ ๊ฐ๋จํ๊ฒ ๊ตฌํํ๋ค.
(๊ถํ ์์)
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
}
}
์ดํ SpringSecurity์ UserDetailsService์์ ์ฌ์ฉํ๊ธฐ ์ํด username์ผ๋ก ์กฐํํ๋ ์ฟผ๋ฆฌ๋ง ํ๋ ์ ์ํ๋ค.
interface MemberRepository : JpaRepository<Member, Long> {
fun findByUsername(username: String) : Member?
}
signup
(ํ์๊ฐ์
), signin
(์ธ์ฆ) ๋ง ์ ๊ทผ์ ํ์ฉํ๊ณ ๋ค๋ฅธ API ๋ฐ ๋ฆฌ์์ค๋ ๋ชจ๋ ์ธ์ฆ์ ํ์๋ก ํ๋๋ก ์ค์ ํ๋ค.STATELESS
๋ก ์ค์ ํ๋ค.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()
}
}
SpringSecurity๊ฐ ๊ด๋ฆฌํ๋ principal ๊ฐ์ฒด์ธ UserDetails ๋ฅผ ์ฐ๋ฆฌ์ Member๊ฐ ๋๋๋ก ์ปค์คํ ํ๊ณ UserDetailsService ๋ํ Member ํ์ ์ ์ธ์ฆํ๋๋ก ํ๊ธฐ ์ํจ์ด๋ค.
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
}
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)
}
}
@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
}
}
@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)
}
}
ํ ํฐ์ด ์๋ ์ํ๋ก ์ต์ด ๋ก๊ทธ์ธ์ ํ๊ฒ ๋๋ฉด 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
}
}
์ธ์ฆ์ ๋ณด(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()
}
}
}