240117 TIL | 시큐리티 예외처리, queryDSL

2024. 1. 17. 20:32· 왕초보일지

🚩 스프링 시큐리티 인증 403 예외처리

인증이 안된 상태로 시도할 때 401이 떠야하는데 403이 뜬다.

=> AuthenticationEntryPoint 를 custom 해준다.

@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 verification failed"))
        response.writer.write(jsonString)
    }
}

 

SecurityFilterChain 에 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(customAuthenticationEntryPoint)
            }
            .build()
    }
}

 

 

 

🚩 인가

 

RBAC / ABAC

spring security role hierarchical

 

 

AOP 기반 인가

SecurityConfig 에 @EnableMethodSecuity 등록

 

Controller 에서 @PreAuthrize 를 통해 SpEL , SecurityExpressonRoot 의 methods와 properties 사용 가능

@PreAuthorize("#user.name == principal.name") 
fun doSomething1(user: User): Unit { ... }

@PreAuthorize("hasRole('ADMIN') or hasRole('STUDENT')") 
fun doSomething2(course: Course): Unit { ... }

 

 

우리가 인증정보를 Authentication 객체에 담을 때 

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

    )
}

역할을 GrandtedAuthority 객체로 앞에 "ROLE_" 을 붙여서 저장했다.

이 Authority는 역할과 권한 모두를 포함할 수 있는데

이 둘을 구분하기 위해 시큐리티에서는 앞에 ROLE_ 이 붙어있으면 이게 역할 Authority 라고 자동으로 판단해준다.

그러므로 담을 땐 ROLE_ 을 붙여서 담고 hasRole 로 확인을 할 때는 ROLE_ 을 떼고

 

 

 🚩스프링 시큐리티 인가 403 예외처리

=> AccessDeniedHandler 를 custom 해준다.

@Component
class CustomAccessDeniedHandler: AccessDeniedHandler {
    override fun handle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        accessDeniedException: AccessDeniedException
    ) {
        response.status = HttpServletResponse.SC_FORBIDDEN
        response.contentType = MediaType.APPLICATION_JSON_VALUE
        response.characterEncoding = "UTF-8"

        val objectMapper = ObjectMapper()
        val jsonString = objectMapper.writeValueAsString(ErrorResponse("No permission to API"))
        response.writer.write(jsonString)

    }
}

accessDeniedHandler 등록

.exceptionHandling{
                it.authenticationEntryPoint(customAuthenticationEntryPoint)
                it.accessDeniedHandler(accessDeniedHandler)
            }

 

 

 

인증을 안한 상태로 course 를 작성하려고 할 때    /    학생 토큰으로 작성하려고 할 때

 

 


🚩 QueryDSL

 

val user: User = queryFactory.selectFrom(user)
		.where(user.email.eq("test@gmail.com"))
		.fetchOne()

 

빌드

plugins {
  ...
    kotlin("kapt") version "1.8.22" // 추가!
}
val queryDslVersion = "5.0.0" // 버전 명시 해야 함!!!
dependencies {
...
    implementation("com.querydsl:querydsl-jpa:$queryDslVersion:jakarta") // 추가!
    kapt("com.querydsl:querydsl-apt:$queryDslVersion:jakarta") // 추가!
...

}

 

Emtity 추가하거나 변경할 때 <complieKotlin> 을 통해 Qclass 업데이트

 

JpaQueryFactory 생성하기

abstract class QueryDslSupport {

    @PersistenceContext
    protected lateinit var entityManager: EntityManager

    protected val queryFactory: JPAQueryFactory
        get() {
            return JPAQueryFactory(entityManager)
        }
}

 

@PersistenceContext

entity manager 를 빈으로 주입할 때

 

각 엔티티querydslrepository

@Repository //jpa외부에서 만드는 거라 직접 등록해줘야 함
class QueryDslCourseRepository: QueryDslSupport() {
    private val course = QCourse.course

    fun searchCourseListByTitle(title: String): List<Course> {
        return queryFactory.selectFrom(course).where(course.title.containsIgnoreCase(title)).fetch()
    }
}

=> 서비스단에서 주입받아서 사용

 

기존 Repository(JpaRepository)와 QueryDslRepository 합치기

내일배움캠프

 

서비스단에서 주입받아서 사용하는건 CourseRepository

이 CourseRepository 가 상속받는 건 JpaRepository 와 CustomCourseRepository

CustomCourseRepository 의 구현체는 CourseRepositoryImpl

CourseRepositoryImpl 은 QueryDslSupprt 도 받아와서 여기서 동적쿼리를 만든다.

@Repository // JPA 외부에서 만드는거라 직접 등록해줘야 함
class CourseRepositoryImpl(): CustomCourseRepository, QueryDslSupport()
interface CourseRepository: JpaRepository<Course, Long>, CustomCourseRepository
interface CustomCourseRepository {fun searchCourseListByTitle(title: String): List<Course>}

 

