240112 TIL | Co-Ha 내가 구현한 기능 공부, 기록

2024. 1. 12. 20:30· 왕초보일지
목차
  1. deprecated 

✍️ 작성중

  1. 어떠한 이유로 해당 기능을 사용하였는지
  2. 해당 기능의 코드는 어떠한 로직을 가지고 있는지
  3. 코드를 작성하며 발견된 버그나 오류는 어떠한게 있었는지 그리고 어떻게 해결하였는지

 

  1. 게시물 조회 + 댓글 조회
  2. 댓글 수정
  3. 사용자 인증/인가 기능

🚩 게시물 단건 조회 + 댓글 조회, 게시물 전체 조회

1 어떠한 이유로 해당 기능을 사용하였는지

  • 정보 공유가 주목적인 뉴스피드이니 작성된 게시물을 확인할 수 있어야 한다.
  • 작성된 게시글에 대한 의견을 주고받을 수 있는 댓글 또한 확인할 수 있어야 한다.

2 해당 기능의 코드는 어떠한 로직을 가지고 있는지 ※ 입력값이 들어가면 어떠한 코드를 통해 어떠한 값으로 변화하는지

 

 - 전체 조회

override fun getAllPostList(): List<PostResponse> {
        return postRepository.findAll().map { PostResponse.toPostResponse(it) }
}

컨트롤러단에서 전체 조회를하면

게시글이 있는 repository에서 모든 데이터를 find 한 뒤

PostResponse DTO 형태로 반환한다.

 

 - 단건 조회

override fun getPostById(postId: Long): PostWithReplyResponse {

    val post = postRepository.findByIdOrNull(postId) ?: throw ModelNotFoundException("Post", postId)
    return PostWithReplyResponse.toPostWithReplyResponse(post)
}

컨트롤러단에서 원하는 게시글의 아이디를 입력하고 조회를 하면

아이디를 기반으로 해당하는 게시글을 find 하고

그 게시글의 id를 가지고 있는 댓글과 함께 PostWithReplyReponse DTO 형태로 반환한다.

 

3 코드를 작성하며 발견된 버그나 오류는 어떠한게 있었는지 그리고 어떻게 해결하였는지

-


🚩 댓글 수정

1 어떠한 이유로 해당 기능을 사용하였는지

  • 작성한 댓글을 수정할 수 있어야 한다.

2 해당 기능의 코드는 어떠한 로직을 가지고 있는지 ※ 입력값이 들어가면 어떠한 코드를 통해 어떠한 값으로 변화하는지

 

@Transactional
    override fun updateReply(replyId: Long, request: UpdateReplyRequest): ReplyResponse {
        val reply = replyRepository.findByIdOrNull(replyId) ?: throw ModelNotFoundException("Reply", replyId)
        val currentUser = SecurityContextHolder.getContext().authentication.name
        if(reply.author != currentUser) throw UnauthorizedAccess()
        reply.content = request.content

        return ReplyResponse.toReplyResponse(reply)
    }

 

댓글의 아이디를 받아와 해당하는 댓글을 reply 에 담는다.

작성자 - 시큐리티 기능

request 로 받아온 content 를 reply 의 content 로 할당한다.

업데이트한 reply 를 ReplyResponse DTO 형태로 반환한다.

영속성 컨텍스트에 의해 reply 는 업데이트 된 내용으로 데이터베이스에 저장된다.

 

 

3 코드를 작성하며 발견된 버그나 오류는 어떠한게 있었는지 그리고 어떻게 해결하였는지

-


🚩 사용자 인증 / 인가

1 어떠한 이유로 해당 기능을 사용하였는지

  • 본인이 작성한 게시글과 댓글만 수정, 삭제할 수 있어야 한다.
  • 게시글과 댓글을 작성할 때 사용자의 아이디가 자동으로 같이 저장된다.
  • 이를 위해서 우선 회원가입과 로그인 기능으로 사용자를 인증할 수 있어야 하고
  • 로그인 하지 않은 사람은 게시글의 조회만 할 수 있어야 한다.
  • 이를 위해서 토큰을 이용한 인가가 필요했다.

