본문 바로가기

카테고리 없음

24.02.05 복습 댓글 돌입

  • 댓글 작성 API
    • [ ] 게시글과 연관 관계를 가진 댓글 테이블 추가
    • [ ] 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
    • [ ] 작성 내용을 입력하기
    • [ ] 게시글에 대한 좋아요
@PostMapping
    fun createComment(@RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.createComment(dto)
        return ResponseEntity.ok(commentDto)
    }
    이걸
    
@PostMapping
fun createComment(@AuthenticationPrincipal user: UserPrincipal, @RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
    val commentDto = commentService.createComment(user, dto)
    return ResponseEntity.ok(commentDto)
} 이렇게 @AuthenticationPrincipal user: UserPrincipal을 넣는다.

이걸 만들어야 한다.

@Entity
@Table(name = "comments")
data class Comment(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    var post: Post,

    val nickname: String,

    @Column(length = 5000)
    val content: String,

    @CreatedDate
    val createdAt: LocalDateTime = LocalDateTime.now(),

    @LastModifiedDate
    val updatedAt: LocalDateTime = LocalDateTime.now()
)

이걸로 [ ] 게시글과 연관 관계를 가진 댓글 테이블 추가를 달성.

 

이제 리포지터리를 만든다.

interface CommentRepository : JpaRepository<Comment, Long> {
    fun findByPostId(postId: Long): List<Comment>
}

findbypostid를 쓰는 이유는 특정 게시글에 달린 댓글을 조회하기 위해서이다.

 

다음으론 dto를 만든다.

 

package com.example.demo.dto

import java.time.LocalDateTime

data class CommentDto(
    val id: Long,
    val postId: Long,
    val nickname: String,
    val content: String,
    val createdAt: LocalDateTime,
    val updatedAt: LocalDateTime
)

이것은 먼저 댓글에 필요한 댓글의 아이디, 연결된 게시글의 아이디, 그걸 쓸 닉네임, 댓글 내용, 날짜를 나타낸다.

 

data class CommentCreateDto(
    val postId: Long,
    val nickname: String,
    val content: String
)

 

이것은 댓글을 만들때 게시글의 아이디, 닉네임, 내용이 필요하단걸 보여주고

 

data class CommentUpdateDto(
    val content: String
)

 

수정은 내용만 할수 있다는걸 보여준다.

 

서비스는 이렇게

 

interface CommentService {
    fun createComment(dto: CommentCreateDto): CommentDto
    fun updateComment(id: Long, dto: CommentUpdateDto): CommentDto
    fun getComment(id: Long): CommentDto
    fun deleteComment(id: Long)
}

 

서비스임플은 이렇게

package com.example.demo.service

import com.example.demo.dto.CommentCreateDto
import com.example.demo.dto.CommentDto
import com.example.demo.dto.CommentUpdateDto
import com.example.demo.model.Comment
import com.example.demo.repository.CommentRepository
import com.example.demo.repository.PostRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
class CommentServiceImpl(
    private val commentRepository: CommentRepository,
    private val postRepository: PostRepository
) : CommentService {

    @Transactional
    override fun createComment(dto: CommentCreateDto): CommentDto {
        val post = postRepository.findById(dto.postId)
            .orElseThrow { IllegalArgumentException("Post not found") }

        val comment = Comment(
            post = post,
            nickname = dto.nickname,
            content = dto.content
        )

        commentRepository.save(comment)

        return CommentDto.from(comment)
    }

    @Transactional
    override fun updateComment(id: Long, dto: CommentUpdateDto): CommentDto {
        val comment = commentRepository.findById(id)
            .orElseThrow { IllegalArgumentException("Comment not found") }

        comment.content = dto.content

        return CommentDto.from(comment)
    }

    @Transactional(readOnly = true)
    override fun getComment(id: Long): CommentDto {
        val comment = commentRepository.findById(id)
            .orElseThrow { IllegalArgumentException("Comment not found") }

        return CommentDto.from(comment)
    }

    @Transactional
    override fun deleteComment(id: Long) {
        commentRepository.deleteById(id)
    }
}

 

