-
[개발 노트] Spring Security + JWT + React(SPA) 디버깅 과정Programming/Note 2021. 3. 13. 02:26
프로젝트를 위해 정리한 글입니다. (진행중)
[PART1] 백단
1. [프로젝트 설정] application.yml으로 변경 => oauth 쉽게 하기 위함2. [프로젝트 설정] build.gradle 수정
- jwt
- security3. [엔티티 생성] Member.class는 추후에 변경하기
- test =====> security 정상 작동. security login 페이지로 이동
4. [시큐리티 설정] config > SecurityConfig extends WebSecurityConfiguerAdapter
- configure(http) 오버라이딩하여 커스터마이징
- disable : csrf, sessioncreationpolicy stateless, formlogin, httpbasic
- /api/members/** 요청 권한 설정
- 나머지는 permit all====================test=======================
securityConfig에서 permitAll 해줘서 리액트 login페이지로 이동
====================test=======================5. [Cors(리소스공유) 설정] config > CorsConfig
- corsFilter() 리소스 공유 범위 설정 필터 생성
- javascript json처리 허용
- 모든 ip에 응답 허용
- 모든 header에 응답 허용
- 모든 요청방식(POST, GET,..)에 응답 허용
빈으로 등록하여 스프링 컨테이너가 다루도록 하기6. [필터 등록]
- @RequiredArgsContructor로 @Autowired 필요한 곳에 의존성 주입====================test=======================
정상 작동
====================test=======================7. [시큐리티 세션 > Authentication 객체에 담을 수 있는 타입 생성]
auth > PrincipalDetails implements UserDetails
- 로그인 요청시 시큐리티가 요청을 낚아채 로그인을 대신 진행함.
- 이 때 /login 요청을 받으면 UserDetailsService의 loadUserByUsername()이 호출된다
- 해당 메서드 내에서 /login 요청에 담겨온 로그인 정보를 가지고 UserDetails 타입 객체를 생성한다.
- 시큐리티 세션의 Authentication 객체는 UserDetails 타입만을 포함할 수 있기 때문에 해당 클래스가 필요하다.
***********@noArgConstructor 붙여줘야 JwtAuthenticationFilter에서 import가능8. [/login 요청 처리 메서드]
auth > PrincipalDetailsService implements UserDetailsService
- [MemberRepository 수정] findByMembername 추가
- loadUserByUsername에서 findByMembername으로 받은 값 PrincipalDetails로 감싸서 리턴====================test=======================
빌드 실패 -> findByMembername ~~
====================test=======================9. 대공사
- findByMembername으로 처리하기 위해서는 Member에 Membername 필드가 존재해야 하는데, 그게 id값을 말하는 거임
- Member 엔티티 변경
- MemberRepository 인터페이스 : findbymembername으로 변경
- MemberService, MemberServiceImpl 변경
- MemberController 변경====================test=======================
정상 작동
====================test=======================10. [jwt 만들기 위한 인증 Filter] jwt > JwtAuthenticationFilter
- attemptAuthentication() : 로그인 시도 시 실행
- successfulAuthentication() : 인증 완료 시 실행11. JWTProperty 인터페이스 생성
- secret 키 : "cw"
- 만료 시간 : 10분
- 토큰 prefix : "Bearer "
- 헤더 string : "Authorization"12. 필터 등록
- addFilter(JwtAuthenticationFilter(authenticateManager()))13. MemberController
- /join 메서드 변경====================test=======================
POSTMAN으로 /join 요청java.lang.IllegalArgumentException:
When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
====================test=======================14. [암호화 인코더 생성] SecurityConfig > 필드에 생성
15. [Membercontroller의 /join]에 암호화 적용하기
====================test=======================
POSTMAN으로 /join 요청java.lang.IllegalArgumentException:
When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
====================test=======================
- 1차시도 : controller의 @CrossOrigin("*") 주석 처리 => 실패
- 2차시도 : SecurityConfig CorsFilter 필드에 @Autowired 붙이기
- 3차시도 : corsFilter() => source.registerCorsConfiguration("/api/**", config);
- 4차시도 : corsFilter() => config.setAllowedOrigins() => OriginPatterns()
참고 : https://github.com/spring-projects/spring-framework/issues/26111====================test=======================
(Cors문제 해결)but
POSTMAN으로 /join 요청
java.lang.IllegalArgumentException: 해결
But 403 forbidden
====================test=======================- SecurityConfig 의 access("/api/members/**") 주석처리 => 이거 접근권한 필요하다는 의미
====================test=======================
(권한 문제 해결)but
POSTMAN으로 /join 요청
null pointer exception
====================test=======================- 1차시도 : 생성자 주입 방식에서 @Autowired로 일단 변경
====================test=======================
(null pointer exception 해결)but
org.hibernate.id.IdentifierGenerationException: ids for this class must be manually assigned before calling save(): com.codeworrisors.Movie_Community_Web.model.Member
at org.hibernate.id.Assigned.generate(Assigned.java:33) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:115) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:185) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:128) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:55) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:93) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.internal.SessionImpl.firePersist(SessionImpl.java:720) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at org.hibernate.internal.SessionImpl.persist(SessionImpl.java:706) ~[hibernate-core-5.4.28.Final.jar:5.4.28.Final]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
====================test=======================- 1차시도 : Member의 `Long id`값 generated value 추가 (까먹음..)
- 2차시도 : [빌드 에러] `int id`로 변경
- 3차시도 : @Builder 추가
- 4차시도 : application.yml create => update
- 5차시도(임시방편) @GENERATED VALUE로 안하고 자체적으로 넣어주기
************************추후 고치기====================test=======================
(org.hibernate.id.IdentifierGenerationException: 해결)회원가입 성공!!
암호화되서 db들어감
====================test=======================
16. [회원가입 한 아이디, 비번으로 로그인 처리 시도]
- 1차 : memberController /login주석처리 => 시큐리티가 낚아챌 수 있게
- 2차 : client에서 전송요청 'localhost:8080/login'으로 해야함.===> 제안
- 전체적으로 join이나 login은 /join, /login으로 변경하고
- 접속 후에 요청할 수 있는 페이지를 /api/members/**로 변경하는 게 나을 거같다.- MemberController의 @RequestMapping("/api/members") 주석처리!!
17. [테스트]
로그인된 멤버의 token값으로 /api/members/asdfasdf 요청 => 결과 : 403(forbidden)
=> 인가 처리 필요18. [인가 처리] jwt > JwtAuthenticationFilter()
- 인증이 정상적으로 완료되면 멤버들만 접속할 수 있게
- doFilterInternal에서 권한 확인되면 시큐리티 세션에 강제로 주입
-MemberRepository 의존성 생성자 주입19. SecurityConfig에 해당 필터 등록
- 등록
- memberRepository 여기서 의존성 주입받고, 필터 생성자로 넘겨주기
20. [테스트] 권한처리 성공![version2] view단과 연동
1. localhost:8080 테스트 => 클라이언트가 인증/권한이 필요한 주소를 요청
java.lang.NullPointerException: null
at com.codeworrisors.Movie_Community_Web.config.jwt.JwtAuthorizationFilter.doFilterInternal(JwtAuthorizationFilter.java:52) ~[main/:na]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.3.4.jar:5.3.4]- 원인 : api/~라고 요청 안보내고 모든 url을 보내면 권한/인증 필요하다고 인식해서 JwtAuthorationFilter를 동작시킴.
- 항상 login후 받은 BEARER값잇어야 제대로 됨. POST MAN에서만 보낼 수 있어서 REACT랑 연동 제대로 안된다.<시큐리티 이해를 제대로 못해서 디버깅이 안되는 거 같다>
우선 시큐리티 개념 잡기
1. 시큐리티 기본 개념
- https://sjh836.tistory.com/1651) 인증관련 architecture
(q1) 프로젝트에 시큐리티를 포함시키면 어떤 HttpRequest 던지 AuthenticationFilter를 포함한
인증과정을 먼저 거치게 되는것? NOPE!!
- securitytest 예제에서는 AuthenticationFilter를 커스터마이징해서 쓰지 않았기 때문에,
- IndexController에서 바로 Authentication을 받아옴.
- jwt 예제에서는 public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter
로 커스터마이징해서 사용하며
- /login 요청시 JwtAuthenticationFilter의 attemptAuthentication()이 실행되고
- 로그인(인증) 성공하면 successfulAuthentication()이 실행된다.1-1) localhost8080/으로 요청보내면
/login이 아니기 때문에 UsernamePasswordAuthenticationFilter이 실행되지 않음
하지만 public class JwtAuthorizationFilter extends BasicAuthenticationFilter 실행됨
흠.... 그럼 일단 BasicAuthenticationFilter 개념 나올 때까지 더 읽어보자2) 시큐리티 필터 종류!!!!!
- SecurityContextPersistenceFilter
- LogoutFilter
- (UsernamePassword)AuthenticationFilter =>jwtAuthenticationFilter가 구현
- 성공시 : 얻은 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 실패시 : AuthenticationFailureHandler 실행
- DefaultLoginPageGeneratingFilter
- BasicAuthenticationFilter => jwtAuthorizationFilter가 구현
- HTTP 기본 인증 헤더를 감시하여 처리한다.????
................2) 일단 jwtAuthorizationFilter의 dofilter에서 null point exception을 살펴봄.
- 여기서 서버 터지는 이유는 jwtHeader가 null 일 때 chain.doFilter하면 돌아가는 것이 아니라
다음 코드를 실행하기 때문... 이유는 모르겠다
- 그래서 if else문으로 묶어줌!
-500에러 해결은 되었음하지만, localhost:8080 등 권한 필요없는 주소 요청해도 여전히 현태 필터를 거침..
왜지?
- jwt 프로젝트 실습해본거에서 localhost:8080 요청해봤다.
여기에서도 권한 없는데 doFilterInternal() 실행됨!! 설명이 잘못된듯 싶다.
3) 의문점! jwt 보고 controller에 맵핑 주소 추가해보았따.
// test
@GetMapping("home")
public String home() {
return "home
";
}// test
@GetMapping("{},{/}")
public String main() {
return "메인페이지
";
}- 단순히 /home 주소 요청(컨트롤러 등록)
doFilterInternal()작동하고
home은 작동- /login 주소 요청하고 post로 name,pwd 보내기
JwtAuthenticationFilter의 attemptAuthentication() 호출
From Client : Member(id=0, memberName=qwer, password=qwer, name=null, email=null, address=null, gender=null, birth=null, phone=null, role=null)
Hibernate: select member0_.id as id1_0_, member0_.address as address2_0_, member0_.birth as birth3_0_, member0_.email as email4_0_, member0_.gender as gender5_0_, member0_.member_name as member_name6_0_, member0_.name as name7_0_, member0_.password as password8_0_, member0_.phone as phone9_0_, member0_.role as role10_0_ from member member0_ where member0_.member_name=?
[로그인 완료] 로그인 정보 : Member(id=0, memberName=qwer, password=$2a$10$j9yAHmZ3dKY2rLlapOEza.JQCgp26ZmqeaBQ4/brg6d6gwSXfLwwK, name=qwer, email=qwer, address=qwer, gender=qwer, birth=qwer, phone=qwer, role=ROLE_USER)
JwtAuthenticationFilter의 successfulAuthentication() 호출 : 로그인 인증 완료- Authentication으로 권한 필요한 주소 요청하는 경우 : api/member/asdf
[BasicAuthenticationFilter의 doFilterInternal() 호출]클라이언트가 인증/권한이 필요한 주소를 요청
request.getContextPath():
서명이 정상처리되었음
Hibernate: select member0_.id as id1_0_, member0_.address as address2_0_, member0_.birth as birth3_0_, member0_.email as email4_0_, member0_.gender as gender5_0_, member0_.member_name as member_name6_0_, member0_.name as name7_0_, member0_.password as password8_0_, member0_.phone as phone9_0_, member0_.role as role10_0_ from member member0_ where member0_.member_name=?- Authentication으로 권한 필요없는 주소 요청하는 경우 : GET localhost:8080
[BasicAuthenticationFilter의 doFilterInternal() 호출]클라이언트가 인증/권한이 필요한 주소를 요청
request.getContextPath():
서명이 정상처리되었음
Hibernate: select member0_.id as id1_0_, member0_.address as address2_0_, member0_.birth as birth3_0_, member0_.email as email4_0_, member0_.gender as gender5_0_, member0_.member_name as member_name6_0_, member0_.name as name7_0_, member0_.password as password8_0_, member0_.phone as phone9_0_, member0_.role as role10_0_ from member member0_ where member0_.member_name=?
2021-03-12 18:35:21.641 WARN 9520 --- [nio-8080-exec-7] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]- jwt 프로젝트에서도
@GetMapping("{}, {/}")
public String main() {
return "main
";
}
넣어서 주소요청 해봤음
- 405 에러
클라이언트가 인증이나 권한이 필요한 주소를 요청함.
jwtHeader:null
필터2
필터1
====> 아마 시큐리티에서 localhost:8080으로 바로 접근하는 것을 막아놓고 잇는듯?가 아니다!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@GetMapping({"/", ""}) =========> 이부분 잘못씀...ㅋ
public String main() {
return "main
";
}
- Authorization과 같이 보내야만 localhost:8080을 준다.
<여기까지 알게된 점>
- /login을 제외한 모든 url 주소는 BasicAuthenticationFilter의 doFilterInternal()를 거치며
- Authentication 처리된 경우에만 response해준다.
- controller에 "/"로 매핑해놓은 메서드 있을 경우 return 해줌.
- 만약 "/" 지운다면 리액트로 안가고 405 에러("Method Not Allowed") 뜬다
<테스트 결과>
- 권한 필요없는 페이지. 컨트롤러에 있다면 정상 응답
- 권한 필요한 페이지. 토큰 있다면 정상 응답.
- 하지만, 리액트와 연동 실패
<마지막 테스트>
// .antMatchers("/",
// "/error",
// "/favicon.ico",
// "/**/*.png",
// "/**/*.gif",
// "/**/*.svg",
// "/**/*.jpg",
// "/**/*.html",
// "/**/*.css",
// "/**/*.js")
// .permitAll()없애고 원래 코드로 다시시도 ==> 모두 정상 동작
[part2]
<이제 해볼 거>
컨트롤러에 존재하는 url요청한다면
권한 없이 localhost:8080 띄울 수 있었다.
하지만 리액트와 연동시킨 뷰페이지로 갈 수 없었음.
리액트 연동 부분을 먼저 이해한 뒤에 back과 연결시켜보자.1) test/Client에서 frontend삭제하고 다시 설치
- 참고 https://leeph.tistory.com/252) yarn과 npm의 차이
3) cmd창 진행 멈추는 법 ctrl + c
4) 희상오빠가 준걸로 다시 깔음
https://hjjooace.tistory.com/entry/React-Spring-Gradle-Project-%EC%97%B0%EB%8F%99#none
- 근데 localhost:8080 했는데 안됨.
<새로운 시도>
[자료] https://velog.io/@dsunni/Spring-Boot-React-JWT%EB%A1%9C-%EA%B0%84%EB%8B%A8%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0
- spring security + react 예제 참고'Programming > Note' 카테고리의 다른 글
[개발노트] Spring Security + JWT + React(SPA) 진행 상황 (0) 2021.03.13 [개발노트] Spring + JPA + Oracle 연동 문제해결 (0) 2021.03.05 [개발노트] 영화 커뮤니티 웹사이트 (1) 2021.03.03 댓글