=> 서비스단에서 CourseRepository 를 통해 CustomCourseRepository 를 구현한 CourseRepositoryImpl 의 메소드를 사용할 수 있다.

 

 

 

 

 

- 동적쿼리

여러 검색 조건을 받을 수도 있고 안 받을 수도 있을 때

Boolean builder 활용

fun serarchUser(email: String?, nickname: String?): List<User> {
		val builder = new BooleanBuilder()
		
		email?.let { builder.and(user.email.contains(it)) }
		nickname?.let { builder.and(user.nickname.contains(it) }

		return queryFactory.selectFrom(user).where(builder).fetch()
}

없다면 넘어가고 있다면 let 으로 builder 에 넣어줌

 

where 가변 인자 활용 (or 조건은 활용 불가)

fun serarchUser(email: String?, nickname: String?): List<User> {
		return queryFactory
        .selectFrom(user)
        .where(
        userEmailContains(email), 
        userNicknameContains(nickname)
        )
        .fetch()
}

private fun userEmailContains(email: String?) {
		return email?.let { user.email.contains(it) }
}

private fun userNicknameContains(nickname: String?) {
		return nickname?.let { user.nickname.contains(it) }
}

 

 

 

- QueryProjection 

결과를 DTO로 받을 수 있다.

 

Projections bean / Projections constructor / @QueryProjection 활용

 

 

 

- 벌크 수정 / 삭제

// 벌크 수정
val updatedCount = queryFactory
			.update(user)
			.set(user.nickname, "TEST_NICKNAME")
			.where(user.id.lt(10L))
			.execute()

// 벌크 삭제
val updatedCount = queryFactory
			.delete(user)
			.where(user.id.lt(10L))
			.execute()

 

 

 

이넘 클래스 컨트롤러에서 바로 활용

더보기
//    companion object {
//        @JvmStatic
//        @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
//        fun parse(name: String?): CourseStatus? =
//            name?.let { EnumUtils.getEnumIgnoreCase(CourseStatus::class.java, it.trim())}
//
//    }

 

 


🚩 QueryDSL활용

목록을 조회할 때 pagination 활용, 정렬 기준, 마감상태 필터링

 

Pageble 인터페이스

 

인자로 받으니까 자동으로 request 를 받는다.

구현체인 PageRequest / PageImpl 를 만들어줘도 된다.

 

 

PageImpl

 

override fun findByPageableAndStatus(pageable: Pageable, courseStatus: CourseStatus?): Page<Course> {
        val whereClause = BooleanBuilder()
        courseStatus?.let { whereClause.and(course.status.eq(courseStatus)) }

        val totalCount = queryFactory.select(course.count()).from(course).where(whereClause).fetchOne() ?: 0L
        val query = queryFactory.selectFrom(course)
            .where(whereClause)
            .offset(pageable.offset)
            .limit(pageable.pageSize.toLong())

        if(pageable.sort.isSorted) { // sort 기준이 들어와있으면
            when(pageable.sort.first()?.property) { // 하나만 들어왔을 때
                "id" -> query.orderBy(course.id.asc())
                "title" -> query.orderBy(course.title.asc())
                else -> query.orderBy(course.id.asc())
            }
            } else {
                query.orderBy(course.id.asc())
        }

        val contents = query.fetch()

        return PageImpl(contents, pageable, totalCount)
    }

 

 

 


querydsl 정말 편한 건 알겠는데 어렵다 🥲🥲🥲

일단 todo 저번에 했던거 정리하고 ->시큐리티 추가 -> QueryDSL 강의 빠르게 한 번 더 보기

저번에 만든 todo 지금보니까 너무 이상하다 

 

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

240119 TIL |  (1) 2024.01.19
240118 TIL |  (1) 2024.01.18
240116 TIL |  (0) 2024.01.16
240113 TIL |  (1) 2024.01.13
240112 TIL | Co-Ha 내가 구현한 기능 공부, 기록  (1) 2024.01.12
'왕초보일지' 카테고리의 다른 글
  • 240119 TIL |
  • 240118 TIL |
  • 240116 TIL |
  • 240113 TIL |
다시은
다시은
🔥
다시은
재은로그
다시은
전체
오늘
어제
  • 분류 전체보기 (127)
    • 코딩테스트 (40)
    • Language (2)
      • JAVA (2)
      • Kotlin (0)
      • TypeScript (0)
    • SQL (1)
    • 인프라 (1)
    • 왕초보일지 (77)
    • 회고 (4)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

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

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
다시은
240117 TIL | 시큐리티 예외처리, queryDSL
상단으로

티스토리툴바

단축키

내 블로그

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

블로그 게시글

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

모든 영역

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

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