2 해당 기능의 코드는 어떠한 로직을 가지고 있는지 ※ 입력값이 들어가면 어떠한 코드를 통해 어떠한 값으로 변화하는지

도움을 받은 블로그 (1 , 

 - 회원가입

@Transactional
    fun registerMember(request: SignUpRequest): SignUpResponse {
        val isExistAccount = userRepository.existsByEmail(request.email)
        if (isExistAccount) throw DuplicateUsernameException(request.email) // 이메일(아이디) 중복 검사

        return SignUpResponse.toSignUpResponse(userRepository.save(User.toUser(request, encoder))) // encoder 가져와서 비밀번호 암호화해서 저장
    }

request의 아이디(email) 와 동일한 유저 정보가 있는지 userRepository 에서 찾고 있다면 중복된 이메일이라는 메시지를 날린다. 없다면 request 정보를 encoder 와 함께 가져와서 User 객체로 만든 뒤 userRepository 에 저장한다.

PasswordEncoder 인  encoder 는 다음과 같이 데이터를 암호화한다.

companion object {
        fun toUser(request: SignUpRequest, encoder: PasswordEncoder) = User(
            email = request.email,
            userPassword = encoder.encode(request.userPassword), // 비밀번호 암호화
            name = request.name
            )
    }

 

 

- 로그인

@Transactional
    fun login(request: SignInRequest): SignInResponse {
        val user = userRepository.findByEmail(request.email)
            .takeIf { encoder.matches(request.password, it.userPassword) } // .matches() 암호화되지 않은 비밀번호와 암호화되어 저장된 비밀번호를 검사해준다.
            ?: throw MisMatchedExcpetion(request.email)
        val token = tokenProvider.createToken("${user.email}:${user.type}") // 사용자 정보로 토큰 생성
        return SignInResponse(user.email, token)
    }

 

request 로 받은 아이디를 userRepository 에서 찾고 그 아이디에 해당하는 비밀번호와 request 로 받은 비밀번호를 검사한다. 일치한다면 해당 사용자의 아이디와 타입을 이용해서 토큰을 생성한 후 아이디와 함께 토큰을 반환해준다.

 

토큰 생성

@PropertySource("classpath:jwt.yml") // 설정 파일 경로
@Service
class TokenProvider(
    @Value("\${secret-key}")
    private val secretKey: String,
    @Value("\${expiration-hours}")
    private val expirationHours: Long,
    @Value("\${issuer}")
    private val issuer: String
)
 fun createToken(userSpecification: String) = Jwts.builder()
        .signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(secretKey.toByteArray()))
        .setSubject(userSpecification) // 고유 정보로 제목 설정
        .setIssuer(issuer) // 발급자
        .setIssuedAt(Timestamp.valueOf(LocalDateTime.now())) // 발급 시간
        .setExpiration(Date.from(Instant.now().plus(expirationHours, ChronoUnit.HOURS))) // 토큰 만료 설정
        .compact()!!

유저정보(userSpecification) 를 받아와서 이를 토대로 토큰을 생성한다. 

Jwt.builder() : JWT 를 생성하기 위한 빌더 생성

signwith() : 알고리즘과 서명키를 지정

이 때 서명키는 Base64로 인코딩한 값을 가져와야하므로 Base64.getEncoder().encodetoString() 으로 인코딩해줬다.

setSubject() : 토큰 제목 설정. 여기서는 user의 email 과 type 을 가져왔다.

setIssuer() : 토큰 발급자 

setIssuedAt() : 토큰 발급 시간. 현재 시간을 사용하고 있다.

setExpiration() : 토큰 만료시간. 현재시간에 설정해놓은 만료시간을 더해서 설정하고 있다.

compact() : 위의 옵션을 기반으로 생성하고 문자열로 반환.

 

 

게시글 작성

@Transactional
    override fun createPost(request: CreatePostRequest): PostResponse {
        val currentUser = SecurityContextHolder.getContext().authentication.name
        val post = postRepository.save(request.toPost(currentUser))
        return PostResponse.toPostResponse(post)
    }

사용자가 게시글을 작성, 수정, 삭제할 때 클라이언트는 엑세스 토큰을 HTTP 요청 헤더에 포함하여 작성 요청을 서버에 보낸다. 

