ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [개발 노트] Spring Security + JWT + React(SPA) 디버깅 과정
    Programming/Note 2021. 3. 13. 02:26

    프로젝트를 위해 정리한 글입니다. (진행중)

     

    [PART1] 백단


    1. [프로젝트 설정] application.yml으로 변경 => oauth 쉽게 하기 위함

    2. [프로젝트 설정] build.gradle 수정
    - jwt
    - security

    3. [엔티티 생성] 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/165

    1) 인증관련 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/25

    2) 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 예제 참고

     

     

     

    댓글

Designed by black7375.