잠깐 써본 것 정리하기

 

 

1. build.gradle

어플리케이션 실행 후, src->main-> generated->entity Q객체들 생성 확인

Q객체가 있어야 selectFrom(comment) 에서 comment가 Comment엔티티라고 알려줄 수 있음. 자세힌 나중에 공부

 

 

QuerydslConfig생성 -> JPAQueryFactory @Bean 등록

 

 

RepositoryCustom, RepositoryImpl 생성

 

RespositoryCustom에 사용할 쿼리 선언

 

RepositoryImpl에 RepositoryCustom 구현

Q객체들 인스턴스이름으로 static import

쿼리 작성

 

Repository에 RepositoryCustom 상속

 

받아줄 DTO에 @QueryProjection(생성자 타입때문에)

'공부 > 잡다' 카테고리의 다른 글

Springboot 저장소  (0) 2023.04.16
IntelliJ 콘솔창 한글 깨질때  (0) 2023.04.15
웹 동작 간략개괄  (0) 2023.04.14
IntelliJ 디버거 활용 - Exception 조건설정  (0) 2023.04.10
Twitter recommendation system code revealed  (0) 2023.04.06

 

 

며칠간 CRUD 게시판 리모델링 하면서 많은 부분을 생각했다.

 

1.Spring Security관련 고민

https://imslo.tistory.com/71

 

CRUD 게시판 리팩토링해보기 - <1> Spring Security, Jwt인증 적용

