묻고 답해요
141만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결스프링부트 시큐리티 & JWT 강의
안녕하세요! 질문있습니당!
11강 네이버 로그인까지 완료하고 @AuthenticationPrincipal을 이용해서 로그인한 정보를 가져오려고 하는데, 구글로 로그인했을 때는 정보가 출력되는데, 네이버로 로그인했을 때는 null로 나와서 구글링하며 찾아보았는데 해결이 안 되어서…. 질문 남깁니다.
-
미해결
EC2 배포후 구글 로그인 안됨
스프링부트 REST + OAuth2 + JWT를 사용하고 있는 상황인데로컬에서는 잘 돌아갑니다.로컬에서 소셜로그인이 성공하면 OAuth2SuccessHandler에서 바로 JSON으로 반환해주는 형태입니다. 즉, 컨트롤러가 딱히 무슨 역할을 하지 않아도 바로 반환을 해줍니다.하지만 EC2에 배포하고 구글 개발자 센터, yml에 EC2 퍼블릭을 제대로 입력해주고 제대로 일치하는 것을 확인했고버튼을 클릭하면 아이디들이 제대로 뜹니다. 하지만 로그인한 결과 로그인할 아이디를 클릭을 하면Whitelabel Error Page 404페이지가 뜹니다. 그래서 실패했을 때 JSON으로 반환시켜주는 로직을 추가해서 확인한 결과"error 발생 : ": "[authorization_request_not_found] "이러한 오류가 발생했습니다. 항상 체크해야하는요소yml 체크함구글 개발자 센터 확인함→ 1, 2번은 일치함EC2 인스턴스의 보안 그룹이 OAuth2 콜백 URL로 요청을 수신할 수 있도록 올바르게 설정되었는지 확인 → 이거는 어떻게 다른 설정법이 있을까요?application.yml 또는 application.properties에 설정된 값들이 프로덕션 환경에 맞게 정확히 설정이렇게 환경변수를 받아서 ec2 배포시 사용할 수 있도록 설정했는데 추가적으로 또 뭔가를 해줘야 하나요? 혹시 OAuth2 google을 테스터로 해놓고 http로 해놓으면 ec2 배포시에는 사용하지 못하나요?
-
미해결
배포후 소셜로그인 에러
스프링부트 REST + OAuth2 + JWT를 사용하고 있는 상황인데로컬에서는 잘 돌아갑니다.로컬:<a id="google-login" href="/oauth2/authorization/google">구글 로그인</a> <a id="naver-login" href="/oauth2/authorization/naver">네이버 로그인</a> @Service @Log4j2 @RequiredArgsConstructor public class PrincipalOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { private final MemberRepository memberRepository; private final JwtProvider jwtProvider; private final TokenRepository tokenRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // userRequest.getClientRegistration()은 인증 및 인가된 사용자 정보를 가져오는 // Spring Security에서 제공하는 메서드입니다. ClientRegistration clientRegistration = userRequest.getClientRegistration(); log.info("clientRegistration : " + clientRegistration); // 소셜 로그인 accessToken String socialAccessToken = userRequest.getAccessToken().getTokenValue(); log.info("소셜 로그인 accessToken : " + socialAccessToken); OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService(); log.info("oAuth2UserService : " + oAuth2UserService); // 소셜 로그인한 유저정보를 가져온다. OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); log.info("oAuth2User : " + oAuth2User); log.info("getAttribute : " + oAuth2User.getAttributes()); // 회원가입 강제 진행 OAuth2UserInfo oAuth2UserInfo = null; String registrationId = clientRegistration.getRegistrationId(); log.info("registrationId : " + registrationId); if(registrationId.equals("google")) { log.info("구글 로그인"); oAuth2UserInfo = new GoogleUser(oAuth2User, clientRegistration); } else if(registrationId.equals("naver")) { log.info("네이버 로그인"); oAuth2UserInfo = new NaverUser(oAuth2User, clientRegistration); } else { log.error("지원하지 않는 소셜 로그인입니다."); } // 사용자가 로그인한 소셜 서비스를 가지고 옵니다. // 예시) google or naver 같은 값을 가질 수 있다. String provider = oAuth2UserInfo.getProvider(); // 사용자의 소셜 서비스(provider)에서 발급된 고유한 식별자를 가져옵니다. // 이 값은 해당 소셜 서비스에서 유니크한 사용자를 식별하는 용도로 사용됩니다. String providerId = oAuth2UserInfo.getProviderId(); String name = oAuth2UserInfo.getName(); // 사용자의 이메일 주소를 가지고 옵니다. // 소셜 서비스에서 제공하는 이메일 정보를 사용합니다. String email = oAuth2UserInfo.getEmail(); // 소셜 로그인의 경우 무조건 USER 등급으로 고정이다. Role role = Role.USER; MemberEntity findUser = memberRepository.findByEmail(email); if(findUser == null) { log.info("소셜 로그인이 최초입니다."); log.info("소셜 로그인 자동 회원가입을 진행합니다."); findUser = MemberEntity.builder() .email(email) .memberName(name) .provider(provider) .providerId(providerId) .memberRole(role) .nickName(name) .build(); log.info("member : " + findUser); findUser = memberRepository.save(findUser); } else { log.info("로그인을 이미 한적이 있습니다."); } // 권한 가져오기 List<GrantedAuthority> authorities = getAuthoritiesForUser(findUser); // 토큰 생성 TokenDTO tokenForOAuth2 = jwtProvider.createTokenForOAuth2(email, authorities, findUser.getMemberId()); // 기존에 이 토큰이 있는지 확인 TokenEntity findToken = tokenRepository.findByMemberEmail(tokenForOAuth2.getMemberEmail()); TokenEntity saveToken; // 기존의 토큰이 없다면 새로 만들어준다. if(findToken == null) { TokenEntity tokenEntity = TokenEntity.tokenEntity(tokenForOAuth2); saveToken = tokenRepository.save(tokenEntity); log.info("token : " + saveToken); } else { // 기존의 토큰이 있다면 업데이트 해준다. tokenForOAuth2 = TokenDTO.builder() .grantType(tokenForOAuth2.getGrantType()) .accessToken(tokenForOAuth2.getAccessToken()) .accessTokenTime(tokenForOAuth2.getAccessTokenTime()) .refreshToken(tokenForOAuth2.getRefreshToken()) .refreshTokenTime(tokenForOAuth2.getRefreshTokenTime()) .memberEmail(tokenForOAuth2.getMemberEmail()) .memberId(tokenForOAuth2.getMemberId()) .build(); TokenEntity tokenEntity = TokenEntity.updateToken(findToken.getId(), tokenForOAuth2); saveToken = tokenRepository.save(tokenEntity); log.info("token : " + saveToken); } // 토큰이 제대로 되어 있나 검증 if(StringUtils.hasText(saveToken.getAccessToken()) && jwtProvider.validateToken(saveToken.getAccessToken())) { Authentication authenticationToken = jwtProvider.getAuthentication(saveToken.getAccessToken()); log.info("authentication : " + authenticationToken); SecurityContextHolder.getContext().setAuthentication(authenticationToken); UserDetails userDetails = new User(email, "", authorities); log.info("userDetails : " + userDetails); Authentication authenticationUser = new UsernamePasswordAuthenticationToken(userDetails, authorities); log.info("authentication1 : " + authenticationUser); SecurityContextHolder.getContext().setAuthentication(authenticationUser); } else { log.info("검증 실패"); } // attributes가 있는 생성자를 사용하여 PrincipalDetails 객체 생성 // 소셜 로그인인 경우에는 attributes도 함께 가지고 있는 PrincipalDetails 객체를 생성하게 됩니다. PrincipalDetails principalDetails = new PrincipalDetails(findUser, oAuth2User.getAttributes()); log.info("principalDetails : " + principalDetails); return principalDetails; } // 권한 가져오기 로직 private List<GrantedAuthority> getAuthoritiesForUser(MemberEntity findUser) { Role role = findUser.getMemberRole(); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_" + role.name())); log.info("권한 : " + role.name()); return authorities; } } @Log4j2 @RequiredArgsConstructor @Component public class OAuth2SuccessHandler implements AuthenticationSuccessHandler { private final MemberRepository memberRepository; private final TokenRepository tokenRepository; // Jackson ObjectMapper를 주입합니다. private final ObjectMapper objectMapper; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { try { log.info("OAuth2 Login 성공!"); // 소셜 로그인 이메일 가져오기 String email = authentication.getName(); log.info("email : " + email); // 토큰 조회 TokenEntity findToken = tokenRepository.findByMemberEmail(email); log.info("token : " + findToken); // 토큰 DTO 반환 TokenDTO tokenDTO = TokenDTO.toTokenDTO(findToken); // 회원 조회 MemberEntity findUser = memberRepository.findByEmail(email); // 회원 DTO 반환 ResponseMemberDTO memberDTO = ResponseMemberDTO.socialMember(findUser); // 헤더에 담아준다. response.addHeader("email", memberDTO.getEmail()); // 바디에 담아준다. Map<String, Object> responseBody = new HashMap<>(); responseBody.put("providerId", memberDTO.getProviderId()); responseBody.put("provider", memberDTO.getProvider()); responseBody.put("accessToken", tokenDTO.getAccessToken()); responseBody.put("refreshToken", tokenDTO.getRefreshToken()); responseBody.put("email", tokenDTO.getMemberEmail()); responseBody.put("memberId", tokenDTO.getMemberId()); responseBody.put("grantType", tokenDTO.getGrantType()); // JSON 응답 전송 response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); response.getWriter().write(objectMapper.writeValueAsString(responseBody)); } catch (Exception e) { // 예외가 발생하면 클라이언트에게 오류 응답을 반환 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); response.getWriter().write("OAuth 2.0 로그인 성공 후 오류 발생: " + e.getMessage()); response.getWriter().flush(); } } }로컬에서 소셜로그인이 성공하면 OAuth2SuccessHandler에서 바로 JSON으로 반환해주는 형태입니다. 즉, 컨트롤러가 딱히 무슨 역할을 하지 않아도 바로 반환을 해줍니다. 하지만 EC2에 배포하고 구글 개발자 센터, yml에 EC2 퍼블릭을 제대로 입력해주고로그인한 결과 아이디들 제대로 뜨는데 로그인할 아이디를 클릭을 하면Whitelabel Error Page 404페이지가 뜹니다. 로컬 코드를 그대로 배포한건데 왜 안될까요?
-
미해결
배포 후 소셜 로그인
로컬에서는 구글 소셜로그인이 제대로 돌아가고 로그인시 가입, JWT 발급까지 제대로 돌아가는데 프로젝트를 EC2에 배포하고 개발자센터에 승인된 URI에 등록하고 yml에 redirect-uri 똑같이 등록을 했는데 배포시에는 에러가 발생합니다.발생한 에러:구글 아이디들이 나오기는 하는데 클릭을 하면이 에러가 발생합니다. 이게 로컬에서도 안되면 이해가 가는데 로컬에서는 에러없이 잘돌아갑니다. 대체 무슨 문제일까요... ㅠㅠ 급합니다.
-
해결됨NodeFull Stack 개발 가이드
강의 자료
안녕하세요 수업 잘 듣고 있습니다!그런데 노션에 올려주신 수업자료 수강생들에게만 공개하신다고 하셨는데 제가 학습방법까지 들었는데 어떻게 접근하는지를 잘 모르겠어서 질문드립니당!
-
미해결
소셜 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을 검증하는 로직을 구현했는데 이렇게 하는게 맞나요?
-
미해결
소셜로그인 accessToken 검증
security에서 OAuth2 클라이언트를 사용해서 구글이나 네이버 api를 사용하지 않고 자동으로 소셜 로그인 accessToken을 검증해주고 정보를 가져올 수 있다는 기능이 있는걸로 알고있어서 gpt에 검색해보니package com.example.social.config.security; import com.example.social.config.jwt.JwtAccessDeniedHandler; import com.example.social.config.jwt.JwtAuthenticationEntryPoint; import com.example.social.config.jwt.JwtProvider; import com.example.social.config.oauth2.PrincipalOauth2UserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.registration.ClientRegistrations; import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.security.oauth2.core.AuthorizationGrantType; 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.web.SecurityFilterChain; import java.util.HashMap; import java.util.Map; @Configuration @RequiredArgsConstructor @EnableWebSecurity public class SecurityConfig { private final PrincipalOauth2UserService principalOauth2UserService; private final JwtProvider jwtProvider; @Value("${spring.security.oauth2.client.registration.google.client-id}") private String googleClientId; @Value("${spring.security.oauth2.client.registration.google.client-secret}") private String googleClientSecret; @Value("${spring.security.oauth2.client.registration.naver.client-id}") private String naverClientId; @Value("${spring.security.oauth2.client.registration.naver.client-secret}") private String naverClientSecret; @Bean // InMemoryClientRegistrationRepository를 생성하고 반환합니다. // InMemoryClientRegistrationRepository는 OAuth 2.0 클라이언트 등록 정보를 // 메모리에 보관하고 관리하는 데 사용됩니다. public InMemoryClientRegistrationRepository clientRegistrationRepository() { // ClientRegistration 객체를 생성하고 OAuth 2.0 클라이언트의 등록 정보를 설정합니다. ClientRegistration googleRegistration = ClientRegistration .withRegistrationId("google") .clientId(googleClientId) .clientSecret(naverClientSecret) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("http://localhost:8080/login/oauth2/code/google") // Google의 authorizationUri .authorizationUri("https://accounts.google.com/o/oauth2/auth") .tokenUri("https://oauth2.googleapis.com/token") .scope("openid", "profile", "email") .clientName("Google") .build(); ClientRegistration naverRegistration = ClientRegistration .withRegistrationId("naver") .clientId(naverClientId) .clientSecret(naverClientSecret) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri("http://localhost:8080/login/oauth2/code/naver") // Naver의 authorizationUri .authorizationUri("https://nid.naver.com/oauth2.0/authorize") .tokenUri("https://nid.naver.com/oauth2.0/token") .scope("openid", "profile", "email") .clientName("Naver") .build(); return new InMemoryClientRegistrationRepository(googleRegistration, naverRegistration); } @Bean // OAuth2UserService 타입의 빈을 생성합니다. 이 빈은 OAuth2 로그인 처리에 사용됩니다. // OAuth2UserService는 OAuth2 로그인 후에 사용자 정보를 가져오고 처리하는 인터페이스입니다. public OAuth2UserService<OAuth2UserRequest, OAuth2User> googleOAuth2UserOAuth2UserService() { // new DefaultOAuth2UserService() { ... }: // OAuth2UserService 인터페이스를 구현하는 익명 클래스를 생성합니다. // 이 클래스는 OAuth2 로그인 처리에 사용될 사용자 서비스를 정의합니다. return new DefaultOAuth2UserService() { @Override // loadUser(OAuth2UserRequest userRequest) { ... }: // OAuth2UserService 인터페이스의 loadUser 메서드를 오버라이드합니다. // 이 메서드는 OAuth2 로그인 후에 호출되며, 사용자 정보를 가져옵니다. public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // super.loadUser(userRequest)를 호출하여 OAuth2 로그인 후에 사용자 정보를 가져옵니다. // OAuth2User 객체는 사용자의 인증된 속성 및 권한을 포함하고 있습니다. OAuth2User user = super.loadUser(userRequest); // OAuth2 로그인 후에 반환되는 OAuth2User 객체를 수정하거나 구성하여 반환합니다. // 여기에서는 사용자의 권한, 속성 및 고유 식별자(sub)를 포함하는 // DefaultOAuth2User 객체를 반환하고 있습니다. return new DefaultOAuth2User( user.getAuthorities(), user.getAttributes(), // 사용자의 고유 식별자 (일반적으로 'sub'라는 속성에 저장됨) "sub" ); } }; } // naver @Bean @Qualifier("naver") public OAuth2UserService<OAuth2UserRequest, OAuth2User> naverOAuth2UserService() { return new DefaultOAuth2UserService() { @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { OAuth2User user = super.loadUser(userRequest); // 사용자 정보 처리 // user.getName(), user.getAttributes()를 사용하여 필요한 정보를 가져올 수 있음 String uniqueIdentifier = user.getAttribute("id"); return new DefaultOAuth2User( user.getAuthorities(), user.getAttributes(), uniqueIdentifier ); } }; } @Bean public SecurityFilterChain filterChain( HttpSecurity http) throws Exception { http // HTTP 기본 인증 비활성화 .httpBasic().disable() // CSRF(Cross-Site Request Forgery) 공격 방어 비활성화 .csrf().disable() // 폼 기반 로그인 비활성화 .formLogin().disable() // 로그아웃 관련 설정 비활성화 .logout().disable() // 세션 관리를 STATELESS로 설정하여 세션을 사용하지 않도록 설정 // JWT를 사용할거기 때문 .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .authorizeRequests() .antMatchers("/api/v1/users/**").permitAll(); http // JWT를 위한 Filter를 아래에서 만들어 줄건데 // 이 Filter를 어느위치에서 사용하겠다고 등록을 해주어야 Filter가 작동이 됩니다. // JWT를 검증하기 위한 JwtSecurityConfig를 적용하고 // jwtProvider를 사용하여 JWT 검증을 수행합니다. .apply(new JwtSecurityConfig(jwtProvider)); // 에러 방지 http .exceptionHandling() // 인증 에러 핸들링을 위한 커스텀 JwtAuthenticationEntryPoint 등록. .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) // 권한 에러 핸들링을 위한 커스텀 JwtAccessDeniedHandler 등록 .accessDeniedHandler(new JwtAccessDeniedHandler()); // OAuth2 http // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다. .oauth2Login() .clientRegistrationRepository(clientRegistrationRepository()) // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userInfoEndpoint() // OAuth2 로그인 성공 시, 후작업을 진행할 서비스 .userService(principalOauth2UserService); // http // // Spring Security에게 OAuth2 리소스 서버를 설정하도록 지시하는 부분입니다. // // 즉, 이 설정 아래에서 JWT를 검증하는 데 필요한 구성을 수행합니다. // .oauth2ResourceServer() // // OAuth2 리소스 서버가 JWT 토큰을 사용한다고 알려줍니다. // // Spring Security에 JWT 검증을 수행하도록 설정합니다. // .jwt() // // JWT 디코더를 설정합니다. // // JWT 디코더는 JWT 토큰을 검증하고 내용을 추출하는 데 사용됩니다. // .decoder(this.jwtDecoder()); return http.build(); } @Bean PasswordEncoder passwordEncoder() { String idForEncode = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); return new DelegatingPasswordEncoder(idForEncode, encoders); } // JWT 디코더를 생성하는 메서드를 정의합니다. // Google의 공개 키를 가져와 JWT 토큰의 서명을 검증하기 위해 사용합니다. // withJwkSetUri 메서드를 통해 Google의 공개 키를 가져오는 URI를 지정합니다. // 이러한 구성을 통해 Spring Security는 Google OAuth2에서 제공하는 JWT 토큰의 유효성을 검사하고, // 토큰이 유효하면 해당 사용자에 대한 정보를 추출할 수 있습니다. // @Bean // public JwtDecoder jwtDecoder() { // return NimbusJwtDecoder.withJwkSetUri("https://www.googleapis.com/oauth2/v3/certs") // .build(); // } } 이런식으로 나오더군요. 생각하는 로직은 프론트에서 소셜로그인이 성공하면 accessToken을 받고 그거를 바로 서버에 보내줘서 헤더에 담긴 accessToken을 security 기능으로 검증하고 정보를 빼오려고 했습니다. 일반로그인시 제가 만든 JWT를 반환해서 accessToken을 JwtAuthenicationFilter에서 검증한다면 소셜 로그인 accessToken은 security가 지원하는 기능으로 검증하려고 했는데 제가 잘못생각하고 있는건지 소셜 로그인 accessToken이 계속 JwtAuthenicationFilter에서 검사하고 틀린 JWT라고 나오네요.git : https://github.com/YuYoHan/social_login질문 1:제가 생각하고 있는 흐름이 맞나요?질문 2:1번이 맞다면 security 기능으로 사용해서 어떻게 소셜 로그인 accessToken을 검사할 수 있나요?질문 3:1번이 틀렸다면 어느 부분이 틀리고 어떻게 수정해야할까요? 현재 JWT 발급과 JWT를 검증하는 부분은 성공했지만 소셜 로그인 accessToken을 검증해서 정보를 빼와서 JWT를 만들어 반환해주는 부분이 막혀있는 상태입니다 ㅠㅠ 도와주세요
-
미해결
스프링부트 소셜로그인 JWT반환
소셜 로그인 후 프론트가 accessToken을 받고 그거를 서버에 헤더에 담아서 보내주면 서버에서는 accessToken을 검증하고 accessToken에서 소셜로그인한 유저의 정보를 가져올 수 있는 걸로 알고 있는데 계속 실패해서 찾아보니 소셜로그인시 발급해주는 accessToken의 구조가 JWT 구조와 다르다고 기존의 JWT 검증하는 코드는 실패가 뜬다고 나오더군요. 그러면 구글, 네이버의 경우 accessToken을 어떻게 검증해서 정보를 빼와서 JWT를 발급해줄 수 있을 까요? JWT 발급 로직은 구성이 되어 있지만 검증하는 부분이 막혔습니다 ㅠㅠ코드는 다음과 같습니다.package com.example.shoppingmall.config.oauth2; import com.example.shoppingmall.config.auth.PrincipalDetails; import com.example.shoppingmall.config.oauth2.provicer.GoogleUserInfo; import com.example.shoppingmall.config.oauth2.provicer.NaverUserInfo; import com.example.shoppingmall.config.oauth2.provicer.OAuth2UserInfo; import com.example.shoppingmall.dto.member.Role; import com.example.shoppingmall.entity.member.MemberEntity; import com.example.shoppingmall.repository.member.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; import java.util.UUID; // 소셜 로그인하면 사용자 정보를 가지고 온다. // 가져온 정보와 PrincipalDetails 객체를 생성합니다. @Service @Log4j2 @RequiredArgsConstructor public class PrincipalOauth2UserService extends DefaultOAuth2UserService { private final BCryptPasswordEncoder bCryptPasswordEncoder; private final MemberRepository memberRepository; @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // registrationId로 어떤 OAuth로 로그인 했는지 확인가능 log.info("clientRegistration in PrincipalOauth2UserService : " + userRequest.getClientRegistration()); log.info("accessToken in PrincipalOauth2UserService : " + userRequest.getAccessToken().getTokenValue()); OAuth2User oAuth2User = super.loadUser(userRequest); // 구글 로그인 버튼 클릭 →구글 로그인 창 → 로그인 완료 → code 를 리턴(OAuth-Client 라이브러리) → AccessToken 요청 // userRequest 정보 → 회원 프로필 받아야함(loadUser 함수 호출) → 구글로부터 회원 프로필을 받아준다. log.info("getAttributes in PrincipalOauth2UserService : " + oAuth2User.getAttributes()); // 회원가입 강제 진행 OAuth2UserInfo oAuth2UserInfo = null; 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("지원하지 않는 소셜 로그인입니다."); } // 사용자가 로그인한 소셜 서비스(provider)를 가져옵니다. // 예를 들어, "google" 또는 "naver"와 같은 값을 가질 수 있습니다. String provider = oAuth2UserInfo.getProvider(); // 사용자의 소셜 서비스(provider)에서 발급된 고유한 식별자를 가져옵니다. // 이 값은 해당 소셜 서비스에서 유니크한 사용자를 식별하는 용도로 사용됩니다. String providerId = oAuth2UserInfo.getProviderId(); String userName = oAuth2UserInfo.getName(); String password = bCryptPasswordEncoder.encode("get"); // 사용자의 이메일 주소를 가져옵니다. 소셜 서비스에서 제공하는 이메일 정보를 사용합니다. String email = oAuth2UserInfo.getEmail(); // 사용자의 권한 정보를 설정합니다. UserType. // 여기서는 소셜로그인으로 가입하면 무조건 User로 권한을 주는 방식으로 했습니다. Role role = Role.USER; // UUID를 사용하여 랜덤한 문자열 생성 UUID uuid = UUID.randomUUID(); // External User 줄임말 : EU String randomNickName = "EU" + uuid.toString().replace("-", "").substring(0, 9); // 이메일 주소를 사용하여 이미 해당 이메일로 가입된 사용자가 있는지 데이터베이스에서 조회합니다. MemberEntity member = memberRepository.findByUserEmail(email); if (member == null) { log.info("OAuth 로그인이 최초입니다."); log.info("OAuth 자동 회원가입을 진행합니다."); member = MemberEntity.builder() .userName(userName) .userPw(password) .userEmail(email) .role(role) .provider(provider) .nickName(randomNickName) .providerId(providerId) .build(); log.info("member : " + member); MemberEntity save = memberRepository.save(member); log.info("save : " + save); } else { log.info("로그인을 이미 한적이 있습니다. 당신은 자동회원가입이 되어 있습니다."); } // attributes가 있는 생성자를 사용하여 PrincipalDetails 객체 생성 // 소셜 로그인인 경우에는 attributes도 함께 가지고 있는 PrincipalDetails 객체를 생성하게 됩니다. PrincipalDetails principalDetails = new PrincipalDetails(member, oAuth2User.getAttributes()); log.info("principalDetails in PrincipalOauth2UserService : " + principalDetails); return principalDetails; } }package com.example.shoppingmall.config.oauth2.provicer; public interface OAuth2UserInfo { String getProviderId(); String getProvider(); String getEmail(); String getName(); }package com.example.shoppingmall.config.oauth2.provicer; import java.util.Map; public class GoogleUserInfo implements OAuth2UserInfo{ private Map<String, Object> attributes; public GoogleUserInfo(Map<String, Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String) attributes.get("sub"); } @Override public String getProvider() { return "google"; } @Override public String getEmail() { return (String) attributes.get("email"); } @Override public String getName() { return (String) attributes.get("name"); } }package com.example.shoppingmall.config.oauth2.provicer; import java.util.Map; public class NaverUserInfo implements OAuth2UserInfo{ private Map<String, Object> attributes; // PrincipalOauth2UserService에서 new NaverUserInfo((Map)oAuth2User.getAttributes().get("response"))로 // Oauth2 네이버 로그인 정보를 받아온다. public NaverUserInfo(Map<String,Object> attributes) { this.attributes = attributes; } @Override public String getProviderId() { return (String) attributes.get("id"); } @Override public String getProvider() { return "naver"; } @Override public String getEmail() { return (String) attributes.get("email"); } @Override public String getName() { return (String) attributes.get("name"); } }package com.example.shoppingmall.config.security; import com.example.shoppingmall.config.jwt.JwtAccessDeniedHandler; import com.example.shoppingmall.config.jwt.JwtAuthenticationEntryPoint; import com.example.shoppingmall.config.jwt.JwtProvider; import com.example.shoppingmall.config.oauth2.PrincipalOauth2UserService; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.DelegatingPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import java.util.HashMap; import java.util.Map; @Configuration @RequiredArgsConstructor @EnableWebSecurity // @EnableGlobalMethodSecurity 어노테이션은 Spring Security에서 메서드 수준의 보안 설정을 활성화하는데 사용되는 어노테이션입니다. @EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true) public class SecurityConfig { private final JwtProvider jwtProvider; private final PrincipalOauth2UserService principalOauth2UserService; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .httpBasic().disable() .csrf().disable() .formLogin().disable() .logout().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http .authorizeRequests() .antMatchers("/api/v1/boards/**") .access("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/admin/**") .access("hasRole('ROLE_ADMIN')") .antMatchers("/api/v1/items/**") .access("hasRole('ROLE_ADMIN')") // /success-oauth 엔드포인트에 대해 인증된 사용자만 접근 가능하도록 설정 // .antMatchers("/success-oauth").authenticated() .antMatchers("/swagger-resources/**").permitAll() .antMatchers("/swagger-ui/**").permitAll() .antMatchers("/api/v1/users/**").permitAll(); http // JWT Token을 위한 Filter를 아래에서 만들어 줄건데, // 이 Filter를 어느위치에서 사용하겠다고 등록을 해주어야 Filter가 작동이 됩니다. .apply(new JwtSecurityConfig(jwtProvider)); // 에러 방지 http .exceptionHandling() .authenticationEntryPoint(new JwtAuthenticationEntryPoint()) .accessDeniedHandler(new JwtAccessDeniedHandler()); // oauth2 http // oauth2Login() 메서드는 OAuth 2.0 프로토콜을 사용하여 소셜 로그인을 처리하는 기능을 제공합니다. .oauth2Login() // OAuth2 로그인 성공 이후 사용자 정보를 가져올 때 설정 담당 .userInfoEndpoint() // OAuth2 로그인 성공 시, 후작업을 진행할 서비스 .userService(principalOauth2UserService); return http.build(); } @Bean PasswordEncoder passwordEncoder() { String idForEncode = "bcrypt"; Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put(idForEncode, new BCryptPasswordEncoder()); return new DelegatingPasswordEncoder(idForEncode, encoders); } }package com.example.social.config.security; import com.example.social.config.jwt.JwtAuthenticationFilter; import com.example.social.config.jwt.JwtProvider; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { private final JwtProvider jwtProvider; public JwtSecurityConfig(JwtProvider jwtProvider) { this.jwtProvider = jwtProvider; } @Override public void configure(HttpSecurity builder) throws Exception { // JwtAuthenticationFilter가 일반 로그인에 대한 토큰 검증을 처리 JwtAuthenticationFilter customFilter = new JwtAuthenticationFilter(jwtProvider); builder.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class); } }package com.example.social.config.jwt; import com.example.social.domain.TokenDTO; import io.jsonwebtoken.*; import io.jsonwebtoken.security.Keys; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import javax.xml.bind.DatatypeConverter; import java.security.Key; import java.time.LocalDateTime; import java.util.*; import java.util.stream.Collectors; // PrincipalDetails의 정보를 가지고 JWT를 만들어준다. // 이곳에서 JWT를 검증하는 메소드도 존재한다. @Log4j2 @Component public class JwtProvider { private static final String AUTHORITIES_KEY = "auth"; @Value("${jwt.access.expiration}") private long accessTokenTime; @Value("${jwt.refresh.expiration}") private long refreshTokenTime; private Key key; public JwtProvider(@Value("${jwt.secret_key}") String secretKey) { byte[] secretByte = DatatypeConverter.parseBase64Binary(secretKey); this.key = Keys.hmacShaKeyFor(secretByte); } // 유저 정보를 가지고 AccessToken, RefreshToken 을 생성하는 메소드 public TokenDTO crateToken(Authentication authentication, List<GrantedAuthority> authorities) { // UsernamePasswordAuthenticationToken // [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]] // 여기서 Authenticated=false는 아직 정상임 // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정 // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며, // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다. log.info("authentication in JwtProvider : " + authentication); // userType in JwtProvider : ROLE_USER log.info("userType in JwtProvider : " + authorities); // 권한 가져오기 // authentication 객체에서 권한 정보(GrantedAuthority)를 가져와 문자열 형태로 변환한 후, // 권한 정보를 문자열로 변환하여 클레임에 추가하는 방식 Map<String, Object> claims = new HashMap<>(); claims.put(AUTHORITIES_KEY, authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); // 유저의 이메일을 클레임에 넣어줍니다. claims.put("sub", authentication.getName()); log.info("claims in JwtProvider : " + claims); long now = (new Date()).getTime(); Date now2 = new Date(); Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(accessTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); // claims subject 확인 in JwtProvider : zxzz45@naver.com log.info("클레임 확인 : " + checkToken(accessToken)); Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("클레임 확인 : " + checkToken(refreshToken)); return TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .accessTokenTime(accessTokenExpire) .refreshToken(refreshToken) .refreshTokenTime(refreshTokenExpire) // principalDeatails에서 getUserName 메소드가 반환한 것을 담아준다. // 이메일을 반환하도록 구성했으니 이메일이 반환됩니다. .userEmail(authentication.getName()) .build(); } // accessToken 만료시 refreshToken으로 accessToken 발급 public TokenDTO createAccessToken(String userEmail, List<GrantedAuthority> authorities) { long now = (new Date()).getTime(); Date now2 = new Date(); Date accessTokenExpire = new Date(now + this.accessTokenTime); Map<String, Object> claims = new HashMap<>(); claims.put(AUTHORITIES_KEY, authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); claims.put("sub", userEmail); String accessToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(accessTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); return TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .accessTokenTime(accessTokenExpire) .userEmail(userEmail) .build(); } // 소셜 로그인 성공시 JWT 발급 // 위의 코드와 비슷하지만 차이점은 // 위에서는 accessToken만 발급하지만 여기에서는 // accessToken과 refreshToken 모두 발급 public TokenDTO createTokenForOAuth2(String userEmail, List<GrantedAuthority> authorities) { log.info("email in JwtProvicer : " + userEmail); log.info("authorities in JwtProvicer : " + authorities); // 권한 가져오기 Map<String, Object> claims = new HashMap<>(); claims.put(AUTHORITIES_KEY, authorities.stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); claims.put("sub", userEmail); long now = (new Date()).getTime(); Date now2 = new Date(); // accessToken 생성 Date accessTokenExpire = new Date(now + this.accessTokenTime); String accessToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(accessTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("claims subject 확인 in JwtProvider : " + checkToken(accessToken)); Date refreshTokenExpire = new Date(now + this.refreshTokenTime); String refreshToken = Jwts.builder() .setIssuedAt(now2) .setClaims(claims) .setExpiration(refreshTokenExpire) .signWith(key, SignatureAlgorithm.HS256) .compact(); log.info("claims subject 확인 in JwtProvider : " + checkToken(refreshToken)); return TokenDTO.builder() .grantType("Bearer ") .accessToken(accessToken) .refreshToken(refreshToken) .accessTokenTime(accessTokenExpire) .refreshTokenTime(refreshTokenExpire) .userEmail(userEmail) .build(); } // JWT 토큰을 복호화하여 토큰에 들어있는 정보를 꺼내는 코드 // 토큰으로 클레임을 만들고 이를 이용해 유저 객체를 만들어서 최종적으로 authentication 객체를 리턴 // 인증 정보 조회 // 이거는 일반 로그인 시 JWT 발급받은 것을 헤더에 보낼 때 작용한다. // 소셜 로그인은 여기서 작동하지 않는다. 그 이유는 소셜 로그인에서 보내주는 토큰은 // 토큰 형식하고 다릅니다. public Authentication getAuthentication(String token) throws Exception { // 토큰 복호화 메소드 Claims claims = paresClaims(token); log.info("claims : " + claims); if(claims.get("auth") == null) { throw new Exception("권한이 없는 토큰입니다."); } // 클레임 권한 정보 가져오기 List<String> authStrings = (List<String>) claims.get(AUTHORITIES_KEY); // [ROLE_USER] log.info("authorityStrings in JwtProvider : " + authStrings); Collection<? extends GrantedAuthority> authorities = authStrings.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); // [ROLE_USER] log.info("authorities in JwtProvider : " + authorities); // UserDetails 객체를 만들어서 Authentication 리턴 /* UserDetails를 사용하는 이유는 다음과 같습니다: * 1. 인증과 권한 정보 분리: Spring Security에서는 인증(Authentication)과 권한(Authorization)을 분리하는 것이 중요합니다. 즉, 사용자의 인증 정보(아이디, 비밀번호 등)와 사용자의 권한 정보(역할, 권한)을 분리하여 관리하고 처리합니다. UserDetails는 사용자의 인증 정보를 담고 있으며, GrantedAuthority 객체들을 통해 사용자의 권한 정보를 나타낼 수 있습니다. 2. 유연성: Spring Security는 다양한 인증 방식과 인증 제공자(Authentication Provider)를 지원합니다. UserDetails를 사용함으로써 각각의 인증 방식에 따라 사용자 정보를 일반화하여 처리할 수 있습니다. JWT 토큰을 사용하는 경우에도 UserDetails를 활용하면 일반적인 Spring Security의 흐름을 따르며, JWT 토큰에 포함된 사용자 정보와 권한을 UserDetails 객체로 추상화하여 처리할 수 있습니다. * */ // Spring Security의 일반적인 원칙을 따르고, 인증 정보를 효율적이고 안전하게 관리하기 위한 방법 중 하나 // PrincipalDetails에 유저 정보가 있는데 밑의 작업을 하는 이유는 다음과 같다. // Spring Security의 내부 동작 및 일관성 유지를 위해 필요한 작업입니다. // PrincipalDetails 클래스는 UserDetails 인터페이스를 구현하여 사용자의 정보와 권한을 저장하는 역할을 하고 있습니다. // 그리고 UsernamePasswordAuthenticationToken은 Spring Security에서 인증을 나타내는 객체이며, // 인증된 사용자 정보와 해당 사용자의 권한 정보를 포함합니다. // JWT를 사용하여 인증을 처리할 때에는 토큰 검증 과정에서 사용자의 권한 정보를 추출하여 // UsernamePasswordAuthenticationToken을 생성합니다. // 하지만 토큰 검증을 통해 가져온 권한 정보는 SimpleGrantedAuthority 객체의 리스트 형태로 제공됩니다. // 이 때, Spring Security가 기대하는 UserDetails 타입의 객체로 변환하여야 합니다. // 요약하면, 토큰 검증을 통해 가져온 권한 정보를 UserDetails 타입으로 변환하여 // UsernamePasswordAuthenticationToken에 담아서 저장하는 것은 Spring Security의 일관성과 내부 동작을 따르는 방식입니다. UserDetails userDetails = new User(claims.getSubject(), "", authorities); log.info("claims.getSubject() in JwtProvider : " + claims.getSubject()); // 일반 로그인 시 주로 이거로 인증처리해서 SecurityContext에 저장한다. // Spring Security에서 인증을 나타내는 객체로 사용됩니다. // 일반적인 경우, 사용자 이름과 비밀번호를 사용하여 인증을 처리하고 해당 사용자의 권한 정보를 포함 // 원래는 사용자 이름과 비밀번호를 사용하여 인증을 처리하고, 해당 사용자의 권한 정보를 포함합니다. // 일반적으로 이 객체를 사용하여 사용자가 입력한 인증 정보를 처리하고, // 인증이 성공하면 해당 사용자의 권한을 포함한 Authentication 객체가 생성되어 SecurityContext에 저장됩니다. // 그러나 JWT를 사용하는 경우에는 비밀번호 대신에 JWT 토큰을 사용하여 인증을 처리합니다. return new UsernamePasswordAuthenticationToken(userDetails, token, authorities); } private Claims paresClaims(String token) { try { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { log.info("ExpiredJwtException : " + e.getMessage()); log.info("ExpiredJwtException : " + e.getClaims()); return e.getClaims(); } } // 토큰 검증하기 위한 메소드 public boolean validateToken(String token) { try { Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); return true; } catch (SecurityException | MalformedJwtException e) { log.error("잘못된 JWT 설명입니다. \n info : " + e.getMessage()); } catch (ExpiredJwtException e) { log.error("만료된 JWT입니다. \n info : " + e.getMessage()); } catch (UnsupportedJwtException e) { log.error("지원되지 않는 JWT입니다. \n info : " + e.getMessage()); } catch (IllegalArgumentException e) { log.error("JWT가 잘못되었습니다. \n info : " + e.getMessage()); } return false; } // 클레임에 제대로 등록되어 있나 확인하기 위해서 메소드를 만듬 private String checkToken(String token) { Claims claims = Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); String subject = claims.getSubject(); return subject; } }package com.example.social.config.jwt; // 여기는 토큰이 header에 담겨서 오면 검증을 하는 곳이다. // 클라이언트 요청 시 JWT 인증을 하기 위해 설치하는 커스텀 필터로 // UsernamePasswordAuthenticationFiler 이전에 실행된다. // 이전에 실행된다는 뜻은 JwtAuthenticationFilter 를 통과하면 // UsernamePasswordAuthenticationFilter 이후의 필터는 통과한 것으로 본다는 뜻이다. // 쉽게 말해서, Username + Password 를 통한 인증을 Jwt 를 통해 수행한다는 것이다. import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; 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; import java.util.HashMap; import java.util.Map; // JWT 방식은 세션과 다르게 Filter 하나를 추가해야 합니다. // 이제 사용자가 로그인을 했을 때, Request 에 가지고 있는 Token 을 해석해주는 로직이 필요합니다. // 이 역할을 해주는것이 JwtAuthenticationFilter입니다. // 세부 비즈니스 로직들은 TokenProvider에 적어둡니다. 일종의 service 클래스라고 생각하면 편합니다. /* * 순서 * 1. 사용자의 Request Header에 토큰을 가져옵니다. * 2. 해당 토큰의 유효성 검사를 실시하고 유효하면 * 3. Authentication 인증 객체를 만들고 * 4. ContextHolder에 저장해줍니다. * 5. 해당 Filter 과정이 끝나면 이제 시큐리티에 다음 Filter로 이동하게 됩니다. * * 이렇게 거치고 나면 컨트롤러에서 정보를 가져올 수 있습니다. * */ @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)) { // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장 Authentication authentication = null; try { authentication = jwtProvider.getAuthentication(jwt); } catch (Exception e) { throw new RuntimeException(e); } 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); } else { log.error("유효한 JWT가 없습니다. : " + requestURI); } 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; } } }package com.example.social.domain; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import java.util.Date; @Getter @ToString @NoArgsConstructor public class TokenDTO { private Long id; private String grantType; private String accessToken; private Date accessTokenTime; private String refreshToken; private Date refreshTokenTime; private String userEmail; @Builder public TokenDTO(Long id, String grantType, String accessToken, Date accessTokenTime, String refreshToken, Date refreshTokenTime, String userEmail) { this.id = id; this.grantType = grantType; this.accessToken = accessToken; this.accessTokenTime = accessTokenTime; this.refreshToken = refreshToken; this.refreshTokenTime = refreshTokenTime; this.userEmail = userEmail; } }package com.example.social.entity; import com.example.social.domain.TokenDTO; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import javax.persistence.*; import java.util.Date; @Entity(name = "token") @Getter @Table @NoArgsConstructor @ToString public class TokenEntity { @Id @GeneratedValue @Column(name = "token_id") private Long id; private String grantType; private String accessToken; private Date accessTokenTime; private String refreshToken; private Date refreshTokenTime; private String userEmail; @Builder public TokenEntity(Long id, String grantType, String accessToken, Date accessTokenTime, String refreshToken, Date refreshTokenTime, String userEmail) { this.id = id; this.grantType = grantType; this.accessToken = accessToken; this.accessTokenTime = accessTokenTime; this.refreshToken = refreshToken; this.refreshTokenTime = refreshTokenTime; this.userEmail = userEmail; } public static TokenEntity toTokenEntity(TokenDTO token) { return TokenEntity.builder() .id(token.getId()) .grantType(token.getGrantType()) .accessToken(token.getAccessToken()) .accessTokenTime(token.getAccessTokenTime()) .refreshToken(token.getRefreshToken()) .refreshTokenTime(token.getRefreshTokenTime()) .userEmail(token.getUserEmail()) .build(); } }package com.example.social.repository; import com.example.social.entity.TokenEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface TokenRepository extends JpaRepository<TokenEntity, Long> { TokenEntity findByRefreshToken(String refreshToken); TokenEntity findByUserEmail(String userEmail); }로그인의 경우 JWT 발급이 제대로 되는것을 확인 // 로그인 @PostMapping("/api/v1/users/login") public ResponseEntity<?> login(@RequestBody MemberDTO member) throws Exception { log.info("member : " + member); try { String userEmail = member.getUserEmail(); String userPw = member.getUserPw(); return memberService.login(userEmail, userPw); } catch (EntityNotFoundException e) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage()); } } // 로그인 public ResponseEntity<?> login(String userEmail, String userPw) { MemberEntity findUser = memberRepository.findByUserEmail(userEmail); log.info("findUser : " + findUser); if(findUser != null) { // 사용자가 입력한 패스워드를 암호화하여 사용자 정보와 비교 if(passwordEncoder.matches(userPw, findUser.getUserPw())) { // UsernamePasswordAuthenticationToken은 Spring Security에서 // 사용자의 이메일과 비밀번호를 이용하여 인증을 진행하기 위해 제공되는 클래스 // 이후에는 생성된 authentication 객체를 AuthenticationManager를 이용하여 인증을 진행합니다. // AuthenticationManager는 인증을 담당하는 Spring Security 의 중요한 인터페이스로, 실제로 사용자의 인증 과정을 처리합니다. // AuthenticationManager를 사용하여 사용자가 입력한 이메일과 비밀번호가 올바른지 검증하고, // 인증에 성공하면 해당 사용자에 대한 Authentication 객체를 반환합니다. 인증에 실패하면 예외를 발생시킵니다. // 인증은 토큰을 서버로 전달하고, 서버에서 해당 토큰을 검증하여 사용자를 인증하는 단계에서 이루어집니다. // 즉, Authentication 객체를 생성하고, 해당 객체를 SecurityContext에 저장하게 되면, // 인증이 완료되지 않은 상태에서 사용자 정보를 가지는 인증 객체가 저장됩니다. // 이후 검증 시에는 해당 인증 객체를 기반으로 다시 UsernamePasswordAuthenticationToken을 생성하여 // 인증 상태를 true로 설정하는 것이 가능합니다. Authentication authentication = new UsernamePasswordAuthenticationToken(userEmail, userPw); // UsernamePasswordAuthenticationToken // [Principal=zxzz45@naver.com, Credentials=[PROTECTED], Authenticated=false, Details=null, Granted Authorities=[]] // 여기서 Authenticated=false는 아직 정상임 // 이 시점에서는 아직 실제로 인증이 이루어지지 않았기 때문에 Authenticated 속성은 false로 설정 // 인증 과정은 AuthenticationManager와 AuthenticationProvider에서 이루어지며, // 인증이 성공하면 Authentication 객체의 isAuthenticated() 속성이 true로 변경됩니다. log.info("authentication in MemberService : " + authentication); List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(findUser); TokenDTO tokenDTO = jwtProvider.crateToken(authentication, authoritiesForUser); TokenEntity findToken = tokenRepository.findByUserEmail(tokenDTO.getUserEmail()); // 사용자에게 이미 토큰이 할당되어 있는지 확인 if(findToken != null) { log.info("이미 토큰이 발급되어 있습니다."); // 기존의 토큰을 업데이트 합니다. tokenDTO = TokenDTO.builder() .id(findToken.getId()) .grantType(tokenDTO.getGrantType()) .accessToken(tokenDTO.getAccessToken()) .accessTokenTime(tokenDTO.getAccessTokenTime()) .refreshToken(tokenDTO.getRefreshToken()) .refreshTokenTime(tokenDTO.getRefreshTokenTime()) .build(); TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenDTO); tokenRepository.save(tokenEntity); } else { log.info("발급한 토큰이 없습니다."); tokenDTO = TokenDTO.builder() .grantType(tokenDTO.getGrantType()) .accessToken(tokenDTO.getAccessToken()) .accessTokenTime(tokenDTO.getAccessTokenTime()) .refreshToken(tokenDTO.getRefreshToken()) .refreshTokenTime(tokenDTO.getRefreshTokenTime()) .build(); TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenDTO); tokenRepository.save(tokenEntity); } HttpHeaders headers = new HttpHeaders(); headers.add(JwtAuthenticationFilter.HEADER_AUTHORIZATION, "Bearer " + tokenDTO); return new ResponseEntity<>(tokenDTO, headers, HttpStatus.OK); } } else { return ResponseEntity.notFound().build(); } return ResponseEntity.notFound().build(); }문제는 소셜로그인 시 JWT발급이 문제입니다. // 소셜 로그인 // 소셜 로그인시 발급받은 accessToken에서 정보를 가져올 때는 // @AuthenticationPrincipal OAuth2User oAuth2User이거를 사용한다. @GetMapping("/success-oauth") public ResponseEntity<?> getOAuth2UserInfo(@AuthenticationPrincipal OAuth2User oAuth2User) throws Exception{ try { String email = oAuth2User.getAttribute("email"); log.info("email : " + email); ResponseEntity<?> tokenForOAuth2 = memberService.createTokenForOAuth2(email); return ResponseEntity.ok().body(tokenForOAuth2); } catch (Exception e) { throw new RuntimeException(e); } }여기서 소셜로그인시 받은 accessToken 검증이 실패해서 정보가 null이 뜹니다.
-
미해결
스프링부트 EC2 배포 시 소셜로그인, JWT, S3 처리
스프링부트를 EC2에 배포할 때 OAuth2, JWT나 S3를 이미지 넣는 설정 파일(yml)을 보안상 git에 안올라가게 막아주는데 그러면 배포할 때는 어떻게 처리를 해줘야 배포상태에서 OAuth2, JWT나 S3를 이미지 넣는 기능을 사용할 수 있나요??
-
미해결스프링부트 시큐리티 & JWT 강의
[Google oauth2 관련] The dependencies of some of the beans in the application context form a cycle
에러 메시지는 아래와 같습니다.*************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: ┌─────┐ | oauth2UserService defined in file [D:\GooGoo\out\production\classes\eunhye\GooGoo\config\oauth\Oauth2UserService.class] ↑ ↓ | securityConfig defined in file [D:\GooGoo\out\production\classes\eunhye\GooGoo\config\security\SecurityConfig.class] └─────┘ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true. Process finished with exit code 1 oauth2UserService와 securityConfig가 cycle을 이루고 있다.뭔가 구조가 잘못됐다는 것을 알 수 있었습니다만...전 강의 잘 듣고 잘 코드 쳤다고 생각이 되거든요ㅠ스스로를 의심하며 아무리 코드를 쳐다봐도 잘못된 점을 모르겠어서 질문 올립니다.다음은 관련된 코드들입니다. Oauth2UserService.javapackage eunhye.GooGoo.config.oauth; import eunhye.GooGoo.config.security.SecurityDetails; import eunhye.GooGoo.dto.UserDTO; import eunhye.GooGoo.entity.UserEntity; import eunhye.GooGoo.entity.UserRole; import eunhye.GooGoo.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; 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; @Service @RequiredArgsConstructor public class Oauth2UserService extends DefaultOAuth2UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; // 구글로부터 받은 userRequest 데이터에 대해 후처리하는 함수 @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException{ OAuth2User oauth2User = super.loadUser(userRequest); // 회원가입 강제 진행 String provider = userRequest.getClientRegistration().getClientId(); String providerId = oauth2User.getAttribute("sub"); String userNickname = provider+"_"+providerId; String userEmail = oauth2User.getAttribute("email"); String userPassword = passwordEncoder.encode("겟인데어"); UserRole authority = UserRole.USER; UserEntity userEntity = userRepository.findByUserEmail(userEmail); if(userEntity == null){ userEntity = UserEntity.builder() .userNickname(userNickname) .userEmail(userEmail) .userPassword(userPassword) .authority(authority) .provider(provider) .providerId(providerId) .build(); userRepository.save(userEntity); } return new SecurityDetails(userEntity, oauth2User.getAttributes()); } } SecurityConfig.javapackage eunhye.GooGoo.config.security; import eunhye.GooGoo.config.oauth.Oauth2UserService; import lombok.RequiredArgsConstructor; 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.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig{ private final AuthenticationFailureHandler customFailureHandler; private final Oauth2UserService oauth2UserService; @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/user/**").authenticated() .antMatchers("/admin/**").access("hasRole('ADMIN')") .anyRequest().permitAll() .and() .formLogin() .loginPage("/login") .loginProcessingUrl("/login") .defaultSuccessUrl("/home") .usernameParameter("userEmail").passwordParameter("userPassword") .failureHandler(customFailureHandler) .and() .logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessUrl("/login") .and().oauth2Login() .loginPage("/login").defaultSuccessUrl("/home") .userInfoEndpoint().userService(oauth2UserService); return http.build(); } } SecurityDetails.javapackage eunhye.GooGoo.config.security; import eunhye.GooGoo.entity.UserEntity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.ArrayList; import java.util.Collection; import java.util.Map; @Data public class SecurityDetails implements UserDetails, OAuth2User { // 일반 로그인 private final UserEntity userEntity; // OAuth 로그인 (Google) private Map<String, Object> attributes; public SecurityDetails(UserEntity userEntity){ this.userEntity = userEntity; } public SecurityDetails(UserEntity userEntity, Map<String, Object> attributes){ this.userEntity = userEntity; this.attributes = attributes; } @Override public Map<String, Object> getAttributes() { return attributes; } @Override public String getName() { return userEntity.getId()+""; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Collection<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new GrantedAuthority() { @Override public String getAuthority() { return userEntity.getAuthority().toString(); } }); return authorities; } @Override public String getPassword() { return userEntity.getUserPassword(); } @Override public String getUsername() { return userEntity.getUserEmail(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } 추가적인 코드가 필요하다면 아래 깃헙 링크 참고해주세요https://github.com/you-eun-hye/GooGoo
-
미해결스프링부트 시큐리티 & JWT 강의
JWT + Oauth2 붙이기
안녕하세요. 강사님! 수업 정말 잘 들었습니다! 비전공자로써 3개월째인데 JWT 강의 중에 정말 이해가 잘 되었습니다!!! 진심으로 감사합니다~~다름이 아니라 더 나아가서, JWT와 Oauth2를 붙이려고 하는데 JWT 강의 초반에 기존의 Oauth2 방식을 사용하지 않고 다른 방식을 써야 된다고 하셔서 질문합니다.어떤 부분이 달라져야할지 전혀 감이 안 오는데, 제가 조금 참고할만한 자료가 있을까요? 혹시 JWT+Oauth를 붙이려고 할 때 코드가 많이 변경될까요?(혹시 강의 제작하실 생각 없으신가요...ㅎㅎ)
-
해결됨스프링부트 시큐리티 & JWT 강의
google oauth2 로그인 과정에서...
안녕하세요. 강의 잘 보고있습니다. 좋은 강의 만들어주셔서 감사합니다. google oauth2 로그인 수업 중, 질문이 있습니다. 다음과 같이 구글 로그인 버튼을 클릭하면 계정 선택을 한 후 로그인을 해야하는데, 최초 로그인 이후로는 계정 선택 없이 초기 로그인한 계정으로 자동 로그인이 됩니다. 서버를 껐다 켜도 초기 로그인 토큰으로 자동 로그인이 되는 것을 보면 로그 아웃 시 토큰을 삭제하거나 해야하는데 교수님 강의에는 그러한 코드 없이 초기 로그인 여부와 상관없이 자동으로 계정 선택 화면으로 넘어가는데 어떤 문제인지 솔루션을 주실 수 있으실까요..?ㅠㅠ
-
미해결스프링 시큐리티
Thymeleaf + spring security + JWT 페이지 이동시 인증 구현방법 문의드립니다
안녕하세요 질문을 둘러보았는데 저랑 딱맞는 고민은 없는거 같아서 질문드립니다. 프론트/백서버를 나누지 않고 한 서버로 구현할 경우 JWT를 페이지 이동시 매번 어떻게 헤더로 보내줄까요?? fetch api나 ajax를 통한 api 호출시 헤더 보내는것은 문제없지만 아예 다른 매핑으로 페이지를 이동할 경우 인증이 필요한 페이지에 접근하려면 header에 bearer token이 필요한데 도대체 어떤 방법으로 구현할지 모르겠습니다. 프론트서버를 나눈경우 이게 어려운게 아닌일 같은데 리다이렉션을 헤더요청과 함께하려는 꼴이 되서 안되는거 같네요,.. 그래서 일단은 cookie에 토큰값을 넣어주고 쿠키값에서 찾아오도록하고 있습니다..... 쿠키는 헤더에 자동으로 계속 추가되니깐요.. 그냥 이렇게 해도 상관없을까요 쿠키에 토큰 자체를 넣어버려도 괜찮을지요 프론트와 백이 분리 되지 않고 spring security로 설계한 서비스의 경우 jwt인증을 매 페이지 마다 어떻게 수행할지가 고민입니다. 감사합니다.