하지만, 이러면 from은 빨간줄이 뜨고 인식을 못한다! 그때 필요한것이

 

package com.example.demo.dto

import com.example.demo.model.Post
import java.time.LocalDateTime

data class PostDto(
    val id: Long,
    val title: String,
    val nickname: String,
    val content: String,
    val createdAt: LocalDateTime
) {
    companion object {
        fun from(post: Post): PostDto {
            return PostDto(
                id = post.id!!,
                title = post.title,
                nickname = post.nickname,
                content = post.content,
                createdAt = post.createdAt
            )
        }
    }
}

 

이제 from은 작동한다. !!는 오류가 뜨길래 추가했다.

 

컨트롤러는

package com.example.demo.controller

import com.example.demo.dto.CommentCreateDto
import com.example.demo.dto.CommentDto
import com.example.demo.dto.CommentUpdateDto
import com.example.demo.service.CommentService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/comments")
class CommentController(private val commentService: CommentService) {

    @PostMapping
    fun createComment(@RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.createComment(dto)
        return ResponseEntity.ok(commentDto)
    }

    @PutMapping("/{id}")
    fun updateComment(@PathVariable id: Long, @RequestBody dto: CommentUpdateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.updateComment(id, dto)
        return ResponseEntity.ok(commentDto)
    }

    @GetMapping("/{id}")
    fun getComment(@PathVariable id: Long): ResponseEntity<CommentDto> {
        val commentDto = commentService.getComment(id)
        return ResponseEntity.ok(commentDto)
    }

    @DeleteMapping("/{id}")
    fun deleteComment(@PathVariable id: Long): ResponseEntity<Unit> {
        commentService.deleteComment(id)
        return ResponseEntity.noContent().build()
    }
}

 

이제 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능을 넣어야 한다.

 

근데 하고 보니까, 인증된 토큰만 맏는다는게 SecurityConfig를 이용한다는거여서 다시 내용들을 바꾸기로 했다. 

기나긴 시행 착오를 견딘 끝에, 강의와 조별과제를 참고하기로 했다.

 

package com.example.demo.security

import com.example.demo.jwt.JwtAuthenticationFilter
import org.springframework.boot.autoconfigure.security.reactive.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.web.AuthenticationEntryPoint
import org.springframework.security.web.SecurityFilterChain
import org.springframework.security.web.access.AccessDeniedHandler
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig(
    private val jwtAuthenticationFilter: JwtAuthenticationFilter,
    private val authenticationEntrypoint: AuthenticationEntryPoint,
    private val accessDeniedHandler: AccessDeniedHandler
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .httpBasic { it.disable() }
            .formLogin { it.disable() }
            .csrf { it.disable() }
            .authorizeHttpRequests {
                it.requestMatchers(
                    "/member/login",
                    "/member/sign_up",
                    "/swagger-ui/**",
                    "/v3/api-docs/**"
                ).permitAll()
                    .anyRequest().authenticated()
            }
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
            .exceptionHandling{
                it.authenticationEntryPoint(authenticationEntrypoint)
                it.accessDeniedHandler(accessDeniedHandler)
            }
            .build()
    }

}

 

이러면 이제 로그인, 회원가입 외에는 인가가 필요해진다. 그런데, 여기서 조회만 예외로 하면 된다.

 

package com.example.demo.jwt

import com.example.demo.security.UserPrincipal
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.web.authentication.WebAuthenticationDetails

class JwtAuthenticationToken(
    private val principal: UserPrincipal,
    details: WebAuthenticationDetails
): AbstractAuthenticationToken(principal.authorities) {

    init {
        super.setAuthenticated(true)
        super.setDetails(details)
    }

    override fun getCredentials() = null


    override fun getPrincipal() = principal

    override fun isAuthenticated(): Boolean {
        return true
    }
}

 

 

package com.example.demo.jwt


