묻고 답해요
158만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결스프링 핵심 원리 - 고급편
setProxyTargetClass()
문서에는 true가 기본적으로 설정해서 사용한다고 적혀있는데 default는 false 아닌가요?? 그냥 true를 기본적으로 많이 쓴다는 소리일까요 ?
-
미해결탄탄한 백엔드 NestJS, 기초부터 심화까지
고양이 정보 커뮤니티 프론트앤드와 합칠 때 오류
일단 먼저 제 상황을 말씀드릴게요 섹션 4에 강의 협업을 위한 API만들기와 CORS 문제 강의를 듣던중 프론트앤드 파일을 다운받아서 프론트앤드 파일을 열고 회원가입을 해보는 부분에서 프론트 앤드 파일이 열리지 않았습니다.강의가 작성 된지 시간이 오래 된 것인지 아니면 제가 아직 고양이 정보 커뮤니티에 대한 강의를 다 듣지 못해서 인지 확신이 되지 않아 강사님의 깃허브 05 실전 프로젝트 파일을 다운받아 npm i 로 전부 설치했고(이 때도 워낙 버전이 달라서 버전 fix 를 하는 과정이 필요했습니다.) 그리고 프론트 부분의 파일도 받아서 cd로 프론트파일쪽 폴더로 접근한다음 npm i 로 프론트에 해당하는 패키지까지 전부 설치했습니다. ( 프론트쪽 버전은 백보다 더욱 심각해서 npm install react@latest react-dom@latest 이 명령어를 쳐서 버전업을 하지 않으면 아에 오류가 뜨는 정도였습니다..) 어쨋든 이러한 과정을 마치고 localhost:3000/signup 을 실행하면 이런식으로 오류가 생기면서 되질않았습니다. 이건 일부분의 오류이고 훨씬 더 많은 오류가 생겼습니다.하나하나 다 열거하기 힘들정도라서 개인적으로 이 코드들을 수정하는것이 제 수준에선 불가능했습니다.질문에 답변하신 것을 보니 2023년에도 호환성에 문제가 없다고 되어있는데 진짜 그런지 잘모르겠습니다.
-
미해결[NarP Series] MVC 프레임워크는 내 손에 [나프1탄]
안녕하세요 자료문의 드립니다.
선생님께서 작성한 엑셀 자료는 어디에 올려주셨는지 알 수 있을까요?
-
미해결프로그래밍 시작하기 : 파이썬 입문 (Inflearn Original)
3-1 패키지강의 mod1_test1( )가 실행이 안되요 ㅠㅠ
선생님의 설명을 따라서 입력했는데 sub패키지의 mod1_test1( ) 함수가 실행이 안됩니다... 혹시 문제가 뭔지 알려주실수 있나요?
-
미해결[C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버
CAS 질문 strong vs weak
스핀락 시간에는 strong을 사용하고 이번엔 weak를 사용했는데 이유가 있나요?? compare_exchange_strongcompare_exchange_weak
-
해결됨[코드캠프] 부트캠프에서 만든 고농축 백엔드 코스
비주얼 스튜디오 코드 폴더명 변경시 오류
section03 의 03-10 폴더를section04 에 복사 후이름을 변경하면error:EPERM: OPERATION NOT PERMITTED 가떠서, 비주얼스튜디오코드를 종료후 다시 시작해야 변경이 되는데, 복사 붙이기로 붙여넣은 후에는 별다른 작업을 하지 않았는데도, 왜 이런 오류가 발생되는 걸까요? node_modules를 제외하고 복사 후 npm i 로 설치해야 하나요? 매번 계속 시작하려니 시간이 많이 낭비되는듯하여질문 드립니다~답변주시면 감사하겠습니다~
-
해결됨[리뉴얼] React로 NodeBird SNS 만들기
안녕하세요, 리덕스를 통해서 좋아요 기능구현을 해보고 있는데 디스패치와 충돌이 나는것 같습니다.
안녕하세요, 좋은 강의 감사드립니다. 제가 좋아요 기능구현을 해보고 있는데, 클릭이 되면 state(clicked)를 통해 true로 만들어서 빨강색으로 만들고 1를 추가시키고 다시 클릭하면 false가 되면서 검게 되고 1을 감소하게 하는 기능을 구현하려고 하는데요. 각각 빨/검 1추가시키기는 되는데 둘이 합치려니까 1을 올리면서 리렌더링이 되고 clicked라는 스테이트가 바로 false로 초기화되버리는것 같습니다. 혹시 방법이있을까요? 뭐라고 검색해야 할지 감이 잘안잡혀서 질문남깁니다. const Heart = ({ pageId, id, currentIndex, postId, }) => { const dispatch = useDispatch(); const { coursePosts } = useSelector((state) => state.coursePost); const { clicked, setClicked } = useState(false); const heart = coursePosts[currentIndex].CourseReview[id].heart; const onClickHeart = () => { if (!clicked) { dispatch(coursePostActions.clickCourseReviewHeart({ pageId, postId })) } else { dispatch(coursePostActions.clickCourseReviewHeartAgain({ pageId, postId })) } setClicked(!clicked); } return ( <button type="button" onClick={onClickHeart} className={clicked === true ? styles.active : ""}> <FontAwesomeIcon icon={faHeart} /> <span>{heart}</span> </button> ); }; export default CourseReviewItems;const coursePostSlice = createSlice({ name: "coursePost", initialState, reducers: { clickCourseReviewHeart(state, action) { const postIndex = state.coursePosts.findIndex((v) => v.id === Number(action.payload.pageId)); const reviewIndex = state.coursePosts.findIndex((v) => v.id === action.payload.postId); state.coursePosts[postIndex].CourseReview[reviewIndex].heart += 1 }, clickCourseReviewHeartAgain(state, action) { const postIndex = state.coursePosts.findIndex((v) => v.id === Number(action.payload.pageId)); const reviewIndex = state.coursePosts.findIndex((v) => v.id === action.payload.postId); state.coursePosts[postIndex].CourseReview[reviewIndex].heart -= 1 } }
-
미해결
완전 생초보 입니다. 모바일 앱 제작 무엇부터 시작할까요.
미래에 제가 원하는 아이디어를 앱에 녹여 만들고 싶은 소망이 있습니다. 무엇부터 해야할지 도무지 모르겠습니다. 관련 지식이나 전공자도 아니며 평범한 일반 직장인 입니다. 공대(기계공학과) 나왔으며, 진짜 컴퓨터 잘 모른다고 보시면 됩니다. 이 글 읽어 주셔서 감사 드리며, 행복한 추석 보내시길 바랍니다. 감사합니다.
-
미해결자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
또다시 서버 접속 질문입니다 ㅠ 죄송합니다
이번엔 인바운드에 –sgr-060b562a401b266118081TCP::/0launch-wizard-3 을 규칙으로 넣었더니 빠르게 연결이 안된다고 뜹니다 어떻게 해결하나요?? 재차 질문드려서죄송합니다 ㅠㅠ
-
미해결[발표스피치] 대기업 임직원, 스타트업 대표들이 배우는 청중을 사로잡는 발표 방법!
수강연장문의
회사 일이 갑자기 바빠져서 수강기간 내에 스피치 강의를 모두 못 볼것 같은데 몇일만 더 연장이 가능할까요?ㅠㅠ짬내서 하루에 하나씩 듣고 있는데 절반도 듣지 못해서 정말정말 아쉽습니다 부탁드립니다ㅠㅠ
-
미해결10주완성 C++ 코딩테스트 | 알고리즘 코딩테스트
테케는 통과하는데 왜 틀렸을까요
#include<iostream>#include<vector>using namespace std;int n,d,p,root,visited[54];vector<int>adj[54];int dfs(int here){ visited[here]=1; int ret=0; if(adj[here].size()==0)return 1; for(int there:adj[here]){ if(visited[there])continue; ret+=dfs(there); } return ret;}int main(){ cin >> n; for(int i=0;i<n;i++){ cin >> p; if(p==-1){ root=i; continue; } adj[p].push_back(i); } cin >> d; if(d==root)cout<<0<<"\n"; else{ visited[d]=1; cout << dfs(root) << "\n"; }}
-
미해결자바와 스프링 부트로 생애 최초 서버 만들기, 누구나 쉽게 개발부터 배포까지! [서버 개발 올인원 패키지]
배포까자 완료하였는데 검색하여 서버에 접속할 수 가 없습니다.
배포도 했고 서버도 켰고 swap 설정도하고 인바운드 규칙도 다열고 했는데 서버에 접속하는 시간이 너무 오래걸려서 접속이 안됩나다ㅠㅠ왜그런걸까요?
-
해결됨[퇴근후딴짓] 빅데이터 분석기사 실기 (작업형1,2,3)
데이터 전처리시 궁금점!
데이터 전처리시 범주형(object)이면 라벨 or 원핫 인코딩수치형(int or float) 이면 민맥스스켈러 or z스켈러인데 타겟값이 범주형인가 수치형인가에 따라 전처리 방법을 다르게 하는건가요 아니면타겟값과 상관 없이 문자형, 수치형 둘다 해보는건지 또는 문자형 or 수치형 중에 아무거나 해도상관없는건지 궁금합니다! 또 어떠한 경우에 라벨, 원핫, 민맥스스켈러, z스켈러를 사용하는지도 궁금합니다! ex) 작업형2번 모의문제 1번에서는 타겟 값이 수치형 이지만 전처리시 민맥스스켈러 혹은 z스켈러를 이용하지 않고 문자형컬럼을 제거하거나 라벨, 원한 인코딩을 한 이유가 있을까요?타겟값이 범주형이기 때문에 그런건가요..ㅠㅠ파이썬이 처음이라 개념이 잘 안잡혀 있어서 헷갈리네요 ㅜㅜ
-
미해결나도코딩의 자바 기본편 - 풀코스 (20시간)
안녕하세요 나도코딩 선생님 :)
안녕하세요 나도코딩 선생님 :)곧 추석인데 잘 지내고 계신가요? 선생님 강의 복습하면서 보고있는데 항상 감사드립니다!다름이 아니라, 저도 해당 글쓴님과 같은 의문점이 들어 답변 주신 부분을 확인하였는데요, https://www.inflearn.com/questions/950351답변 주신 해당 부분을 반복해서 읽고 있는데 명확하게 이해가 되지 않아 질문드리는 점 양해 부탁드립니다(__ __)제가 이해한 바로는 Resolution()의 경우, null값으로 반환되어 if문을 getter로 작성하셨고, Price()의 경우는 특정 값 반환으로 인해 if문을 setter로 작성하신 점으로 이해해도 될까요? 추후 코드를 작성할 시 getter와 setter 중 어떤 조건으로 어디에 조건문을 작성하는 것일지 알고싶어 여쭈어봅니다!편안한 연휴 보내시길 바라며, 감사합니다! 전달값이 없는 단순한 getter 메소드는 일반적으로 그냥 인스턴스 변수의 값을 반환하는 역할을 수행합니다.하지만 getResolution() 과 같이 getter 메소드에 조건을 추가하는 이유는 해당 인스턴스 변수의 값이 특정 조건을 만족하지 않을 경우에 대응하는 로직을 제공하기 위해서입니다. 이러한 접근 방식은 일반적으로 값을 반환하는 단순한 getter 메소드에서는 유용합니다.이런 방식을 사용하는 이유는 몇 가지가 있는데요. 1. 안정성인스턴스 변수의 값이 null일 때 getter 메소드가 null을 반환하면, 클라이언트 코드에서 해당 값의 null 여부를 체크하여 잠재적인 문제(=NullPointerException)를 방지하는데 도움이 됩니다.2. 기본값 제공해당 인스턴스 변수가 기본값인 경우, 또는 아직 값이 설정되지 않은 경우를 대비하여 기본 메시지나 기본값을 반환할 수 있습니다.3. 클라이언트 코드 간소화클라이언트 코드에서 추가적인 null 체크나 예외 처리를 하지 않도록 도와줍니다.4. 의도 표현인스턴스 변수가 특정 조건을 만족하지 않을 때 어떤 동작을 하는지 미리 설명하는 역할을 합니다. 이번에는 setPrice() 에 대해 설명 드릴게요.setter 메소드는 주로 인스턴스 변수 값을 설정하는 역할을 합니다.하지만 때로는 값의 유효성을 검사하거나 특정 조건을 만족하지 않을 때 기본값이나 다른 값으로 설정해야 할 경우가 있는데 이때 setter 메소드에서 조건을 확인하고 특정 동작을 수행하는 것은 올바른 접근 방법일 수 있습니다.setPrice() 메소드는 price 인스턴스 변수를 설정하는데, 만약 price가 100,000보다 작으면 100,000으로 설정하고, 그렇지 않으면 주어진 price 값을 그대로 price 인스턴스 변수에 설정합니다. 이렇게 setter 메소드에 조건을 추가하여 값을 검증하고 처리하면 코드의 안정성과 유효성을 높일 수 있습니다. 예를 들어, price가 음수가 될 수 없거나 특정 범위를 벗어나면 이를 감지하고 기본값이나 다른 유효한 값으로 설정할 수 있게 되지요.
-
미해결[코드팩토리] [초급] Flutter 3.0 앱 개발 - 10개의 프로젝트로 오늘 초보 탈출!
textAlign :TextAlign.start 로 정렬하지 않는이유
- 학습 관련 질문을 남겨주세요. 상세히 작성하면 더 좋아요! - 먼저 유사한 질문이 있었는지 검색해보세요. - 서로 예의를 지키며 존중하는 문화를 만들어가요. - 잠깐! 인프런 서비스 운영 관련 문의는 1:1 문의하기를 이용해주세요.코드팩토리 디스코드https://bit.ly/3HzRzUMFlutter 강의를 구매하시면 코드팩토리 디스코드 서버 플러터 프리미엄 채널에 들어오실 수 있습니다! 디스코드 서버에 들어오시고 저에게 메세지로 강의를 구매하신 이메일을 보내주시면 프리미엄 채널에 등록해드려요! 프리미엄 채널에 들어오시면 모든 질의응답 최우선으로 답변해드립니다! text field의 label를 정렬할때Column에 Text와 TextField의 자식이있는데label정렬시에textAlign: TextAlign.start로 적용은 안되고 Column의 crossAxisAlignment를 start로 주었습니다. textAlign이 왜 적용이 안되는지 알기위해text에다가 backgroundColor을 주었더니 딱 label글자크기만큼 차지하는것을 알수있었습니다.Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, //세로축(주축)은 최대너비 / 가로축(반대축은 최소너비) children: [ Container( width: 900, color: Colors.red, child: Text( label, style: TextStyle( color: PRIMARY_COLOR, fontWeight: FontWeight.w600, backgroundColor: Colors.blue, ), textAlign: TextAlign.end, //??왜 이게아니고 crossAxis를 start로 정렬해야하는지.. ), ), textAlign의 사용법을 보니 Text위젯에 Container를 감싸서 너비를 준다음에 textAlign으로 정렬을 하더라고요text의 너비는 그대로 글자크기만큼이고Container의 너비에서 정렬하는게 textAlign인것 같은데 왜 Column으로 감싼 경우는 textAlign이 적용 안되는건가요?? crossAxisAlignment가 start, end 다 적용되는걸 보니 반대축(가로) 너비는 사이즈가 있는것 같아서요
-
미해결자바 ORM 표준 JPA 프로그래밍 - 기본편
값 타입 컬렉션 27분 30초 값 타입 컬렉션을 remove() 할 때 질문드립니다.
학습하는 분들께 도움이 되고, 더 좋은 답변을 드릴 수 있도록 질문전에 다음을 꼭 확인해주세요.1. 강의 내용과 관련된 질문을 남겨주세요.2. 인프런의 질문 게시판과 자주 하는 질문(링크)을 먼저 확인해주세요.(자주 하는 질문 링크: https://bit.ly/3fX6ygx)3. 질문 잘하기 메뉴얼(링크)을 먼저 읽어주세요.(질문 잘하기 메뉴얼 링크: https://bit.ly/2UfeqCG)질문 시에는 위 내용은 삭제하고 다음 내용을 남겨주세요.=========================================[질문 템플릿]1. 강의 내용과 관련된 질문인가요? 예2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? 예3. 질문 잘하기 메뉴얼을 읽어보셨나요? 예[질문 내용]여기에 질문 내용을 남겨주세요.안녕하세요값 타입 컬렉션 27분 30초 질문드립니다. findMember.getAddressHistory().remove(new Address("old1", "street", "10000"));이 코드를 실행할 때deletefromADDRESSwhereMEMBER_ID=?이 쿼리가 실행되어 ADDRESS 테이블에서해당 멤버 id를 외래키로 가지는 레코드를 전부 지우는 것이 이해가 되지 않아서 질문드립니다.저는 자바 코드에서 MEMBER_ID, CITY, STREET, ZIPCODE 에 해당하는 정보를 모두 가지고 있기에아래의 쿼리가 실행되어, 레코드를 하나만 삭제해야 한다고 생각했습니다.deletefromADDRESSwhereMEMBER_ID=? (예를 들어 1)AND CITY=? (예를 들어 newCity1)AND STREET =? (예를 들어 street)AND ZIPCODE=? (예를 들어 10000)이러면 해당 레코드를 JPA에서 직접 식별하고 접근해서 삭제할 수 있을 것입니다.그런데 왜 JPA에서는 2번째 쿼리가 아닌 1번째 쿼리가 실행되어 불필요하게 레코드를 많이 삭제하는지 궁금해서 질문드립니다.
-
미해결
소셜 accessToken 검증
현재 일반 로그인과 소셜 로그인(구글, 네이버)을 구현하려고 하는데 일반 로그인 부분은 해결했지만 막힌 부분이 소셜 로그인을 했을 때 제가 알기로는 프론트에서 받아서 헤더에 담아서 서버(스프링부트)에 보내주면 그거를 검증하고 인증받아야 SecurityContextHoder에 넣으면 컨트롤러에서 정보를 가지고 올 수 있는 것으로 알고 있습니다. 구현하려는 방식(프론트) 소셜 로그인 → (프론트) 헤더에 소셜 accessToken을 서버로 보내준다. → (백엔드) 받고 검증해준다. → (백엔드) 통과 시 SecurityContextHolder.getContext().setAuthentication()에 넣어준다. → 컨트롤러에서 정보를 빼와서 JWT를 만들어서 프론트한테 반환 → 프론트가 뭔가 처리할 때 헤더에 별도로 만든 accessToken 보내준다. 코드SpringSecurity http // JWT를 위한 Filter를 아래에서 만들어 줄건데 // 이 Filter를 어느위치에서 사용하겠다고 등록을 해주어야 Filter가 작동이 됩니다. // JWT를 검증하기 위한 JwtSecurityConfig를 적용하고 // jwtProvider를 사용하여 JWT 검증을 수행합니다. .apply(new JwtSecurityConfig( jwtProvider, googleOAuth2UserService, clientRegistration(), naverOAuth2UserService)); // OAuth2 http // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다. .oauth2Login() .clientRegistrationRepository(clientRegistrationRepository()) // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userInfoEndpoint() // OAuth2 로그인 성공 시, 후작업을 진행할 서비스 .userService(principalOauth2UserService);JwtSecurityConfig일반 로그인 시 발급받은 JWT를 검증하는 곳과 소셜 로그인 accessToken을 검증하는 곳을 나눠서 실행public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final JwtProvider jwtProvider; private final GoogleOAuth2UserService googleOAuth2UserService; // OAuth2 클라이언트 등록 정보가 포함된 객체 private final ClientRegistration clientRegistration; private final NaverOAuth2UserService naverOAuth2UserService; public JwtSecurityConfig(JwtProvider jwtProvider, GoogleOAuth2UserService googleOAuth2UserService, ClientRegistration clientRegistration, NaverOAuth2UserService naverOAuth2UserService) { this.jwtProvider = jwtProvider; this.googleOAuth2UserService = googleOAuth2UserService; this.clientRegistration = clientRegistration; this.naverOAuth2UserService = naverOAuth2UserService; } @Override public void configure(HttpSecurity builder) throws Exception { // JwtAuthenticationFilter가 일반 로그인에 대한 토큰 검증을 처리 // JwtAuthenticationFilter는 Jwt 토큰을 사용하여 사용자의 인증을 처리하는 필터 // 이 필터는 일반 로그인 요청에서 Jwt 토큰을 검증하고 사용자를 인증합니다. // 이 필터를 UsernamePasswordAuthenticationFilter 앞에 추가하여 Jwt 토큰 검증을 먼저 수행하도록 합니다. JwtAuthenticationFilter customFilter = new JwtAuthenticationFilter(jwtProvider); builder.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); // OAuth2 프로바이더(Google 또는 Naver)로부터 제공된 OAuth2 토큰을 사용하여 사용자를 인증하는 역할을 합니다. // 이 필터를 JwtAuthenticationFilter 앞에 추가하여 Jwt 토큰 검증 후 OAuth2 토큰 인증을 수행하도록 합니다. builder.addFilterBefore( new OAuth2TokenAuthentication( googleOAuth2UserService, clientRegistration, naverOAuth2UserService), JwtAuthenticationFilter.class); } }JwtAuthenticationFilter@Log4j2 @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { public static final String HEADER_AUTHORIZATION = "Authorization"; private final JwtProvider jwtProvider; // doFilter는 토큰을 검증하고 // 토큰의 인증정보를 SecurityContext에 담아주는 역할을 한다. @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; // request header에서 JWT를 추출 // 요청 헤더에서 JWT 토큰을 추출하는 역할 String jwt = resovleToken(httpServletRequest); log.info("jwt in JwtAuthenticationFilter : " + jwt); // 어떤 경로로 요청을 했는지 보여줌 String requestURI = httpServletRequest.getRequestURI(); log.info("uri JwtAuthenticationFilter : " + requestURI); if (StringUtils.hasText(jwt) && jwtProvider.validateToken(jwt)) { try { Authentication authentication = jwtProvider.getAuthentication(jwt); log.info("authentication in JwtAuthenticationFilter : " + authentication); // Spring Security의 SecurityContextHolder를 사용하여 현재 인증 정보를 설정합니다. // 이를 통해 현재 사용자가 인증된 상태로 처리됩니다. // 위에서 jwtProvider.getAuthentication(jwt)가 반환이 UsernamePasswordAuthenticationToken로 // SecurityContext에 저장이 되는데 SecurityContextHolder.getContext().setAuthentication(authentication); // 처리를 하는 이유는 다음과 같다. /* * 1. 인증 정보 검증: JWT 토큰이나 다른 인증 정보를 사용하여 사용자를 식별하고 * 권한을 확인하기 위해서는 토큰을 해독하여 사용자 정보와 권한 정보를 추출해야 합니다. * 이 역할은 jwtProvider.getAuthentication(jwt)에서 수행됩니다. * 이 메서드는 JWT 토큰을 분석하여 사용자 정보와 권한 정보를 추출하고, 해당 정보로 인증 객체를 생성합니다. * * 2. 인증 정보 저장: * 검증된 인증 객체를 SecurityContextHolder.getContext().setAuthentication(authentication);를 * 사용하여 SecurityContext에 저장하는 이유는, Spring Security에서 현재 사용자의 인증 정보를 * 전역적으로 사용할 수 있도록 하기 위함입니다. 이렇게 하면 다른 부분에서도 현재 사용자의 인증 정보를 사용할 수 있게 되며, * Spring Security가 제공하는 @AuthenticationPrincipal 어노테이션을 통해 현재 사용자 정보를 편리하게 가져올 수 있습니다. * */ SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { throw new RuntimeException("잘못된 형식의 JWT입니다."); } } filterChain.doFilter(request, response); } // 토큰을 가져오기 위한 메소드 // Authorization로 정의된 헤더 이름을 사용하여 토큰을 찾고 // 토큰이 "Bearer "로 시작하거나 "Bearer "로 안온 것도 토큰 반환 private String resovleToken(HttpServletRequest httpServletRequest) { String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION); log.info("token : " + token); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { return token.substring(7); } else if (StringUtils.hasText(token)) { return token; } else { return null; } }OAuth2TokenAuthenticationpackage com.example.social.config.oauth2; import com.example.social.config.oauth2.verifirer.GoogleOAuth2UserService; import com.example.social.config.oauth2.verifirer.NaverOAuth2UserService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Log4j2 @RequiredArgsConstructor public class OAuth2TokenAuthentication extends OncePerRequestFilter { public static final String HEADER_AUTHORIZATION = "Authorization"; private final GoogleOAuth2UserService googleOAuth2UserService; private final ClientRegistration clientRegistration; private final NaverOAuth2UserService naverOAuth2UserService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest httpServletRequest = (HttpServletRequest) request; String jwt = resovleToken(httpServletRequest); String getIssuer = checkToken(jwt); if ("https://accounts.google.com".equals(getIssuer)) { // Google에서 발급한 토큰을 기반으로 OAuth2AccessToken 객체를 생성합니다. // 이 객체는 토큰의 타입, 값, 발급 시간 및 만료 시간과 같은 정보를 포함합니다. OAuth2AccessToken accessToken = new OAuth2AccessToken( // 토큰 타입 OAuth2AccessToken.TokenType.BEARER, // 토큰 값 jwt, // 발급 시간 null, // 만료 시간 null); // OAuth2UserRequest 생성 // 이 객체는 클라이언트 등록 정보와 OAuth2AccessToken 객체를 포함하여 // OAuth 2.0 사용자 정보 요청에 필요한 정보를 제공합니다. OAuth2UserRequest userRequest = new OAuth2UserRequest( // ClientRegistration 객체 clientRegistration, // OAuth2AccessToken 객체 accessToken); // googleOAuth2UserService를 사용하여 사용자 정보를 검증하고 OAuth2User 객체를 얻어옵니다. OAuth2User oAuth2User = googleOAuth2UserService.loadUser(userRequest); // SecurityContextHolder에 인증 정보 설정 // 검증된 사용자 정보를 기반으로 OAuth2AuthenticationToken을 생성합니다. // 이 토큰은 Spring Security에서 사용되며, 사용자 정보와 사용자의 권한을 포함합니다. Authentication authentication = new OAuth2AuthenticationToken( oAuth2User, oAuth2User.getAuthorities(), // registrationId "google"); // SecurityContextHolder에 인증 정보 설정 SecurityContextHolder.getContext().setAuthentication(authentication); } if ("https://api.naver.com".equals(getIssuer)) { // 다른 발급자 (Google이 아닌) 경우 // 네이버로부터 받은 토큰을 OAuth2AccessToken 객체로 생성합니다. // 이 객체는 토큰의 타입, 값, 발급 시간 및 만료 시간 등을 포함합니다. // 이 정보는 OAuth2 인증을 수행하는 데 사용됩니다. OAuth2AccessToken naverAccessToken = new OAuth2AccessToken( OAuth2AccessToken.TokenType.BEARER, jwt, null, null); // 네이버 OAuth2 클라이언트 등록 정보와 받은 토큰을 사용하여 OAuth2 사용자 정보 요청을 만듭니다. // 이 요청은 네이버로부터 사용자 정보를 가져오는 데 사용됩니다. OAuth2UserRequest naverUserRequest = new OAuth2UserRequest( clientRegistration, naverAccessToken); // 네아로 API를 사용하여 사용자 정보 가져오기 // 이것은 네이버 API를 호출하고 사용자 정보를 가져오는 로직이 포함된 서비스입니다. OAuth2User naverUser = naverOAuth2UserService.loadUser(naverUserRequest); // 네이버에서 가져온 사용자 정보를 기반으로 Authentication 객체 생성 // 네이버로부터 가져온 사용자 정보를 기반으로 OAuth2 인증 토큰을 생성합니다. // 이 토큰은 Spring Security에서 사용되며 사용자 정보와 사용자의 권한을 포함합니다. // 여기서 "naver"는 등록 ID로 지정되며, 네이버와 관련된 토큰임을 나타냅니다. Authentication naverAuthentication = new OAuth2AuthenticationToken( naverUser, naverUser.getAuthorities(), // registrationId "naver"); // 네이버 Authentication 객체를 SecurityContext에 저장 SecurityContextHolder.getContext().setAuthentication(naverAuthentication); } } // 토큰을 가져오기 위한 메소드 // Authorization로 정의된 헤더 이름을 사용하여 토큰을 찾고 // 토큰이 "Bearer "로 시작하거나 "Bearer "로 안온 것도 토큰 반환 private String resovleToken(HttpServletRequest httpServletRequest) { String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION); log.info("token : " + token); if (StringUtils.hasText(token) && token.startsWith("Bearer ")) { return token.substring(7); } else if (StringUtils.hasText(token)) { return token; } else { return null; } } private String checkToken(String token) { try { JwtParser parser = Jwts.parserBuilder().build(); // 주어진 토큰을 디코딩하여 JWT 클레임을 추출합니다. // 이 클레임에는 토큰에 대한 정보가 포함되어 있으며, // 이 코드에서는 클레임에서 발급자(issuer) 정보를 확인하기 위해 사용합니다. Claims claims = parser.parseClaimsJws(token).getBody(); // iss 클레임을 확인하여 Google 발급 토큰 여부를 판별 String issuer = (String) claims.get("iss"); log.info("issuer : " + issuer); return issuer; } catch (Exception e) { // 예외 처리: JWT 디코딩 실패 시 처리할 내용을 여기에 추가 log.error("JWT 디코딩 실패: " + e.getMessage(), e); throw new RuntimeException("JWT 디코딩 실패: " + e.getMessage(), e); } } }GoogleOAuth2UserServicepackage com.example.social.config.oauth2.verifirer; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.jackson2.JacksonFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.oauth2.core.user.OAuth2UserAuthority; import org.springframework.stereotype.Service; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.Collections; import java.util.HashMap; import java.util.Map; @Service public class GoogleOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { @Value("${spring.security.oauth2.client.registration.google.client-id}") private String googleClientId; private final HttpTransport httpTransport = new NetHttpTransport(); private final JsonFactory jsonFactory = new JacksonFactory(); @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { String accessToken = userRequest.getAccessToken().getTokenValue(); // Google API 클라이언트 라이브러리를 사용하여 accessToken을 검증 // GoogleIdTokenVerifier는 Google API의 ID 토큰을 검증하기 위한 도구입니다. // Google의 OAuth 2.0 서비스를 통해 발급된 ID 토큰의 유효성을 확인하고, // 해당 토큰이 애플리케이션의 클라이언트 ID에 대한 것인지 확인하는 역할을 합니다. // httpTransport: HTTP 통신을 처리하는 라이브러리를 지정하는 부분입니다. // Google API와 통신하기 위해 사용하는 HTTP 트랜스포트 라이브러리를 설정합니다. // jsonFactory: JSON 데이터를 파싱하고 생성하는 데 사용되는 라이브러리를 지정하는 부분입니다. // Google API와 통신할 때 JSON 형식의 데이터를 처리하는 데 사용됩니다. GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(httpTransport, jsonFactory) // 이 부분에서는 검증할 ID 토큰의 대상(audience)을 설정합니다. // 여기서는 Google API 클라이언트 ID를 대상으로 설정하고 있으며, // 이것은 해당 ID 토큰이 특정 클라이언트(여기서는 구글 클라이언트)에 대한 것임을 나타냅니다. .setAudience(Collections.singletonList(googleClientId)) .build(); GoogleIdToken idToken; try { // Google의 ID 토큰 검증기(GoogleIdTokenVerifier)를 사용하여 // 주어진 액세스 토큰(accessToken)을 검증하는 작업을 수행합니다. idToken = verifier.verify(accessToken); } catch (GeneralSecurityException e) { throw new RuntimeException(e); } catch (IOException e) { throw new RuntimeException(e); } if(idToken != null) { // Google ID 토큰(idToken)에서 클레임(claims)을 추출하기 위해 // idToken 객체에서 Payload를 얻어옵니다. 토큰의 페이로드는 토큰에 포함된 클레임 정보를 담고 있습니다. Payload payload = idToken.getPayload(); // 사용자 정보를 저장할 attributes 맵을 생성합니다. // 이 맵에는 사용자의 이름, 이메일, 서브젝트(sub), 그리고 권한 정보가 포함될 것입니다. Map<String, Object> attributes = new HashMap<>(); // 사용자 정보를 추출하고 OAuth2User 객체로 변환 // 페이로드에서 이름(사용자의 실제 이름)을 추출 attributes.put("name", payload.get("name")); // 페이로드에서 이메일을 추출 attributes.put("email", payload.getEmail()); // 페이로드에서 서브젝트(sub) 정보를 추출 attributes.put("sub", payload.getSubject()); // 사용자에게 ROLE_USER 권한을 부여하기 위해 attributes 맵에 권한 정보를 추가합니다. attributes.put("auth", new SimpleGrantedAuthority("ROLE_USER")); // DefaultOAuth2User는 Spring Security에서 OAuth 2.0 사용자를 나타내는 구현 클래스입니다. // 이 객체는 사용자의 인증 정보와 권한 정보를 가지고 있습니다. return new DefaultOAuth2User( Collections.singleton(new OAuth2UserAuthority(attributes)), attributes, "sub" ); } else { throw new OAuth2AuthenticationException("Google AccessToken verification failed"); } } }NaverOAuth2UserServicepackage com.example.social.config.oauth2.verifirer; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.DefaultOAuth2User; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestTemplate; import java.util.Collections; import java.util.Map; @Service public class NaverOAuth2UserService { public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { String accessToken = userRequest.getAccessToken().getTokenValue(); // 네이버 API 엔드포인트 URL String naverApiUrl = "https://openapi.naver.com/v1/nid/me"; // 네이버 API 호출을 위한 HTTP 헤더 설정 HttpHeaders headers = new HttpHeaders(); // headers 객체의 setContentType 메서드를 사용하여 요청의 Content-Type 헤더를 설정합니다. // MediaType.APPLICATION_FORM_URLENCODED는 HTTP 요청 본문이 폼 데이터로 인코딩되어 있다는 것을 나타냅니다. // 이러한 형태의 요청은 주로 HTML 폼 데이터를 서버로 제출할 때 사용됩니다. headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); // Authorization 헤더를 설정합니다. 이 헤더는 HTTP 요청에 인증 정보를 포함하는 데 사용됩니다. // 여기서 "Bearer " + accessToken는 OAuth 2.0 인증 프로토콜을 사용하여 // 보호된 리소스에 액세스하기 위한 액세스 토큰을 Bearer 스타일로 설정하는 것을 나타냅니다. // Bearer는 토큰의 타입을 나타내며, 액세스 토큰의 실제 값을 나타냅니다. // 이렇게 설정된 Authorization 헤더를 통해 서버는 요청이 인증되었음을 확인하고, // 해당 액세스 토큰의 유효성을 검증할 수 있습니다. headers.set("Authorization", "Bearer " + accessToken); // 네이버 API 호출을 위한 요청 파라미터 설정 // MultiValueMap은 HTTP 요청 본문의 데이터를 표현하기 위한 자료 구조입니다. // 네이버 API 호출 시 요청 본문에 보낼 데이터를 설정하기 위해 body라는 MultiValueMap 객체를 생성합니다. // 이 데이터는 폼 데이터 형식으로 API 서버로 전송될 것입니다. MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); // 네이버 API 호출 // RestTemplate은 Spring Framework에서 제공하는 // HTTP 요청을 보내고 응답을 받는 데 사용되는 클라이언트 라이브러리입니다. RestTemplate restTemplate = new RestTemplate(); // RestTemplate을 사용하여 네이버 API를 호출하고 응답을 받습니다. // naverApiUrl: 네이버 API의 엔드포인트 URL을 나타냅니다. // 이 URL은 네이버 API의 특정 엔드포인트에 요청을 보내기 위해 사용됩니다. // body 객체가 요청 본문에 담길 것입니다. // API 응답을 받을 데이터 유형을 지정합니다. // 여기서는 API 응답을 Map 형태로 받을 것이며, 이 Map에는 API 응답의 JSON 데이터가 매핑됩니다. Map<String, Object> naverApiResult = restTemplate.patchForObject(naverApiUrl, body, Map.class); // 네이버 API 응답을 파싱하여 사용자 정보 추출 // naverApiResult라는 Map에서 "id" 키에 해당하는 값을 추출합니다. // 이 값은 네이버 사용자의 고유 식별자인 사용자 ID를 나타냅니다. String naverUserId = (String) naverApiResult.get("id"); String naverUserEmail = (String) naverApiResult.get("email"); String naverUserName = (String) naverApiResult.get("name"); // 추출한 사용자 정보로 OAuth2User 객체 생성 // 이객체는 Spring Security OAuth2에서 사용자 정보를 나타내는 데 사용됩니다. OAuth2User naverUser = new DefaultOAuth2User( // 사용자에게 부여할 권한을 설정합니다. Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), // 사용자의 고유 식별자인 네이버 사용자 ID를 "naverUserId"라는 키와 함께 맵 형태로 저장합니다. Collections.singletonMap("naverUserId", naverUserId), // OAuth2User의 이름을 지정합니다. 이것은 OAuth2User 객체 내에서 사용됩니다. "naverUserId" ); // 이메일 정보 추가 // 생성한 OAuth2User 객체에서 추가 정보를 설정합니다. // 이 경우, "naverUserEmail" 키를 사용하여 네이버 사용자의 이메일 정보를 맵에 추가합니다. ((DefaultOAuth2User) naverUser).getAttributes().put("naverUserEmail", naverUserEmail); // 이렇게 생성된 naverUser 객체에는 사용자의 권한과 사용자 ID, 그리고 이메일 정보가 포함되어 있습니다. // 이 객체는 사용자 인증 후에 Spring Security의 SecurityContextHolder에 // 저장되어 사용자 정보를 유지하고 제공합니다. return naverUser; } }controller@GetMapping("/success-oauth") public ResponseEntity<?> oauth2Token(@AuthenticationPrincipal OAuth2User oAuth2User) { log.info("oAuth2User : " + oAuth2User); String name = oAuth2User.getName(); log.info("name :" + name); ResponseEntity<?> tokenForOAuth2 = memberService.createTokenForOAuth2(name); return ResponseEntity.ok().body(tokenForOAuth2); }PrincipalOauth2UserService소셜 로그인 버튼을 누르고 성공하면 바로 소셜 로그인 유저 정보와 바로 가입할 수 있도록 구현package com.example.social.config.oauth2; import com.example.social.config.auth.PrincipalDetails; import com.example.social.config.oauth2.provider.GoogleUserInfo; import com.example.social.config.oauth2.provider.NaverUserInfo; import com.example.social.config.oauth2.provider.OAuth2UserInfo; import com.example.social.domain.Role; import com.example.social.entity.MemberEntity; import com.example.social.repository.MemberRepository; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; import java.util.Map; // 소셜 로그인하면 사용자 정보를 가지고 온다. // 가져온 정보와 PrincipalDetails 객체를 생성합니다. @Service @Log4j2 @RequiredArgsConstructor public class PrincipalOauth2UserService extends DefaultOAuth2UserService { private final MemberRepository memberRepository; private final BCryptPasswordEncoder bCryptPasswordEncoder; @Override // loadUser 함수 : userRequest 정보로 loadUser 함수를 이용하여 회원 프로필을 받는다. public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // registrationId로 어떤 OAuth로 로그인 했는지 확인가능 log.info("clientRegistration in PrincipalOauth2UserService : " + userRequest.getClientRegistration()); log.info("accessToken in PrincipalOauth2UserService : " + userRequest.getAccessToken().getTokenValue()); // OAuth2 유저 정보를 가져옵니다. OAuth2User oAuth2User = super.loadUser(userRequest); // 구글 로그인 버튼 클릭 →구글 로그인 창 → 로그인 완료 → code 를 리턴(OAuth-Client 라이브러리) → AccessToken 요청 // userRequest 정보 → 회원 프로필 받아야함(loadUser 함수 호출) → 구글로부터 회원 프로필을 받아준다. log.info("getAttributes in PrincipalOauth2UserService : " + oAuth2User.getAttributes()); // 회원가입 강제 진행 OAuth2UserInfo oAuth2UserInfo = null; // 소셜 정보를 가지고 옵니다. // OAuth2 서비스 아이디 if(userRequest.getClientRegistration().getRegistrationId().equals("google")) { log.info("구글 로그인 요청"); oAuth2UserInfo = new GoogleUserInfo(oAuth2User.getAttributes()); } else if(userRequest.getClientRegistration().getRegistrationId().equals("naver")) { log.info("네이버 로그인 요청"); // 네이버는 response를 json으로 리턴을 해주는데 아래의 코드가 받아오는 코드다. // response={id=5SN-ML41CuX_iAUFH6-KWbuei8kRV9aTHdXOOXgL2K0, email=zxzz8014@naver.com, name=전혜영} // 위의 정보를 NaverUserInfo에 넘기면 oAuth2UserInfo = new NaverUserInfo((Map) oAuth2User.getAttributes().get("response")); }else { log.info("지원하지 않는 소셜 로그인입니다."); } // 사용자가 로그인한 소셜 서비스를 가지고 옵니다. // 예시) google or naver 같은 값을 가질 수 있다. String provider = oAuth2UserInfo.getProvider(); // 사용자의 소셜 서비스(provider)에서 발급된 고유한 식별자를 가져옵니다. // 이 값은 해당 소셜 서비스에서 유니크한 사용자를 식별하는 용도로 사용됩니다. String providerId = oAuth2UserInfo.getProviderId(); String name = oAuth2UserInfo.getName(); String password = bCryptPasswordEncoder.encode("get"); // 사용자의 이메일 주소를 가지고 옵니다. // 소셜 서비스에서 제공하는 이메일 정보를 사용합니다. String email = oAuth2UserInfo.getEmail(); // 사용자의 권한 정보를 설정합니다. // 소셜 로그인으로 접속하면 무조건 USER로 등록되게 설정 Role role = Role.USER; // UUID를 사용하여 랜덤한 문자열 생성 // 닉네임을 랜덤으로 설정할거면 이 코드 적용 // UUID uuid = UUID.randomUUID(); // // External User 줄임말 : EU // String randomNickName = // "EU" + uuid.toString().replace("-", "").substring(0, 9); // 닉네임을 소셜 아이디 이름을 가지고 적용한다. String nickName = oAuth2UserInfo.getName(); // 이메일 주소를 사용하여 이미 해당 이메일로 가입된 사용자가 있는지 데이터베이스에서 조회합니다. MemberEntity member = memberRepository.findByUserEmail(email); if(member == null) { log.info("OAuth 로그인이 최초입니다."); log.info("OAuth 자동 회원가입을 진행합니다."); member = MemberEntity.builder() .userEmail(email) .role(role) .userName(name) .provider(provider) .providerId(providerId) .userPw(password) .nickName(nickName) .build(); log.info("member : " + member); memberRepository.save(member); }else { log.info("로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다."); // 이미 존재하는 회원이면 업데이트를 해줍니다. member = MemberEntity.builder() .userId(member.getUserId()) .userEmail(email) .role(role) .userName(name) .provider(provider) .providerId(providerId) .userPw(password) .nickName(nickName) .build(); memberRepository.save(member); } // attributes가 있는 생성자를 사용하여 PrincipalDetails 객체 생성 // 소셜 로그인인 경우에는 attributes도 함께 가지고 있는 PrincipalDetails 객체를 생성하게 됩니다. PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes()); log.info("principalDetails in PrincipalOauth2UserService : " + principalDetails); return principalDetails; } }PrincipalDetailspackage com.example.social.config.auth; import com.example.social.entity.MemberEntity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collection; import java.util.Map; // PrincipalDetails 클래스는 UserDetails 인터페이스를 구현하여 사용자의 정보와 권한을 저장하는 역할을 하고 있습니다. // 여기서 JwtProvider 에 정보를 줘서 토큰을 생성하게 한다. // UserDetails → 일반 로그인 // OAuth2User → 소셜 로그인 @Setter @Getter @ToString @Log4j2 @NoArgsConstructor @Component public class PrincipalDetails implements UserDetails, OAuth2User { // 일반 로그인 정보를 저장하기 위한 필드 private MemberEntity member; // OAuth2 로그인 정보를 저장하기 위한 필드 // attributes는 Spring Security에서 OAuth2 인증을 수행한 후에 // 사용자에 대한 추가 정보를 저장하는 데 사용되는 맵(Map)입니다. // OAuth2 인증은 사용자 인증 후에 액세스 토큰(Access Token)을 발급받게 되는데, // 이 토큰을 사용하여 OAuth2 서비스(provider)로부터 사용자의 프로필 정보를 요청할 수 있습니다. // 예를 들어, 소셜 로그인을 사용한 경우에는 attributes에는 사용자의 소셜 서비스(provider)에서 제공하는 프로필 정보가 담겨 있습니다. // 소셜 로그인 서비스(provider)마다 제공하는 프로필 정보가 다를 수 있습니다. // 일반적으로 attributes에는 사용자의 아이디(ID), 이름, 이메일 주소, 프로필 사진 URL 등의 정보가 포함됩니다. /* * 구글의 경우 * { "sub": "100882758450498962866", // 구글에서 발급하는 고유 사용자 ID "name": "John Doe", // 사용자 이름 "given_name": "John", // 이름(이름 부분) "family_name": "Doe", // 성(성(성) 부분) "picture": "https://lh3.googleusercontent.com/a/AAcHTtdzQomNwZCruCcM0Eurcf8hAgBHcgwvbXEBQdw3olPkSg=s96-c", // 프로필 사진 URL "email": "johndoe@example.com", // 이메일 주소 "email_verified": true, // 이메일 주소 인증 여부 "locale": "en" // 지역 설정 } * */ private Map<String, Object> attributes; // 일반 로그인 // 여기서는 Oauth2를 사용하지 않고 JWT와 security만 사용할 거임 public PrincipalDetails(MemberEntity member) { this.member = member; } // OAuth2 로그인 public PrincipalDetails(MemberEntity member, Map<String, Object> attributes) { this.member = member; this.attributes = attributes; } // 해당 유저의 권한을 권한을 리턴 @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> collection = new ArrayList<>(); collection.add(new SimpleGrantedAuthority("ROLE_" + member.getRole().toString())); return collection; } // 사용자 패스워드를 반환 @Override public String getPassword() { return member.getUserPw(); } // 사용자 이름 반환 @Override public String getUsername() { return member.getUserEmail(); } // 계정 만료 여부 반환 @Override public boolean isAccountNonExpired() { // 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 잠금 여부 반환 @Override public boolean isAccountNonLocked() { // true = 잠금되지 않음 return true; } // 패스워드의 만료 여부 반환 @Override public boolean isCredentialsNonExpired() { // 패스워드가 만료되었는지 확인하는 로직 // true = 만료되지 않음 return true; } // 계정 사용 가능 여부 반환 @Override public boolean isEnabled() { // 계정이 사용 가능한지 확인하는 로직 // true = 사용 가능 return true; } @Override public Map<String, Object> getAttributes() { log.info("attributes : " + attributes); return attributes; } @Override // OAuth2 인증에서는 사용되지 않는 메서드이므로 null 반환 public String getName() { return null; } } JWT 구현 로직 생략 문제점(백엔드) 받고 검증해준다. ↑ 이 부분에서 막혔습니다. private String checkToken(String token) { try { JwtParser parser = Jwts.parserBuilder().build(); // 주어진 토큰을 디코딩하여 JWT 클레임을 추출합니다. // 이 클레임에는 토큰에 대한 정보가 포함되어 있으며, // 이 코드에서는 클레임에서 발급자(issuer) 정보를 확인하기 위해 사용합니다. Claims claims = parser.parseClaimsJws(token).getBody(); // iss 클레임을 확인하여 Google 발급 토큰 여부를 판별 String issuer = (String) claims.get("iss"); log.info("issuer : " + issuer); return issuer; } catch (Exception e) { // 예외 처리: JWT 디코딩 실패 시 처리할 내용을 여기에 추가 log.error("JWT 디코딩 실패: " + e.getMessage(), e); throw new RuntimeException("JWT 디코딩 실패: " + e.getMessage(), e); } }여기서 issuer을 가져와서 구글인지 네이버인지 확인하려고 하는 과정에서 JWT는 .이 2개인데 구글이나 네이버 같은 경우는 .이 1개라 오류가 발생하는 거 같습니다... 구글, 네이버 검증해서 JWT를 반환하려고 하는데 계속 실패하니 막막하네요... 질문소셜 로그인같이 .이 1개일 때는 어떻게 거기서 정보를 빼올 수 있나요? 보통 강의에서 보면 소셜 accessToken을 받고 검증해서 거기서 정보를 빼는게 아니라 그냥 로직을 구현하면 정보를 가져오던데 저는 JWT 검증 로직에서 계속 소셜로그인 accessToken이 잘못되었다고 떠서 소셜 로그인 accessToken을 검증하는 로직을 구현했는데 이렇게 하는게 맞나요?
-
미해결[리뉴얼] React로 NodeBird SNS 만들기
강좌 끝나고 댓글 수정 기능 만들어 보고 있습니다
{commentFormOpened && ( <div> {commentEditMode ? <CommentContent post={post} commentEditMode={commentEditMode} onClickUpdateComment={onClickUpdateComment} onCancelUpdateComment={onCancelUpdateComment} /> : ( <> <CommentForm post={post} /> <CommentContent post={post} commentEditMode={commentEditMode} onClickUpdateComment={onClickUpdateComment} onCancelUpdateComment={onCancelUpdateComment} /> </> ) } </div> )} //PostCard.js import React, { useCallback, useEffect, useState } from 'react'; import { Avatar, Comment, List, Input, Button, Popover } from 'antd'; import { useDispatch, useSelector } from 'react-redux'; import Link from 'next/link'; import PropTypes from 'prop-types'; import { EllipsisOutlined } from '@ant-design/icons'; import { UPDATE_COMMENT_REQUEST } from '../reducers/post'; const {TextArea} = Input; const CommentContent = ({ post, onCancelUpdateComment, commentEditMode, onClickUpdateComment }) => { const dispatch = useDispatch(); const id = useSelector((state) => state.user?.me.id); const { updateCommentLoading, updateCommentDone } = useSelector((state) => state.post); const [editText, setEditText] = useState(post.Comments.content); useEffect(() => { if (updateCommentDone) { onCancelUpdateComment(); } }, [updateCommentDone]); const onChangeCommentText = useCallback((e) => { setEditText(e.target.value); }, []); const onChangeComment = useCallback(() => { dispatch({ type: UPDATE_COMMENT_REQUEST, data: { PostId: post.id, CommentId: post.Comments.id, UserId: id, content: editText, }, }); }, [post, id, editText, post.Comments.id]); return ( <div> {commentEditMode ? ( <> <TextArea value={editText} onChange={onChangeCommentText} /> <Button.Group> <Button loading={updateCommentLoading} onClick={onChangeComment}>수정</Button> <Button type="danger" onClick={onCancelUpdateComment}>수정 취소</Button> </Button.Group> </> ) : <List header={`${post.Comments.length}개의 댓글`} itemLayout="horizontal" dataSource={post.Comments} renderItem={(item) => ( <li> <Comment actions={[<Popover key="more" content={ <Button.Group> {id && item.User.id === id ? ( <> <Button onClick={onClickUpdateComment}>수정</Button> <Button type="danger"> 삭제 </Button> </> ) : ( <Button>신고</Button> )} </Button.Group> } > <EllipsisOutlined /> </Popover>,]} author={item.User.nickname} avatar={ <Link href={`/user/${item.User.id}`}> <a><Avatar>{item.User.nickname[0]}</Avatar></a> </Link> } content={item.content} /> </li> )} /> } </div> ) } CommentContent.propTypes = { post: PropTypes.shape({ id: PropTypes.number.isRequired, Comments: PropTypes.arrayOf(PropTypes.shape({ id: PropTypes.number.isRequired, content: PropTypes.string.isRequired, })) }), onCancelUpdateComment: PropTypes.func.isRequired, onClickUpdateComment: PropTypes.func.isRequired, commentEditMode: PropTypes.bool }; CommentContent.defaultsProps = { commentEditMode: false, } export default CommentContent; //CommentContent.js case UPDATE_COMMENT_REQUEST: draft.updateCommentLoading = true; draft.updateCommentDone = false; draft.updateCommentError = null; break; case UPDATE_COMMENT_SUCCESS: draft.updateCommentLoading = false; draft.updateCommentDone = true; const post = draft.mainPosts.find((v) => v.id === action.data.PostId); post.Comments = post.Comments.find((v) => v.id === action.data.CommentId); post.Comments = post.Comments.find((v) => v.id === action.data.UserId); post.Comments.content = action.data.content; break; case UPDATE_COMMENT_FAILURE: draft.updateCommentLoading = false; draft.updateCommentError = action.error; break; //reducers/post.js function updateCommentAPI(data) { return axios.patch(`/post/${data.PostId}/comment`, data); } function* updateComment(action) { try { const result = yield call(updateCommentAPI, action.data); yield put({ type: UPDATE_COMMENT_SUCCESS, data: result.data, }); } catch (err) { console.error(err); yield put({ type: UPDATE_COMMENT_FAILURE, error: err.response.data, }); } } // sagas/post.js router.patch('/:postId/comment', isLoggedIn, async (req, res, next) => { // PATCH post/2/comment try { await Comment.update({ content: req.body.content, }, { where: { PostId: req.params.postId, UserId: req.user.id, }, }); res.status(200).json({ PostId: parseInt(req.params.postId, 10), UserId: req.user.id, content: req.body.content, }); } catch (error) { console.error(error); next(error); } }); //routes/post.js이렇게 PostId, UserId, content가 보내지고, 실패가 뜨면서 새로고침을 하면 해당 글에 달았던 댓글들이 모두 다 "zzz"로 변경되어 있습니다. 그래서 CommentId를 보내줘야 될 거 같은데 여기서 막혀서 감이 도무지 잡히질 않습니다.
-
미해결
pandas-profiling 사용 문제
--------------------------------------------------------------------------- PydanticImportError Traceback (most recent call last) Cell In[207], line 8 6 from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score 7 from sklearn.linear_model import LinearRegression ----> 8 from pandas_profiling import ProfileReport File ~\anaconda3\Lib\site-packages\pandas_profiling\__init__.py:6 1 """Main module of pandas-profiling. 2 3 .. include:: ../../README.md 4 """ ----> 6 from pandas_profiling.controller import pandas_decorator 7 from pandas_profiling.profile_report import ProfileReport 8 from pandas_profiling.version import __version__ File ~\anaconda3\Lib\site-packages\pandas_profiling\controller\pandas_decorator.py:4 1 """This file add the decorator on the DataFrame object.""" 2 from pandas import DataFrame ----> 4 from pandas_profiling.profile_report import ProfileReport 7 def profile_report(df: DataFrame, **kwargs) -> ProfileReport: 8 """Profile a DataFrame. 9 10 Args: (...) 15 A ProfileReport of the DataFrame. 16 """ File ~\anaconda3\Lib\site-packages\pandas_profiling\profile_report.py:13 10 from tqdm.auto import tqdm 11 from visions import VisionsTypeset ---> 13 from pandas_profiling.config import Config, Settings 14 from pandas_profiling.expectations_report import ExpectationsReport 15 from pandas_profiling.model.alerts import AlertType File ~\anaconda3\Lib\site-packages\pandas_profiling\config.py:5 2 from enum import Enum 3 from typing import Any, Dict, List, Optional ----> 5 from pydantic import BaseModel, BaseSettings, Field 8 def _merge_dictionaries(dict1: dict, dict2: dict) -> dict: 9 """ 10 Recursive merge dictionaries. 11 (...) 14 :return: Merged dictionary 15 """ File ~\anaconda3\Lib\site-packages\pydantic\__init__.py:210, in __getattr__(attr_name) 208 dynamic_attr = _dynamic_imports.get(attr_name) 209 if dynamic_attr is None: --> 210 return _getattr_migration(attr_name) 212 from importlib import import_module 214 module = import_module(_dynamic_imports[attr_name], package=__package__) File ~\anaconda3\Lib\site-packages\pydantic\_migration.py:289, in getattr_migration.<locals>.wrapper(name) 287 return import_string(REDIRECT_TO_V1[import_path]) 288 if import_path == 'pydantic:BaseSettings': --> 289 raise PydanticImportError( 290 '`BaseSettings` has been moved to the `pydantic-settings` package. ' 291 f'See https://docs.pydantic.dev/{version_short()}/migration/#basesettings-has-moved-to-pydantic-settings ' 292 'for more details.' 293 ) 294 if import_path in REMOVED_IN_V2: 295 raise PydanticImportError(f'`{import_path}` has been removed in V2.') PydanticImportError: `BaseSettings` has been moved to the `pydantic-settings` package. See https://docs.pydantic.dev/2.3/migration/#basesettings-has-moved-to-pydantic-settings for more details. For further information visit https://errors.pydantic.dev/2.3/u/import-error뭐가 문제일까요ㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠㅠ
-
미해결스프링 시큐리티
다중 필터 구현 시, antMatchers 사용
예제에서는 antMatcher('/admin/**') 이런식으로특정 url 하나만 지정을 했는데혹시 '/user/**' 도 같이 Match 시키고 싶은데antMatcher 는 1개만 처리해주는 것 같습니다.꼭 새로운 FilterChain 을 생성해서 설정해야하나요?