spring security 는 요청을 받아들이고, 엑세스 토큰을 추출하여 토큰의 유효성을 확인한다.

토큰이 유효하면 사용자 정보를 컨텍스트에 저장한다.

그리고 그 사용자 정보를  SecurityContextHolder.getContext().authentication  에서 가져와서 게시글과 연결시킨다.

 

❓ SecurityContextHolder

더보기

: 인증 객체가 저장되는 보관소

인증 정보가 필요할 때 언제든지 인증 객체를 꺼내서 사용할 수 있다. 코드의 아무곳에서나 참조할 수 있고 다른 스체드와 공유하지 않으므로 독립적으로 사용할 수 있다.

 

 

 게시글 수정

@Transactional
    override fun updatePost(postId: Long, request: UpdatePostRequest): PostResponse {
        val savedPost = postRepository.findByIdOrNull(postId) ?: throw ModelNotFoundException("Post", postId)
        val currentUser = SecurityContextHolder.getContext().authentication.name
        if (savedPost.author != currentUser) throw UnauthorizedAccess()
        savedPost.updatePost(request)
        return PostResponse.toPostResponse(savedPost)
    }

 

게시글이나 댓글을 수정, 삭제 할 때도 마찬가지로 현재 사용자 정보를 가져와서 비교한다.

 

 

요청받은 토큰의 필터링    JwtAuthenticalFilter : OncePerRequestFilter()

// 헤더에서 토큰 추출
private fun parseBearerToken(request: HttpServletRequest)
= request
    .getHeader(HttpHeaders.AUTHORIZATION) // http 요청의 헤더에서 authorizarion 값을 찾아서
    .takeIf { it?.startsWith("Bearer ", true) ?: false } // 접두어 확인, 제거 ( Bearer 타입 )
    ?.substring(7)

 

HTTP 헤더의 Authorization 에서토큰을 추출한다.

이 때 지금 사용중인 토큰인 Bearer 를 확인하고 제거해서 실제 토큰을 가져온다.

 

❓Bearer Token 

더보기

: 인증 타입 중 하나. JWT 혹은 OAuth 에 대한 토큰을 사용한다. RFC 6750 확인

// 토큰으로 정보 추출
private fun parseUserSpecification(token: String?) = (
        token?.takeIf { it.length >= 10 }
            ?.let { tokenProvider.validateTokenAndGetSubject(it) } // 토큰이 유효한지 확인, 복호화
            ?: "anonymous:anonymous" // 유효하지 않을 때 설정
        ).split(":")
    .let { User(it[0], "", listOf(SimpleGrantedAuthority(it[1]))) } // 사용자 정보 가져와서 스프링 시큐리티 User 객체 생성

 

받아온 토큰으로 토큰이 유효한지 확인하고

더보기
// 토큰의 subject 를 복호화하여 문자열 형태로 반환 ( 유효한지 확인, 유효하지 않을 경우 null )
fun validateTokenAndGetSubject(token: String): String? = Jwts.parserBuilder()
    .setSigningKey(secretKey.toByteArray()) // 비밀키로 복호화
    .build()
    .parseClaimsJws(token) // 파싱 : jwt 형식에 맞게 데이터를 해석하고 추출
    .body // 토큰의 본문 (claims) 에 접근
    .subject // 주체 반환

사용자 정보로 스프링 시큐리티의 User 객체를 생성한다. 

// 인증 정보 설정
override fun doFilterInternal(
    request: HttpServletRequest,
    response: HttpServletResponse,
    filterChain: FilterChain
) {
    val token = parseBearerToken(request) // 토큰 추출
    val user = parseUserSpecification(token) // 사용자 정보 추출
    UsernamePasswordAuthenticationToken.authenticated(user, token, user.authorities) // 인증된 사용자를 나타내는 토큰 생성
        .apply { details = WebAuthenticationDetails(request) } // 요청날린 client 또는 프록시의 ip 주소와 세션 id 저장
        .also { SecurityContextHolder.getContext().authentication = it } // sercurityContextHolder 에 인증 정보 저장
    filterChain.doFilter(request, response) // 다음 필터
}