그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기 1.Spring Security 적용 (JWT방식 커스텀 필터 사용) 2.Controller에 @Valid 어노테이션 적용 3.JPA 연관관계 변경 (양방향 ->

imslo.tistory.com

 

2. CRUD 관련 고민

https://imslo.tistory.com/72

 

CRUD 게시판 리팩토링해보기 - <2> 프로젝트 구조 만들기, Entity 생성, Exception handler

그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기 1.Spring Security 적용 (JWT방식 커스텀 필터 사용) 2.Controller에 @Valid 어노테이션 적용 ( 간단해서 보류 ) 2.5 기존 Articl

imslo.tistory.com

 

특히 Domain의 Dto의존성을 줄인 부분과

Entity간의 조회 사례를 생각해보면서 관계설정을 한 부분이 시간이 오래걸렸는데, 도움이 되었다.

<개발 공부>

Spring Boot를 통해 간단한 CRUD API를 만들 수 있게 되었다.

부트캠프의 강의가 도움이 된 부분도 있지만, 온전히 내 것으로 만들려면 개인적으로도 공부를 많이 해야된다는 것을 느꼈다. Spring이라는 거대한 프레임워크를 몇 시간 강의 안에 담아낼 수는 없기 때문에, 강의에서는 전체적인 흐름과 소소한 팁들만 전달해줄 수 있을 뿐이다. 강의 내용만 따라하는 것보다는, 전체적인 틀을 벗어나지 않는 선에서 조금 씩 바꿀 수 있는 것들을 실험해보면서 이해하는 것이 가장 효율적인 방법이라고 느꼈다.

Spring Security를 적용할 때 가장 많이 느꼈는데, 강의에서 나온 것과 조금 다르게 하려고 할 때마다 새로운 것을 배워야했고, 결국 Spring Security의 구조를 가 공부하게 되었다. 지금까지 부트캠프에서 주어진 과제로 했던 것을 로그인/회원가입 기능부터 혼자서 만들어보며, 서비스로 생각했을 때 기능이 어떻게 되어야하는지, 그에 맞춰 Entity간의 관계는 어떻게 변해야하는지 생각하여 적용해보고 있다.

JPA를 사용하며 느낀 것은, JPA가 개발자들이 반복하여 작성하는 DB관련 코드를 줄여주고, 효율적으로 작성하게 해주는 것은 맞지만 결국에는 SQL을 알아야 이를 더 잘 활용할 수 있다는 것이다. 이 뿐만 아니라 많은 부분에서 내가 그동안 놓쳐왔던 CS관련 공부가 필요하다는 것을 느끼고 있다. 다른 개발자들이 몇년에 걸쳐 공부한 것을 짧은 시간 안에 공부하려는 데서 필연적으로 마주칠 수 밖에 없는 벽인 것 같다. 조금씩 시간을 내서 꾸준히 공부해야겠다.

 

<인생>

조카를 처음으로 안아봤다. 한달 된 아기를 안아본 것은 처음인데, 신비로운 경험이었다. 공부 열심히 해서 떳떳한 삼촌이 되고싶다고 생각했다.

IFSC 클라이밍월드컵을 직관했다. 용마폭포공원 스포츠클라이밍경기장에서 열렸는데, 앞에는 경기장이, 뒤에는 폭포가 있는 환경이 아주 멋졌다. 세계적인 선수들을 가장 가까이서 볼 수 있어서 너무 좋은 경험이었다.

'일기 > 주간회고' 카테고리의 다른 글

코딩테스트 응시 // Spring 입문  (0) 2023.04.17
일기 겸 개발 부트캠프 일주일차 후기  (0) 2023.04.01

그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기

 

<변경점>

1.Spring Security 적용 (JWT방식 커스텀 필터 사용)

2.Controller에 @Valid 어노테이션 적용 ( 간단해서 보류 )

2.5 기존 Article, Comment 등록/수정/삭제 기능 재구현 (추가)

3.JPA 연관관계 변경 (양방향 -> 단방향)

4.Custom Exception 처리하기

5.ERD, API 작성

 

<패키지 구조>

현재 있는 기능 : 회원기능 / 게시글 / 댓글(대댓글)

Controller // Service // Repository // Exception // Security // Handler // Jwt관련

                                                          =>묶어서 Utils

 

 

<게시글 관련 기능 구현>

 

Controller

게시글 전체목록 조회시에는 게시판의 형태대로 제목, 작성자만 가져오게 했다.

게시글 상세 조회(게시물클릭) 시에는 게시글의 필드를 모두 가져오게 했다.

둘을 구분짓기 위해 ResponseDto를 ListResponseDto // DetailResponseDto로 나누어 받게 함

 

Service

Controller에 맞게 구현했다. Article 조회 시 Comment를 어떻게 가져올지 아직 정하지 못했기 때문에, ArticleDetailResponseDto 작성을 보류했다. Comment에 대댓글 기능을 추가할 생각이기 때문에, 게시글에 달린 댓글을 모두 긁어와서 Article에 List로 나갈 댓글 목록을 만들어줄 생각이다. 자세한건 Comment에서

 

*추가 : 게시글 삭제 요청에 대해 DB에서 삭제하는 것이 아닌, Article의 isDeleted 필드를 true로 만들어주는 방식으로 하기로 했다. 따라서 게시글 긁어오는 부분을 findAllByIsDeletedFalse()로 대체하였다

게시글 상세조회 요청에 대해 가져온 게시글이 isDeleted면 ArticleDeletedException을 throw하도록 바꾸었다.

 

 

<Comment 관련 기능>

Comment 기능에 대댓글 기능을 구현할 생각이다. 구현 방법으로는 여러가지가 있을 것이다.

1. Comment 내부에 Comment필드를 가져 연관관계 맺어주기

- Comment와 Comment의 연관관계를 정의할 수가 없다. (댓글인지, 대댓글인지에 따라 관계가 달라지니까)

 

2. 대댓글 entity를 만들어 관리

- 대댓글이 한 depth로만 달리면 괜찮은 방법일 수 있지만, 대댓글에도 대댓글을 달 수 있는 경우 계속 Entity를 만들어주어야 하기 때문에 좋지 않다고 생각함

 

3. Comment에 commentType 필드를 가져서 게시글에 단 댓글인지 Comment에 단 댓글인지 분류

- Article을 조회했을 때 commentType이 ArticleComment인 댓글을 가져오고, 해당 댓글들에 대해서 commentType이 CommentComment 인 애들을 전부 조회한 뒤 Id를 비교해야 하기 때문에 쿼리가 불필요하게 많이 나가게 될 것 같다.

 

4. 댓글과 대댓글을 구분짓지 않고 Comment 엔티티로 관리하며, nullable한 rootComment 필드를 가지게 한다.

    Article 조회 시, 해당 Article에 달린 댓글 전부를 긁어온다. CommentBuilder 메소드를 하나 만들어 Comment를 구분하       고, 댓글이 대댓글을 포함하도록 하는 로직을 구현한다.

 

==> 4번은 추가적인 로직을 구현해야 해서 복잡할 것 같지만, 이렇게 하는게 맞는 방법 같아서 이렇게 하기로 함.

 

일단, 댓글과 대댓글의 관계를 정리해주는 로직을 제외하고, Comment 등록/삭제/수정 기능을 구현하려고 한다.

또한, 댓글 삭제 시에도 대댓글은 유지하기 위해 댓글 삭제 요청에 대해 DB에서 댓글을 삭제하는 것이 아닌, isDeleted flag를 true로 바꿔주는 방식으로 구현하려고 한다. 이에 따라 CommentResponseDto의 생성자에는 isDeleted를 확인하고, 삭제된 댓글일 시에는 Dto의 content를 "삭제된 댓글"로 바꿔주어야한다.

추가로, 테스트를 위해 Article에서 Comment를 조회하는 부분을 일단 모든 Comment를 조회하도록 구현하려고 한다.

 

(Article상세조회부분)

Article상세조회

(CommentController)

user의 고유ID만 넘겨주면 좋겠지만, 서비스에서 User의 권한을 확인하고 관리자일 시 요청을 처리해주도록 API가 설계되어있기 때문에 User객체 자체를 넘긴다

 

(CommentService)

delete요청에 대해 DB에서 삭제하는 것이 아니라, comment 엔티티의 isDeleted 값을 true로 바꾸는 메서드를 구현했다.

Comment 엔티티의 생성자와 메서드

Comment의 생성자는 Article에 단 Comment인 경우와 대댓글의 경우를 나누어서 오버로딩 해주었다.

기존에는 Comment의 생성자에 CommentRequestDto를 받도록 하였는데, CommentRequest가 바뀔 가능성이 있다는 점을 생각해보면 이에 따라 도메인인 Comment 엔티티의 생성자가 바뀌어야 하는것은 좋지 않다고 생각했다.(의존성문제)

따라서 생성자로 필드값을 받게 한 뒤, 

Dto의 Comment 생성 메서드

requestDto에서 Comment객체를 만드는 메서드를 통해 Comment를 생성하도록 하였다.

이렇게 하면 Comment요청을 받는 방식이 바뀐다 하더라도 도메인인 Comment 엔티티를 바꾸는 것이 아니라 Dto의 메서드만 바꾸면 된다.

 

 

 

(CommentService 내부 메서드 리팩토링)

Comment를 Optional로 불러오는 부분과, 해당 코멘트 삭제/수정 요청에 대해 작성자가 맞는지 혹은 관리자권한이 있는지를 검증하는 부분은 중복되기 때문에 따로 빼주었다.

 

<엔티티 연관관계 변경>

기존에는 한 객체에서 다른 객체를 조회할 일이 있으면 연관이 있다고 생각하여 @OneToMany나 @ManyToOne을 사용하여 연관관계를 맺었다. 전체적인 설계를 먼저 하고, 그에 맞게 연관관계를 맺어주는 것이 더 좋은 것 같다.

 

1. User와 Article의 관계 : 단방향 @ManyToOne

- 기존에는 User가 @OneToMany로 Article을, Article이 @ManyToOne으로 User를 바로보게 하였다.

   이렇게 했을 때 장점은 user.getArticles로 작성 글 목록을 한번에 가져올 수 있다는 것이 있다.

   회원 Entity가 모든 요청에 조회되는데 비해 비중이 굉장히 낮은 이점일 뿐더러, 이런식으로 설정 했을 때 fetch되어야하     는 시점이 언제인지, Article의 연관관계는 어떻게 되는지 등 생각할 것이 많아진다. 따라서 이번엔 

   Article이 User를 단방향 @ManyToOne으로 바라보는 관계로 설정하였다. 게시글이 조회될 땐 거의 무조건 작성자를  알    아야 하기 때문이다.

 

*Article의 User필드는 user_id가 아닌 User 객체로 한 이유는 Article을 조회할 때 username(작성자 닉네임)을 알아야하는데, Article의 필드에 가지고 있는 외래키는 고유id이다. 따라서 Article을 가지고 올 때 User객체를 같이 가져와야 작성자명을 찾기 위한 추가 쿼리가 나가지 않게 된다.

 

2. User와 Comment의 관계 : 단방향 @ManyToOne

- 1과 마찬가지의 이유로 Comment가 @ManyToOne으로 User를 바라보게 하였다.

 

3. Comment와 Article의 관계 : 연관관계없이 필드값으로 id 가지고있기

-  Article의 경우 전체 글 목록을 보여줄 때는 Comment를 알 필요가 없다. 따라서 게시글 상세보기 시에만 Comment 엔티티에서 Article의 id로 검색하여 댓글을 가져오면 된다.

Comment에서 Article에 대해 알아야할 정보는 Article의 고유번호밖에 없다. Comment를 통해서 해당 댓글이 달린 Article의 내용이나 작성자를 조회할 일이 없기 때문이다. 따라서 따로 연관관계를 맺어주지 않고, Article의 고유id를 하나의 필드article_id로 가지게 하였다. Article에서 해당 글의 Comment를 가지고 올 때는 findby article_id로 쿼리를 날리면 된다.

저장할 때는 어차피 요청 쿼리스트링에서 해당 게시글의 id를 가지고 들어오기 때문에 Article 객체를 찾는 추가적인 쿼리가 나갈일이 없다.

 

 

그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기

 

<변경점>

1.Spring Security 적용 (JWT방식 커스텀 필터 사용)

2.Controller에 @Valid 어노테이션 적용

3.JPA 연관관계 변경 (양방향 -> 단방향)

4.Custom Exception으로 처리하기

5.ERD, API 작성

 

 

 

<1. Spring Security 적용>

Spring Security  : https://imslo.tistory.com/70

 

Spring Security

인증 / 인가 절차를 Spring에서 관리해주는 기능 Document : https://docs.spring.io/spring-security/reference/servlet/getting-started.html Hello Spring Security :: Spring Security You can now run the Spring Boot application by using the Maven P

imslo.tistory.com

 

(1). 기존에는 로그인 요청이 오면 ID와 비밀번호를 확인해서 JWT 토큰을 발급해줬다.

     ==> Spring Security에서 Username과 password를 확인해서 jwt 토큰으로 응답하게 하기 가능한지

     ==> usernameandpassword 필터를 상속받아서 구현 가능하다. 일단 Service에서 jwt토큰 반환하는식으로 구현하고, 그 다음 진행해보기

(2). 이후 다른 요청에 대해 JWT 요청을 일일히 확인했다.

     ==> 컨트롤러로 가기 전에 Spring Security에서 JWT토큰을 확인하는 필터를 추가 (커스텀 필터)

 

(1) 먼저,  Spring security에서 기본으로 제공하는 UsernamePasswordAuthenticationFilter에서 User를 찾는 부분을 커스텀

UsernamePasswordAuthenticationFilter의 attemptAuthentication 메소드

이 필터는 로그인 요청이 들어오면 ID와 PW를 받아와서 unauthenticated 상태의 Token을 생성해 AuthenticationManager에게 던진다.

Spring Security에서는 AuthenticationManager interface의 구현체는 ProviderManager가 있다

ProviderManager
ProviderManager가 Provider를 찾아 검증하는 for문

ProviderManager는 AuthenticationProvider를 리스트로 가진다. Provider는 실제로 인증 로직이 이루어지는 곳이며, Manager가 이를 List로 가지는 이유는 여러가지 인증 방식이 있을 수 있기 때문이다. 따라서 각 Provider 안에는 들어온 토큰이 내가 인증 가능한 타입인지 확인하는 메소드가 있다. 이를 확인한 후, 가능한 Provider를 찾으면 authenticate 메소드를 호출하여 인증을 진행한다.

 

DaoAuthenticationProvider
DaoAuthenticationProvider가 상속받은 AbstractUserDetailsAuthenticationProvider의 supports 메소드

 

DaoAuthenticationProvider가 상속받은 AbstractUserDetailsAuthenticationProvider의 authentication메소드

각 Provider는 검증을 해야하기 때문에 유저정보를 가져와야한다. 따라서 로그인 시도 시 입력한 Principal(아이디)를 통해 유저정보를 가져오는 UserDetailsService를 필드로 가진다.

정리하자면 로그인요청 -> UsernamePasswordAuthenticationFilter(토큰생성) -> ProviderManager ->                               알맞은 Provider  (유저 찾아와서 Credential(비밀번호) 검증)  -> 성공 시 성공에 해당하는 토큰 생성하여 return

 

이런 Spring 기본 인증로직을 활용하되, 내 DB에서 User를 찾아와 검증하게끔 하고싶다면 DaoAuthenticationUser의 UserDetailsService가 내가 구현한 UserDetailsService를 가지게 하고, 그 안에 내 repository에서 유저를 찾아오게끔 loadByUserName 메소드를 오버라이드 해주면 된다.

 

UserDetailsServiceImpl : loadByUsername 메소드 오버라이드 -> User 찾아서 UserDetails를 상속받은 클래스 반환

UserDetailsImpl : User를 받아서 UserDetails에 맞는 클래스로 변환해줌

 

 

 

<구현>

UserDetailsService
UserDetails

UserDetails는 getAuthorities, getPassword, isNonExpired 등 필수 override해야하는 메소드들 있음.

 

UserDetailsServiceimpl을 Bean으로 등록해주면 이제 SpringSecurity가 이 Service를 찾을 수 있다.

추가로, 비밀번호를 Encode 하는 방식을 검증하는 부분과 DB에 저장하는 부분 통일해야함. PasswordEncoder를 Bean으로 등록해서 SpringSecurity에게 알려주고, 회원가입 시 DB에 저장할 때도 같은 Encoder 사용해준다.

PasswordEncoder를 Bean으로 등록한다

커스텀 로그인페이지를 사용한다면 로그인 요청이 어떤 URL로 들어오는지 써줘야한다. 그래야 Username~~필터가 이 요청을 Process할 수 있다.

 

이렇게 되면 UsernamePasswordAuthenticationFilter가 로그인요청을 처리해줄 것이다. 하지만 로그인이 확인되면 JWT토큰을 발급하는 로직이 구현되지 않았다. 로그인요청이 Controller에 오기 전에 처리되기 때문에 요청을 받아서 Controller나 Service에서 발급해줄 수는 없다. 따라서 Spring Security가 authentication에 성공했을 때 하는 행동을 정의해주어야 한다.

이는 SuccessHandler에서 가능하다

AuthenticationSuccessHandler

onAuthenticationSuccess를 Override 해서 커스텀 LoginSuccessHandler를 구현하고, 로그인 시 이 핸들러를 사용하라고 알려주면 된다. 나는 로그인 성공 시 response에 토큰을 담아서 보내주되, 이후에는 기존 SuccessHandler의 기능을 모두 사용하고싶다. 따라서 다음과 같이 구현했다.

커스텀 핸들러

Spring Security의 디폴트 SuccessHandler는 SavedRequestAwareAuthenticationSuccessHandler() 이다.

이 default handler는 이전 요청으로의 리디렉션, 인증 성공/실패 로깅 등 여러가지 기능을 수행해주기 때문에 내 커스텀 기능(jwt 토큰을 헤더에 추가)을 수행한 후, default handler를 호출하게 하였다.

 

문제는, default핸들러를 호출하여 인증이 성공 시 이전 요청으로 redirection 하게 되면, 클라이언트가 헤더에 담긴 JWT 토큰을 받아볼 수 없다는 점이다.

기본 RedirectStrategy의 sendRedirect
HttpServletResponse의 sendRedirect - 헤더가 있으면 새로운 것으로 덮어쓴다

해결방법이 보이지 않아 일단은 defaulthandler를 호출하는 것을 주석처리 해놓았다. 나중에 해결하겠다 ..

결과↓↓↓↓↓↓↓

토큰을 헤더에 잘 받아오긴 한다 (임시)

 

 

 

(2). JWT 필터 만들기

 

로그인 된 유저가 헤더에 JWT 토큰을 추가해서 요청을 보내면, 이를 검증할 필터가 필요하다.

Jwt필터는 OncePerRequestFilter를 상속받아 구현한다. 서버에 로그인 세션을 유지할 것이 아니기 때문에 요청마다 헤더를 확인하고, 토큰이 있다면 토큰을 검증해야하기 때문이다. 만약 헤더에 토큰이 없다면 이후 필터가 실행되도록 해야한다.

 

OncePerRequestFilter

OncePerRequestFilter를 보면, doFilter가 동작했을 때 해당 Filter가 동작해야하는지를 확인한 후, 동작해야한다면 

doFilterInternal을 실행한다.

 

OncePerRequestFilter의 doFilterInternal()

내 커스텀필터는 doFilterInternal에 검증 로직을 override해야한다.

 

dofilterinternal을 override해주고, 로직을 작성. hasBearer는 토큰이 있는지 확인하는 메소드

HttpRequest.getHeader(String asdfasdf)

토큰을 담기로 정해진 헤더이름으로 getHeader를 날렸을 때 있으면 가져와서 Bearer_prefix로 시작하는지 확인하고, 없으면 Empty라고 하므로 위와 같이 작성하였다. 헤더에 토큰이 없으면 filterchain의 다음 필터를 실행하도록 dofilter해주고 return;

 

UserDetail을 만드는 메소드

token을 받아서 User를 내 repository에서 username으로 찾아서(기존 userDetailsService 활용 ) UserDetail을 반환하는 메소드 작성

 

나머지 로직 작성

토큰이 없을 때, 토큰이 유효하지 않을 때를 거치면 1번이 실행된다. 1번은 2,3이 실행된 후 뒤 필터를 실행시킨다.

2번은 헤더에 담긴 토큰을 가지고 UserDetail을 가져온다. 이 때 User는 인증이 된 상태이므로 인증토큰을 만들어주는데, AbstractAuthenticationToken 을 상속받은 UsernamePasswordAuthenticationToken을 사용하였다. 내 경우엔 principal이 username이기 때문. 만약 username이 아니라 다른 방법으로 식별한다면, Token을 커스텀하던지 다른Token 사용.

UsernamePasswordAuthenticationToken은 생성자를 두 개 가지는데, authorities가 세팅되지 않으면 isAuthenticated를 false로, authorities가 세팅되면 isAuthenticated를 true로 설정한다.

==> principal에 userDetail 자체를 넣어주었다.

 

3번은 Authenticated 된 토큰을 securitycontext에 세팅하고, 이 context를 SecurityContextHolder에 세팅한다.

SecurityContextHolder는 ThreadLocal이기 때문에 따로 선언하는 것이 아니라 그냥 가져다 쓸 수 있다.

 

websecurityconfig에 filter추가해주기

 

 

<테스트>

테스트코드 짤줄몰라서 테스트용 컨트롤러 만들어버리기 ,,,,

 

헤더에 JwtToken 있을 때
헤더에 JwtToken 없거나, 유효하지 않을 때

토큰이 검증되지 않으면 controller까지도 못 오고 로그인페이지로 튕겨버리는 모습.👍

로그인기능은 얼추 끝난 것 같다. config에서 요청URL 별 권한설정, 액세스 실패 시 forwarding URL 설정 등 자잘한 것들이 남았지만, 다 간단하게 수정할 수 있는 것들이니 여기까지 해야겠다.

 

<결론>

간단하게 구현할 수 있을 줄 알았는데 공부하는데 생각보다 오래걸렸다 ...........

그래도 Spring Security를 활용해서 Authentication, Authorization을 전부 처리했으니 서비스와 컨트롤러가 가벼워졌을 듯

로그인 컨트롤러 없애서 뿌듯하다 !

 

 

 

 

 

 

 

 

 

<Spring Security>

인증 / 인가 절차를 Spring에서 관리해주는 기능

Document : https://docs.spring.io/spring-security/reference/servlet/getting-started.html

 

Hello Spring Security :: Spring Security

You can now run the Spring Boot application by using the Maven Plugin’s run goal. The following example shows how to do so (and the beginning of the output from doing so): Running Spring Boot Application $ ./mvn spring-boot:run ... INFO 23689 --- [ resta

docs.spring.io

 

<Spring Security Component>

-Spring Filter : Spring Security는 Filter를 통해 요청이 전달되기 전후의 URL 패턴에 맞는 모든 요청을 필터링해준다.

                          Filter는 보안검사를 통해 올바른 요청이 아닐경우 차단. 이러한 Filter들의 연속을 Filter chain이라고 한다.

                          FilterChain에서 필요한 설정을 분리해서 사용하면 된다.

                          요청이 들어오면 Http객체가 filter를 타고 컨트롤러로 들어올 때, Spring Security가 인증/인가 처리

FilterChain -> Spring Filterchain. 필요한 설정을 분리할 수 있는 환경을 제공한다.

 

 

-AbstractAuthenticationProcessingFilter :  인증을 처리하는 base filter

 

 

 

 

-UsernamePasswordAuthenticationFilter : AbstractAuthenticationProcessingFilter를 상속받은 Filter

                                                                        Form Login을 사용할 때 username과 password를 확인하여 인증

                                                                        Form Login은 인증이 되지 않았다면 로그인페이지를 반환한다.

 

 

 

 

-SecurityContextHolder : Spring security로 인증을 한 사용자의 상세 정보를 가지고 있는 holder

                                           Holder 안의 SecurityContext객체는 Authentication 객체를 가지고있다.

 

 

-Authentication : 인증 객체로, principal // credentials // authorities를 가진다.

                              principal : 사용자를 식별한다. 보통 UserDetails 인스턴스다

                              credentials : 주로 비밀번호로, 사용자 인증에 사용한 후에는 비워둔다.

                              authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용한다.

 

-UserDetailsService : 사용자를 조회하고, 검증한 뒤 UserDetails를 반환한다. Custom하여 Bean으로 등록 후 사용가능

 

-UserDetails : 검증된 UserDetails는 ID/PW token 타입의 Authentication을 만들어 ContextHolder에 세팅할 때 사용.                                  custom 가능

 

 

 

<Spring Security 설정>

 

build.gradle에 dependency 추가

// 스프링 시큐리티
implementation 'org.springframework.boot:spring-boot-starter-security'

 

WebSecurityConfig 클래스 생성

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean   // Bean으로 등록
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf().disable();
        
        http.authorizeRequests()

                           .antMatchers(HttpMethod.GET, "/api/article").permitAll()     <-해당 url로들어오는 GET요청은 허가 

                           .anyRequest().authenticated();         <-나머지 요청은 다 인증을 거치게

        // 로그인 사용
        http.formLogin();      <-Security에서 사용하는 default form login을 사용하겠다

        //  ==> 내 커스텀 로그인페이지 쓰고싶으면 formLogin.loginPage(/api/user/내로그인페이지).permitAll();
        
        return http.build();
    }
}

 