import com.example.demo.security.UserPrincipal
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.http.HttpHeaders
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
    private val jwtPlugin: JwtPlugin
):OncePerRequestFilter() {

    companion object {
        private val BEARER_PATTERN = Regex("^Bearer (.+?)$")
    }

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val jwt = request.getBearerToken()

        if (jwt != null) {
            jwtPlugin.validateToken(jwt)
                .onSuccess {
                    val memberId = it.payload.subject.toLong()
                    val role = it.payload.get("role", String::class.java)
                    val nickName = it.payload.get("nickName", String::class.java)

                    val principal = UserPrincipal(
                        id = memberId,
                        nickName = nickName,
                        roles = setOf(role)
                    )

                    val authentication = JwtAuthenticationToken(
                        principal = principal,
                        details = WebAuthenticationDetailsSource().buildDetails(request)
                    )
                    SecurityContextHolder.getContext().authentication = authentication

                }
        }

        filterChain.doFilter(request, response)
    }

    private fun HttpServletRequest.getBearerToken(): String? {
        val headerValue = this.getHeader(HttpHeaders.AUTHORIZATION) ?: return null
        return BEARER_PATTERN.find(headerValue)?.groupValues?.get(1)
    }
}

 

 

package com.example.demo.security

import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority

data class UserPrincipal(
    val id: Long,
    val nickName: String,
    val authorities: Collection<GrantedAuthority>
) {
    constructor(id: Long , nickName: String , roles: Set<String>): this(
        id, nickName, roles.map { SimpleGrantedAuthority("ROLE_$it") }
    )
}

 

이렇게 추가해주고

package com.example.demo.controller

import com.example.demo.dto.CommentCreateDto
import com.example.demo.dto.CommentDto
import com.example.demo.dto.CommentUpdateDto
import com.example.demo.service.CommentService
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/comments")
class CommentController(private val commentService: CommentService) {

    @PostMapping
    fun createComment(@RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.createComment(dto)
        return ResponseEntity.ok(commentDto)
    }

    @PutMapping("/{id}")
    fun updateComment(@PathVariable id: Long, @RequestBody dto: CommentUpdateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.updateComment(id, dto)
        return ResponseEntity.ok(commentDto)
    }

    @GetMapping("/{id}")
    fun getComment(@PathVariable id: Long): ResponseEntity<CommentDto> {
        val commentDto = commentService.getComment(id)
        return ResponseEntity.ok(commentDto)
    }

    @DeleteMapping("/{id}")
    fun deleteComment(@PathVariable id: Long): ResponseEntity<Unit> {
        commentService.deleteComment(id)
        return ResponseEntity.noContent().build()
    }
}

 

이걸 바꿔야 한다.

 

 

@PostMapping
    fun createComment(@RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
        val commentDto = commentService.createComment(dto)
        return ResponseEntity.ok(commentDto)
    }
이녀석을
@PostMapping
fun createComment(@AuthenticationPrincipal user: UserPrincipal, @RequestBody dto: CommentCreateDto): ResponseEntity<CommentDto> {
    val commentDto = commentService.createComment(user, dto)
    return ResponseEntity.ok(commentDto)
}

 

 

그런데, (user, dto)에서 dtㅐ가 빨간줄이 뜬다. 그래서,

 

interface CommentService {
    fun createComment(dto: CommentCreateDto): CommentDto
    이녀석을
    fun createComment(userId: Long, dto: CommentCreateDto): CommentDto
    이렇게 바꾼다.
    fun updateComment(id: Long, dto: CommentUpdateDto): CommentDto
    fun getComment(id: Long): CommentDto
    fun deleteComment(id: Long)
}

 

그런데, class CommentServiceImpl이 빨간줄이 뜬다.

e: file:///C:/Users/asdf/Desktop/demo/src/main/kotlin/com/example/demo/service/CommentServiceImpl.kt:13:1 Class 'CommentServiceImpl' is not abstract and does not implement abstract member public abstract fun createComment(userId: Long, dto: CommentCreateDto): CommentDto defined in cohttp://m.example.demo.service.CommentService 이것이 에로코드!

 