위에서 가져온 토큰과 사용자 정보, 사용자 권한으로 UsernamePasswordQuthenticationToken 을 생성하고 (이것을 사용하여 사용자를 인증한다) 사용자의 인증 세부정보를 추가해서 SecurityContextHolder 에 이를 저장한다.

 

 

 

 

3 코드를 작성하며 발견된 버그나 오류는 어떠한게 있었는지 그리고 어떻게 해결하였는지

deprecated 

signWith 

 

 

"이 메서드의 문제점: 키 인자로 문자열이 사용되었는데, 이는 암호화 작업에 사용되는 키가 항상 바이너리 데이터여야 하는데, 이를 문자열에서 어떻게 바이트로 얻는지에 대한 혼동을 야기했습니다."

"올바른 로직 예제: 올바른 방법으로 작업하려면 문자열로 된 키를 먼저 BASE64 디코딩하여 바이트 배열로 변환한 다음, 이를 사용하여 암호화 작업을 수행해야 합니다. 아래는 이를 수행하는 예제 코드입니다"

  byte[] keyBytes = Decoders.BASE64.decode(base64EncodedSecretKey);
  Key key = Keys.hmacShaKeyFor(keyBytes);
  jwtBuilder.signWith(key); //or signWith(Key, SignatureAlgorithm

 

원래는 아래와 같이 secretkey 를 바이트로 변환 후 Base64 로 인코딩한 걸 넣었었다.

 

.signWith(io.jsonwebtoken.SignatureAlgorithm.HS256, Base64.getEncoder().encodeToString(secretKey.toByteArray()))

 

문자열로 된 secretKey 를 Base64 로 디코딩하여 바이트 배열로 변환

-> 시큐리티의 Key 객체로 HMAC 키 생성

-> secretKey, 알고리즘 적용

    val keyBytes: ByteArray = Decoders.BASE64.decode(secretKey)
    val key: Key = Keys.hmacShaKeyFor(keyBytes)
    .signWith(key, io.jsonwebtoken.SignatureAlgorithm.HS256)

잘 따라했고 빗금도 없어졌는데  또 JWT 에러가 떴다.

구글링해보니 파싱할 때의 secreKey 와 생성할 때 secretKey 가 달라서 파싱할 때도 바이트로 변환해줘야 한다고한다.

그런데 난 이미 바이트 변환을 해주고 있었다.

 

혹시 모르니 바이트  변환을 빼주고 다시 실행했다.

됐다! 대신 이번엔 파싱부분에 빗금이 생겼다...

signWith() 와 마찬가지로 

문자열로 된 secretKey 를 Base64 로 디코딩하여 바이트 배열로 변환

-> 시큐리티의 Key 객체로 HMAC 키 생성

한 key 를 인자로 넣으라고 한다.

위에서 선언해준 key 변수를 집어넣었더니 해결되었다.

 

 

 

 

 

 

네트워크 연결

2024-01-15T13:01:43.691+09:00 ERROR 11788 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Exception during pool initialization.org.postgresql.util.PSQLException: This connection has been closed.

 

 

 

 

 

 

 

'왕초보일지' 카테고리의 다른 글

240116 TIL |  (0) 2024.01.16
240113 TIL |  (1) 2024.01.13
240111 TIL |  (4) 2024.01.11
240110 TIL | 스프링시큐리티 로그인 구현하는 중  (1) 2024.01.10
240109 TIL |  (2) 2024.01.09
  1. deprecated 
'왕초보일지' 카테고리의 다른 글
  • 240116 TIL |
  • 240113 TIL |
  • 240111 TIL |
  • 240110 TIL | 스프링시큐리티 로그인 구현하는 중
다시은
다시은
🔥
다시은
재은로그
다시은
전체
오늘
어제
  • 분류 전체보기 (127)
    • 코딩테스트 (40)
    • Language (2)
      • JAVA (2)
      • Kotlin (0)
      • TypeScript (0)
    • SQL (1)
    • 인프라 (1)
    • 왕초보일지 (77)
    • 회고 (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • sql
  • 문자열변환
  • mysql
  • googleapis
  • SQL문법
  • Kotlin
  • 스프레드시트

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
다시은
240112 TIL | Co-Ha 내가 구현한 기능 공부, 기록
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.