====> localhost:8080으로 요청이 들어오면, 권한을 확인한다. 권한이 Ananymous기 때문에, loginPage로 팅군다.

 

<구현예제>

https://imslo.tistory.com/71

 

CRUD 게시판 리팩토링해보기 - <1> Spring Security, Jwt인증 적용

그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기 1.Spring Security 적용 (JWT방식 커스텀 필터 사용) 2.Controller에 @Valid 어노테이션 적용 3.JPA 연관관계 변경 (양방향 ->

imslo.tistory.com

 

 

@ControllerAdvice와 @ExceptionHandler를 이용하여 예외처리 하는 것을 연습했다.

커스텀Exception을 처리하는 것은 좀 더 공부해야할 것 같다.

 

이번 주 페어와 연관관계에 대해 얘기했다.

 

User정보를 받아와 Article을 생성할 때, User에게도 Article을 추가해줘야하는 이유

=> 아마 DB단에서는 상관이 없을 텐데, Java객체를 사용할 때 DB에서 추가적으로 조회하지 않아도 User가 가진 Article의 정보를 양쪽으로 업데이트 해주는 것이 데이터 정합성이 맞다고 생각함

--------------------------------->공부한 뒤에 포스팅하기

 

@OneToMany에서 발생할 수 있는 문제

=> User를 조회하면 Article을 다 가져와야하고, Article은 또 Comment를 다 가져와야한다.

근데 fetchType을 lazy로 했을 경우에, 객체가 사용될 때 쿼리가 날라간다.

그럼 User객체에 Article을 List로 만들어서 넣어줄 때, Article을 조회하는 쿼리가 날라갈것이다.

그럼 Article을 가져올 때 Comment를 List로 만들어줘야하니까 매 번 쿼리가 새로 날라갈것임.

이렇게 연관관계에서 쿼리가 불필요하게 많이 날라가는 문제를 N+1 문제라고 함.

==> fetch타입을 join으로 해놓으면, 처음 User를 find할 때, fetchtype이 join으로 되어있는 컬럼들을 한꺼번에 가져와 이런 문제를 해결할 수 있음.

---------------------------->까지 얘기했는데, 검증 안되고 우리가 이해만 한 내용이므로 나중에 따로 공부해서 포스팅해야함

 

 

간단한 게시판 CRUD를 만들고 로그인, 회원가입 기능을 구현했다.

그런데, 현재는 서비스 부분에서 발생하는 Exception들을 처리해주지 않아 클라이언트 요청을 처리할 수 없을 때 아무런 응답도 주지 않고 있다.

 

클라이언트의 요청을 처리할 수 없을 때 처리할 수 없음을 알리고, 왜 처리할 수 없는지를 알려주는 것은 중요하다.

 

예를 들면 회원가입 요청 시, 닉네임에 숫자5가 들어가면 안된다는 것을 서버에서만 알고있고 클라이언트에게는 알려주지 않는다고 하자. Exception을 처리하지 않으면 유저는 계속

와 같이 알 수 없는 응답만 받을 것이다.

 

이를 해결하기 위해 @RestControllerAdvice, @ExceptionHandler를 사용했다.

 

@RestControllerAdvice : 컨트롤러에서 발생하는 Exception을 전역적으로 처리할 수 있도록 하는 어노테이션. 파라미터를 주어 적용범위를 특정 패키지나 클래스로 지정할 수 있다. @ControllerAdvice + @ResponseBody로, 바디를 리턴할 수 있다.

@ControllerAdvice

@ControllerAdvice나 @RestControllerAdvice가 달린 클래스 내부 메소드에는 @ExceptionHandler를 선언할 수 있다.

코드와 같이 @ExceptionHandler(Exception.Class)를 선언하면, 해당 Exception을 Controller에서 잡아서 처리해줄수있다.

파라미터에 custom Exception을 주면 해당 Exception을 잡아올 수 있다.

 

Exception이 발생한 경우 클라이언트에게 이를 알려주려면, 무언가를 리턴해주어야한다. HTTP 통신의 Response는 

헤더 // 바디 // HttpStatus로 구성된다.

기존에 내가 하던 것 처럼 Dto를 반환하게 되면, 헤더는 null, 바디에는 Dto의 내용을 JSON으로 파싱한 값이 담기고, 요청에 따른 응답이 리턴되었기에 HTTP status는 200(OK)로 반환하게 된다.

우리는 Exception을 처리했기 때문에 그에 맞춰 HTTP status를 바꿔줘야한다.

ResponseEntity 클래스는 Controller의 반환타입 그 자체인데, 여기에 Dto만 담아 리턴하면 기존에 하던것과 같다.

HttpStatus enum을 같이 담으면 헤더만 비어있게 되며, 헤더도 HttpHeaders 객체에 담아서 반환할 수 있다.

 

제네릭 타입을 받기 때문에 우리가 원하는 내용을 Body에 담을 수 있고, HttpStatus를 포함하는 다양한 생성자가 오버로딩 되어있어 편리하게 사용할 수 있다.

 

<구현>

전역으로 발생하는 IllegalArgumentException을 받아, 그 메세지를 Body에 담고, 내가 원하는 HttpStatus코드와 함께 클라이언트에게 리턴해준다. 쉽게 구현할 수 있다.

 

 

<추가공부>

클라이언트에게 조금 더 자세한 정보를 주고싶을 땐 어떨까 ?

Exception 클래스를 상속받은 나만의 커스텀Exception 클래스를 만들어보자

MyException의 생성자에 message, HttpStatus를 받아 Exception 자체가 HttpStatus를 가지게 만들고,

서비스 부분의 코드에서 Exception을 MyException("아직 미구현입니다", HttpStatus.METHOD_NOT_ALLOWED)

같이 생성해주고, ExceptionHandler에서는 ResponseEntity에 좀 더 자세한 내용을 담고싶었다.

HttpStatus Enum

{

"error code" : 405

"error type" : "Method not allowed",

"message" : "아직 미구현입니다"

}

와 같은 내용을 전달하고 싶었다.

HttpStatus는 Status 코드와, reasonPhrase라는 해당 코드에 대한 설명을 담고있기 때문에 조금더 Exception에 대한 상세한 처리를 할 수 있을 것 같았다.

 

기존 코드
변경 희망 코드

처리되지 않은 예외라고 뜬다.............

이를 해결하기 위해서는 메서드 뒤에 throws MyException을 붙여야하는데, 현재 checkRegisterRequest에 이를 붙이면

이 메서드를 가져다쓰는 메서드 모두에 throws MyException을 달아줘야한다. 심지어 제일 시작점인 Controller에도  throws MyException을 붙여줘야 해결이 되는 것을 확인했다.

 

나는 @ExceptionHandler를 통해서 예외를 처리해주었다고 생각했는데 그게 아니었던 모양이다.

또한, IllegalException으로 처리했을 때는 왜 이런 문제가 발생하지 않았는지 궁금해졌다. 

결국 기본 제공되는 Exception으로 일단 처리해놓았고, 나중에 해결방안을 찾아보고 글 수정해야겠다.

+ Recent posts