@Transactional
override fun createComment(dto: CommentCreateDto): CommentDto {
    val post = postRepository.findById(dto.postId)
        .orElseThrow { IllegalArgumentException("Post not found") }

    val comment = Comment(
        post = post,
        nickname = dto.nickname,
        content = dto.content
    )

    commentRepository.save(comment)

    return CommentDto.from(comment)
}

 

이게 문제인 모양이다. 오류 메시지를 보면, CommentService 인터페이스에서 정의한 createComment 함수가 CommentServiceImpl 클래스에서 구현되지 않았다는 내용이라는데, service만 바꾸고 servicrimpl은 바꾸지 않아서 생긴 문제인 모양이다.

 

@Transactional
override fun createComment(userId: Long, dto: CommentCreateDto): CommentDto {
    val post = postRepository.findById(dto.postId)
        .orElseThrow { IllegalArgumentException("Post not found") }

    val comment = Comment(
        post = post,
        nickname = dto.nickname,
        content = dto.content
    )

    commentRepository.save(comment)

    return CommentDto.from(comment)
}

이렇게 userid를 넣어주자 해결된지 알았는데,

Description:

Parameter 1 of constructor in cohttp://m.example.demo.security.SecurityConfig required a bean of type 'org.springframework.security.web.AuthenticationEntryPoint' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.security.web.AuthenticationEntryPoint' in your configuration.

 

그걸 해결하려면 AuthenticationEntryPoint 을 만들어야 한다고한다.

 

그래서

@Component
class CustomAuthenticationEntrypoint: AuthenticationEntryPoint {

    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException
    ) {
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = "UTF-8"

        val objectMapper = ObjectMapper()
        val jsonString = objectMapper.writeValueAsString(ErrorResponse("JWT 인증 실패"))
        response.writer.write(jsonString)
    }
}

 

를 만들었는데, ErrorResponse가 빨간줄이고 e: file:///C:/Users/asdf/Desktop/demo/src/main/kotlin/com/example/demo/security/CustomAuthenticationEntrypoint.kt:25:58 Interface ErrorResponse does not have constructors

라고 하길래

 

data class ErrorResponse(
    val message: String?,
)

 

@Component
class CustomAuthenticationEntrypoint: AuthenticationEntryPoint {

    override fun commence(
        request: HttpServletRequest,
        response: HttpServletResponse,
        authException: AuthenticationException
    ) {
        response.status = HttpServletResponse.SC_UNAUTHORIZED
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = "UTF-8"

        val objectMapper = ObjectMapper()
        val jsonString = objectMapper.writeValueAsString(com.example.demo.dto.ErrorResponse("JWT 인증 실패"))
        response.writer.write(jsonString)
    }

이러자 작동했다!

그러나, 


Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2024-02-05T21:36:00.725+09:00 ERROR 19412 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   :

APPLICATION FAILED TO START

Description:

Parameter 2 of constructor in cohttp://m.example.demo.security.SecurityConfig required a bean of type 'org.springframework.security.web.access.AccessDeniedHandler' that could not be found.

Action:

Consider defining a bean of type 'org.springframework.security.web.access.AccessDeniedHandler' in your configuration.

 

이건 SecurityConfig의 생성자에서 AccessDeniedHandler 타입의 빈이 필요하지만, 이를 찾을 수 없다는 뜻이라, AccessDeniedHandler 를 없애버렸다! 해결!!!

 

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SecurityConfig(
    private val jwtAuthenticationFilter: JwtAuthenticationFilter,
    private val authenticationEntrypoint: AuthenticationEntryPoint
) {

    @Bean
    fun filterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .httpBasic { it.disable() }
            .formLogin { it.disable() }
            .csrf { it.disable() }
            .authorizeHttpRequests {
                it.requestMatchers(
                    "/login",
                    "/signup",
                    "/swagger-ui/**",
                    "/v3/api-docs/**",
                ).permitAll()
                    .anyRequest().authenticated()
            }
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter::class.java)
            .exceptionHandling{
                it.authenticationEntryPoint(authenticationEntrypoint)
            }
            .build()
    }
}