묻고 답해요
131만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결스프링 시큐리티 OAuth2
커스텀 인증 필터 만들때 질문이 있습니다.
커스텀 Authentication Filter를 만들때 어떤 코드는 AbstractAuthenticationProcessingFilter를 상속 받거나 강사님은 UsernamePasswordAuthenticationFilter를 상속 받는데요 혹시 무엇을 상속 받을지 구분하는게 있을까요? 어떤 필터를 상속 받는것을 추천한다던지
-
미해결스프링 시큐리티 OAuth2
소셜로그인과 JWT 인증 방식에 대해 궁금한점이 있습니다.
일반 로그인 + 소셜 로그인 전체로 JWT 인증 방식으로 도입하고자 할때소셜 로그인 인증 성공 후에 JWT 인증 객체를 발급해서 그 토큰으로 프론트단과 통신하면 되는건가요?? 그리고 JWT로 인증하는 과정이 현 강의와 밑에 나중 강의들에 나오는지 궁금합니다. 단지 인가서버에서 내려준 엑세스 토큰을 리소스 서버에서 JWT 토큰을 검증하기 위한 것인지. 아니면 일반 로그인에 대해서도 JWT 인증을 도입할시에 도움이 되는지 궁금합니다.
-
미해결스프링 시큐리티 OAuth2
authenticationEntryPoint 를 기본 설정된 /login이 아닌 react 웹 페이지로 설정 시 cors 문제가 지속해서 발생합니다.
authenticastion Entry Point로 지정 시 cors문제가 계속해서 진행되어 Authorization Code를 받아올 수 없습니다.코드 첨부Authorization Server Config@Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http .getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults() ); http .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("http://localhost:3000") // new LoginUrlAuthenticationEntryPoint("/login") )) http.oauth2ResourceServer((resourceServer) -> resourceServer .jwt(Customizer.withDefaults())); return http.build(); } @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("oidc-client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .redirectUri("https://oidcdebugger.com/debug") .redirectUri("https://test-b821b.web.app") .postLogoutRedirectUri("http://127.0.0.1:8080/") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(oidcClient); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().issuer("http://localhost:6080").build(); } } Default Web Config @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .exposedHeaders("*"); } }; } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowCredentials(true); configuration.addAllowedHeader("*"); configuration.addAllowedMethod("*"); configuration.addAllowedOrigin("http://localhost:3000"); configuration.setExposedHeaders(Arrays.asList("*")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .cors(customCorsConfig -> customCorsConfig.configurationSource(corsConfigurationSource())) .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(authorizeHttpRequestsCustomizer -> authorizeHttpRequestsCustomizer.requestMatchers(AUTH_WHITELIST).permitAll() .anyRequest().authenticated() ) .formLogin( httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer.loginPage("http://localhost:3000").loginProcessingUrl("/login") // Customizer.withDefaults() ) ; return http.build(); } @Bean public UserDetailsService userDetailsService() { PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); UserDetails userDetails = User.builder() .username("user") .password(passwordEncoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public PasswordEncoder encoder() { return new BCryptPasswordEncoder(); } 위 코드로 진행했을 시 발생하는 Cors Error입니다 Access to XMLHttpRequest at 'http://localhost:6080/oauth2/authorize?client_id=oidc-client&redirect_uri=https%3A%2F%2Foidcdebugger.com%2Fdebug&scope=openid&response_type=code&response_mode=form_post&state=3it6dl9kewr&nonce=hyq3pnronj7&continue' (redirected from 'http://localhost:6080/login') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.3. 이로 인해 CorsFilter를 적용하게 되면 해당 Cors는 해결되지만 authorization code 가 Redirect되는 시점에 Cors 에러가 새롭게 나옵니다.CorsFilter @Component @Slf4j @Order(Ordered.HIGHEST_PRECEDENCE) public class CorsFilter implements Filter { private static final Set<String> allowedOrigins = new HashSet<String>(); static { allowedOrigins.add( "http://localhost:3000" ); } @Override public void init(FilterConfig fc) throws ServletException { } @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response = (HttpServletResponse) res; HttpServletRequest request = (HttpServletRequest) req; String origin = request.getHeader(HttpHeaders.ORIGIN); log.info("Origin: {}", origin); if (origin != null) { Optional<String> first = allowedOrigins.stream().filter(origin::startsWith).findFirst(); first.ifPresent(s -> response.setHeader("Access-Control-Allow-Origin", s)); } response.setHeader("Access-Control-Allow-Credentials", "true"); response.setHeader("Access-Control-Allow-Methods", "*"); response.setHeader("Access-Control-Max-Age", "3600"); response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Key, Authorization"); if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { response.setStatus(HttpServletResponse.SC_OK); } else { chain.doFilter(req, res); } } @Override public void destroy() { } Cors 오류Access to XMLHttpRequest at 'https://oidcdebugger.com/debug?code=1o_jSQQVu6EB4XDq-xCWP3qrzp2VDdGbH_AN7iBJGU5gQRC7BRFkQYdn2A6P-6G5Aoi8XLJLaVR4MSVktNzDvYLMmj_UXahcWfEwQdA-tk4GCw5Bff4mXn2q6mIYfs_d&state=3it6dl9kewr' (redirected from 'http://localhost:6080/login') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.혹시 외부 로그인 페이지와 연동한 예시가 있거나, Cors 문제를 해결할 방안이 있는지 궁금합니다ㅠㅠ
-
미해결스프링 시큐리티 OAuth2
spring boot 3.1 security entryPoint 관련 질문입니다
스프링부트 2.7.7 버전에서는 사용할때 아무 문제없던 코드들인데 3.1 버전 업데이트하면서 securityFilterChain 이부분 문법만 살짝 바뀐정도입니다 그런데jwt 토큰에서 chain.doFilter(request, response); 까지 잘 넘어가고비즈니스 로직인 service 단에서 에러가 터졌을때 (NullPointException,IllegalArgumentException)에러가 났을경우다시 엔트리포인트로 넘어와서 에러처리가 되는데 왜 이럴까요 ?? 무슨 에러가 나든엔트리포인트로 넘어오는데 엔트리포인트는 인증 실패일경우에 실행되어야하는걸로 알고있습니다토큰값은 모두 유효하고 jwt 토큰 인증은 되었음에도 불구하고 엔트리포인트로 넘어가는 이유를 모르겠습니다아래 에러는 비즈니스로직에서 에러 난 코드이며 2023-10-30T23:56:16.287+09:00 ERROR 26006 --- [nio-8081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: No enum constant com.tripcoach.core.domain.alarm.enums.AlarmType.COACH] with root causejava.lang.IllegalArgumentException: No enum constant com.tripcoach.core.domain.alarm.enums.AlarmType.COACHat java.base/java.lang.Enum.valueOf(Enum.java:273) ~[na:na]at org.hibernate.type.descriptor.java.EnumJavaType.fromName(EnumJavaType.java:231) ~[hibernate-core-6.2.9.Final.jar:6.2.9.Final]at org.hibernate.type.descriptor.converter.internal.NamedEnumValueConverter.toDomainValue(NamedEnumValueConverter.java:53) ~[hibernate-core-6.2.9.Final.jar:6.2.9.Final] 바로 에러코드 다음이 2023-10-30T23:56:16.291+09:00 ERROR 26006 --- [nio-8081-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] threw exceptionjava.lang.NullPointerException: Cannot invoke "Object.getClass()" because "exception" is nullat org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.resolveMethodByThrowable(ExceptionHandlerMethodResolver.java:146) ~[spring-web-6.0.12.jar:6.0.12]at org.springframework.web.method.annotation.ExceptionHandlerMethodResolver.resolveMethod(ExceptionHandlerMethodResolver.java:134) ~[spring-web-6.0.12.jar:6.0.12] 엔트리포인트에서 넘어와버리네요부족한 코드이지만 아무리 찾아도 모르겠어서 글 남깁니다. 아래 전체코드 첨부합니다
-
미해결스프링 시큐리티 OAuth2
Naver Login시 권한에 대해 질문이 있습니다
CustomAuthorityMapper 클래스에서강사님과 코드는 동일하나authorities가 넘어올시에 위와 같이 넘어옵니다.그래서 강사님 영상과 다르게ROLE_USER만이 authority에 대해서 담기게 되고위의 사진은 hasRole 권한을 해제하여 찍어본것입니다. /api/user에 진입이 불가합니다 코드는 동일한데 뭐가 문제일까요,,,? 강사님의 네이버 로그인시는 response를 안벗기고도 진입이 가능해서 질문드립니다 package io.security.oauth2.sociallogin; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; import java.util.Collection; import java.util.HashSet; import java.util.Set; public class CustomAuthorityMapper implements GrantedAuthoritiesMapper { private String prefix = "ROLE_"; @Override public Set<GrantedAuthority> mapAuthorities(Collection<? extends GrantedAuthority> authorities) { HashSet<GrantedAuthority> mapped = new HashSet<>(authorities.size()); for (GrantedAuthority authority : authorities) { mapped.add(mapAuthority(authority.getAuthority())); } return mapped; } private GrantedAuthority mapAuthority(String name) { if(name.lastIndexOf(".") > 0){ int index = name.lastIndexOf("."); name = "SCOPE_" + name.substring(index+1); } if (!this.prefix.isEmpty() && !name.startsWith(this.prefix)) { name = this.prefix + name; } return new SimpleGrantedAuthority(name); } }
-
미해결스프링 시큐리티 OAuth2
Session id가 인가 코드와 access token을 교환하는 도중에 변경됩니다.
안녕하세요. 강의 잘 듣고 있습니다.제목에서처럼, session id가 access token을 교환하는 일련의 과정 중에 변경되어 다음과 같은 오류가 브라우저 상에서 보여집니다.해당 에러 메시지를 내뱉는 로직이 OAuth2LoginAuthenticationFilter 클래스의 attemptAuthentication 메소드에 있음을 확인하였습니다. 제가 찾은 원인은 OAuth2AuthorizationRequestRedirectFilter에서 AuthorizationRequest의 저장까지는 성공적인데, OAuth2LoginAuthenticationFilter에서 AuthorizationRequest를 Repository에서 꺼내오려고 시도할 때 바뀐 session id값 때문에 이전의 AuthorizationRequest 객체를 가져오지 못하고 null을 반환하는 것이었습니다. 이렇게 session id가 바뀌는 원인이 무엇인지 알려주시면 감사하겠습니다 ㅠㅠ
-
미해결스프링부트 시큐리티 & JWT 강의
Facebook 로그인 버튼만든 후 error(Sorry...) 발생시
인증 및 계정 만들기 > 수정 에서 Email 권한 추가해주어야 오류 안나네요.
-
미해결스프링 시큐리티 OAuth2
AuthorizedClient를 Service를 통해서 가져올시
위에 사진으로 authorizedClient가 nullpoint 에러가 발생합니다. repository시에는 authorizedClient는 잘 가져오는데 service로 load시에 못 가져오는거 같습니다. 왜 그런건지 궁금합니다
-
미해결스프링 시큐리티 OAuth2
임시 코드 요청시 Redirect에 대해 질문 드립니다
client registration의 redirect uri는 인가 서버로부터 임시코드를 발급받고, redirect uri로 임시 코드를 리다이렉트 시키는데,강사님께서redirect uri를 http://localhost:8081/client 로 설정하였고,ClientController의 매핑주소도 위와 같은 주소로 설정하였습니다. 여기서 궁금한점이 있습니다. 임시 코드를 발급한 시점에 저 리다이렉트 주소로 콜백하게 되는데, 이 시점에 clientcontroller에 저 매핑주소가 호출되어야 할텐데왜 token 발급시에 컨트롤러가 호출되는지그리고 redirect uri가 token 발급시 콜백 주소도 해당되는건가요?? 엑세스 토큰 발급후에도 http://localhost:8081/client여기로 호출되어서 의문이 듭니다.
-
미해결스프링 시큐리티 OAuth2
keycloak 연동 (클라이언트 앱 시작하기 - application.yml)
아래 사진과 같이 yml 파일을 작성했는데, keycloak 로그인 폼이 적용이 안되고 기존 로그인 폼이 나옵니다.(프로그램 실행 시 기존처럼 비밀번호가 출력되고 해당 비밀번호로 id pw 입력하여 로그인하면 index 화면이 나오는 상황입니다.)keycloak과 연동 문제인 듯 한데, keycloak 인가서버 도메인과 관련되어있는 것 일까요?혹은, keycloak 연동을 위해 build.gradle 같은 곳에 추가 설정해 줘야 하는 부분이 있을 까요? 참고로 build.gradle은 아래 사진과 같고,종속성 'org.springframework.boot:spring-boot-starter-oauth2-client' 추가해 준 상태 입니다.
-
미해결스프링 시큐리티 OAuth2
JWT 자체 검증에 대해서 질문드립니다
강의 막바지에 인가서버에서는 개인키로 서명하고, 자원 서버에서는 공개키로 검증한다고 하셨는데, 말그대로 공개키는 노출되어있는 키여서, 누구나 검증할 수 있어서 보안적인 측면에서 취약한거 아닌가요?? 공개키로 서명하고 -> 리소스 서버에서 개인키로 검증해야하는게 맞지 않나 싶어서 질문드립니다
-
미해결스프링 시큐리티 OAuth2
30:55초 경에 요청이 한번 더 오는거 같던데
요청을 한번 보냈는데, 다시 한번 요청이 와서, filterchainproxy에 요청 위임하는 과정이다시 나오는데 어떤 이유에서 그런건지 궁금합니다
-
미해결스프링 시큐리티 OAuth2
진도에 관해서 질문있습니다
먼저 저번에 스프링 시큐리티 1편 강의를 듣고 전체적인 인증흐름 등 소개가 만족스러워서 이번에 oauth2 도 샀습니다. 단도직입적으로 얘기하면(OAuth2의 인증흐름+RESTAPI 식으로 OAuth2)를 어떤 식으로 코드를 짜는지 보고 싶습니다. 이에 대해 도움이 되는 강의 chapter를 먼저 수강하고 싶습니다. 학기 중이라 44시간을 듣기 좀 부담스럽기도 하고 한번 oauth2의 인증흐름+restAPI 식 oauth2를 먼저 구현하고 난 후에 다시 강의를 들을때 꼼꼼히 들을 예정입니다그래서 질문은1)(OAuth2의 인증흐름+RESTAPI 식으로 OAuth2)에 관련된 chapter를 소개해 주실수 있을까요??(예를 들면 MAC 등 이런 것은 제외하고요..)2)이거 쓰다가 궁금해지는게 소셜로그인-Form Login 을 수강하면 RESTAPI 식으로 바꾸는 것은 별차이가 없을까요?
-
미해결스프링 시큐리티 OAuth2
소셜로그인 정보가져오기
강사님 수업을 보면 정보를 다 가지고 오는 것을 볼 수 있는데 package com.example.oauth.config.OAuth2; import org.springframework.security.core.authority.SimpleGrantedAuthority; import java.util.List; import java.util.Map; public interface ProviderUser { String getId(); String getUserName(); String getPassword(); String getEmail(); String getProvider(); List<SimpleGrantedAuthority> getAuthorities(); Map<String, Object> getAttributes(); }package com.example.oauth.config.OAuth2; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.stream.Collectors; // 여기는 공통적인 부분만 추상화해서 모아놓은 곳이다. // Google, Naver에서 다른 부분들은 따로 만들어줄 것이다. public abstract class OAuth2ProviderUser implements ProviderUser{ private Map<String, Object> attributes; private OAuth2User oAuth2User; private ClientRegistration clientRegistration; public OAuth2ProviderUser(Map<String, Object> attributes, OAuth2User oAuth2User, ClientRegistration clientRegistration) { this.attributes = attributes; this.oAuth2User = oAuth2User; this.clientRegistration = clientRegistration; } @Override public String getPassword() { return UUID.randomUUID().toString(); } @Override public String getEmail() { return (String) getAttributes().get("email"); } @Override public List<SimpleGrantedAuthority> getAuthorities() { return oAuth2User.getAuthorities().stream() .map(authority -> new SimpleGrantedAuthority(authority.getAuthority())) .collect(Collectors.toList()); } @Override public String getProvider() { return clientRegistration.getRegistrationId(); } @Override public Map<String, Object> getAttributes() { return attributes; } }package com.example.oauth.config.OAuth2; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.user.OAuth2User; import java.util.Map; public class NaverUser extends OAuth2ProviderUser{ public NaverUser(OAuth2User oAuth2User, ClientRegistration clientRegistration) { super(( Map<String, Object>) oAuth2User.getAttributes().get("response"), oAuth2User, clientRegistration); } @Override public String getId() { return (String) getAttributes().get("id"); } @Override public String getUserName() { return (String) getAttributes().get("name"); } }package com.example.oauth.config.OAuth2; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.core.user.OAuth2User; public class GoogleUser extends OAuth2ProviderUser{ public GoogleUser(OAuth2User oAuth2User, ClientRegistration clientRegistration) { // 사용자의 정보는 oAuth2User.getAttributes() 여기에 담겨져 있다. // 여기는 클레임 형식 즉, Map 형식으로 되어 있다. super(oAuth2User.getAttributes(), oAuth2User, clientRegistration); } @Override // 식별자의 역할 public String getId() { return (String) getAttributes().get("sub"); } @Override // 유저 id public String getUserName() { return (String) getAttributes().get("name"); } }package com.example.oauth.config.OAuth2; import com.example.oauth.repository.UserRepository; import com.example.oauth.service.UserService; import org.springframework.security.oauth2.client.registration.ClientRegistration; 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.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; @Service // OAuth2UserService : Spring Security에서 OAuth 2.0을 사용하여 인증한 사용자 정보를 가져오기 위한 인터페이스입니다. // 이 인터페이스를 구현하여 사용자 정보를 가져오는 방법을 정의하고, // OAuth 2.0 프로바이더(예: Google, Facebook, GitHub 등)로부터 인증된 사용자 정보를 추출할 수 있습니다. // OAuth2UserRequest : 이 객체는 OAuth 2.0 클라이언트 정보, 권한 부여 코드, 액세스 토큰 등을 포함합니다. // 이 정보를 사용하여 사용자 정보를 요청하고 처리할 수 있습니다. // OAuth2User : OAuth 2.0 프로바이더(인증 제공자)로부터 가져온 인증된 사용자 정보를 나타냅니다. // 이 정보는 사용자의 프로필 데이터, 권한(스코프), 사용자 ID 등을 포함할 수 있습니다. public class CustomOAuth2UserService extends AbstractOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> { public CustomOAuth2UserService(UserRepository userRepository, UserService userService) { super(userRepository, userService); } @Override public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { // ClientRegistration은 Spring Security에서 OAuth 2.0 또는 OpenID Connect (OIDC) 클라이언트 // 애플리케이션의 등록 정보를 나타내는 클래스입니다. 클라이언트 애플리케이션의 설정 및 속성을 포함합니다. // userRequest.getClientRegistration()은 인증 및 인가된 사용자 정보를 가져오는 // Spring Security에서 제공하는 메서드입니다. ClientRegistration clientRegistration = userRequest.getClientRegistration(); OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService(); OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest); // 여기에는 구글, 네이버 정보가 담겨져 있다. ProviderUser providerUser = super.providerUser(clientRegistration, oAuth2User); // 회원가입 super.register(providerUser, userRequest); return oAuth2User; } }package com.example.oauth.config.OAuth2; import com.example.oauth.entity.UserEntity; import com.example.oauth.repository.UserRepository; import com.example.oauth.service.UserService; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.security.oauth2.client.registration.ClientRegistration; import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; @Service @Getter @RequiredArgsConstructor @Log4j2 // 사용자 등록과 어떤 사용자인지 알아볼 수 있는 곳 public abstract class AbstractOAuth2UserService { private final UserRepository userRepository; private final UserService userService; protected void register(ProviderUser providerUser, OAuth2UserRequest userRequest) { UserEntity findUser = userRepository.findByUserName(providerUser.getUserName()); if(findUser == null) { String registrationId = userRequest.getClientRegistration().getRegistrationId(); userService.register(registrationId, providerUser); } else { log.info("user : " + findUser); } } protected ProviderUser providerUser(ClientRegistration clientRegistration, OAuth2User oAuth2User) { String registrationId = clientRegistration.getRegistrationId(); if(registrationId.equals("google")) { return new GoogleUser(oAuth2User, clientRegistration); } else if(registrationId.equals("naver")) { return new NaverUser(oAuth2User, clientRegistration); } else { return null; } } }이거를 REST 방식으로 하려고 컨트롤러 // 소셜 로그인 @GetMapping("/api/v1/user/success") public ResponseEntity<?> socialLogin(Authentication authentication, @AuthenticationPrincipal OAuth2User oAuth2User) { OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication; if (oAuth2AuthenticationToken != null) { ResponseEntity<?> oAuth2Login = memberService.login(oAuth2AuthenticationToken, oAuth2User); return ResponseEntity.ok().body(oAuth2Login); } else { return ResponseEntity.badRequest().build(); } }서비스 // 소셜 로그인 public ResponseEntity<?> login(OAuth2AuthenticationToken oAuth2AuthenticationToken, OAuth2User oAuth2User) { Map<String, Object> attributes = oAuth2User.getAttributes(); log.info("attributes : " + attributes); String authorizedClientRegistrationId = oAuth2AuthenticationToken.getAuthorizedClientRegistrationId(); String email = null; TokenDTO jwt = null; if (authorizedClientRegistrationId.equals("google")) { email = (String) attributes.get("email"); log.info("email : " + email); jwt = createJWT(email); log.info("jwt : " + jwt); } else if (authorizedClientRegistrationId.equals("naver")) { Map<String, Object> response = (Map) attributes.get("response"); email = (String) response.get("email"); jwt = createJWT(email); log.info("jwt : " + jwt); } else { log.info("아무런 정보를 받지 못했습니다."); } return ResponseEntity.ok().body(jwt); } private TokenDTO createJWT(String email) { MemberEntity findUser = memberRepositroy.findByEmail(email); List<GrantedAuthority> authoritiesForUser = getAuthoritiesForUser(findUser); TokenDTO tokenForOAuth2 = jwtProvider.createTokenForOAuth2(email, authoritiesForUser); TokenEntity findToken = tokenRepository.findByMemberEmail(tokenForOAuth2.getMemberEmail()); if (findToken == null) { TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenForOAuth2); tokenRepository.save(tokenEntity); log.info("token : " + tokenForOAuth2); } else { tokenForOAuth2 = TokenDTO.builder() .id(findToken.getId()) .grantType(tokenForOAuth2.getGrantType()) .accessToken(tokenForOAuth2.getAccessToken()) .refreshToken(tokenForOAuth2.getRefreshToken()) .memberEmail(tokenForOAuth2.getMemberEmail()) .build(); TokenEntity tokenEntity = TokenEntity.toTokenEntity(tokenForOAuth2); tokenRepository.save(tokenEntity); } return tokenForOAuth2; }이렇게 구성을 했는데 JWT는 제외하고 public ResponseEntity<?> socialLogin(Authentication authentication,@AuthenticationPrincipal OAuth2User oAuth2User) { 이거로 정보를 가지고 오려고 했는데 정보를 안가지고 와지고 계속 JWT 검증 로직에 걸려서 잘못된 JWT라고 뜹니다 ㅠㅠ package com.example.study01.config.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; @Log4j2 @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { public static final String HEADER_AUTHORIZATION = "Authorization"; private final JwtProvider jwtProvider; @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 = 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); } else { log.error("유효한 JWT가 없습니다. : " + requestURI); } filterChain.doFilter(request, response); } // 토큰을 가져오기 위한 메소드 // Authorization로 정의된 헤더 이름을 사용하여 토큰을 찾고 // 토큰이 "Bearer "로 시작하거나 "Bearer "로 안온 것도 토큰 반환 private String resovleToken(HttpServletRequest httpServletRequest) { String token = httpServletRequest.getHeader(HEADER_AUTHORIZATION); // 토큰이 포함하거나 Bearer 로 시작하면 true if(StringUtils.hasText(token) && token.startsWith("Bearer ")) { return token.substring(7); } else if(StringUtils.hasText(token)) { return token; } else { return null; } } }이럴 경우는 어떻게 해야하나요... 컨트롤러에서 정보를 가지고 와서 그 정보로 JWT를 만들어 반환하려고 하는데 정보를 가지고 오는게 계속 실패를 해서...
-
미해결스프링 시큐리티 OAuth2
이제 spring boot 버전 3으로 가는데
spring boot 3 버전은spring security 디폴트가 6로 설정되어 있더라구요spring security 6 진행시에 Oauth2 강의 진행에 문제가 없을까요?? 여러가지로 바뀐게 많아서
-
미해결스프링 시큐리티 OAuth2
HttpSecurity에서 생성한 SecurityFilterChain과 WebSecurity에서 생성한 SecurityFilterChain의 차이점과 사용 사례가 궁금합니다.
지금까지의 강의 내용을 참고해보면, HttpSecurity, WebSecurity 둘다 SecurityConfigurer를 구성할 수 있기 때문에, 둘다 SecurityFilterChain을 형성할 수 있다로 이해했습니다. 그런데, springSecurity가 적용된 프로젝트들의 설정 빈들을 살펴보면 거의 HttpSecurity로만 활용하여 SecurityFilterChain을 구성하고 WebSecurity의 경우, WebSecurityCustomizer를 살짝 커스터마이징(리소스 접근 가능여부)이 적용만 되어있는것으로 확인했습니다. 이렇게 거의 HttpSecurity만을 활용하여 SecurityFilterChain을 커스터마이징하고 추가하는 경우가 많은 이유는 WebSecurity와 HttpSecurity가 담당하는 역할의 차이점 때문일까요? 아니면 프레임워크 구조 때문인걸까요?
-
미해결스프링 시큐리티 OAuth2
OAuth2 인가서버로부터 access token 발급 후 저장 관련 질문
안녕하세요 강사님.OAuth2 인가 프레임워크 관련하여 학습 중 인가서버로부터 받은 access token을 어떻게 저장하고 어떻게 활용할 수 있을지 고민하다가 궁금한 점이 있어 질문드립니다. 제가 생각했던 프로세스는 이렇습니다.[리소스 오너가 소셜 로그인 시 (구글인 경우)]리소스 오너가 구글 로그인을 한다.클라이언트 (내가 만든 스프링 서버)는 구글의 인가서버로부터 리소스 오너의 userInfo 엔드포인트 접근 시 사용할 수 있는 access token을 받는다.access token을 활용해서 userInfo 엔드포인트에 접근하여 리소스 오너의 이메일, 이름 등의 정보를 가져온다. (이 부분은 내가 구현하지 않아도 스프링 시큐리티에서 제공)소셜 로그인 시 얻은 정보 (oidc 활성화 시 id-token, 리소스 오너가 허용한 userInfo 등)를 클라이언트의 세션에 저장한다.그리고 로그인 프로세스 완료되어 LoginSuccessHandler에서 이후 로직을 처리할 수 있다. [이후 로그인 된 상태에서 클라이언트의 API 호출 시]클라이언트의 API를 호출할 때 SecurityContextHolder에 userInfo를 담아 활용할 수 있다.access token은 클라이언트에 저장되어 있고, API 호출 시 꺼내어 리소스 서버에 접근할 수 있다. 그런데 SecurityContextHolder에 access token 정보가 보이지 않아 궁금한점이 생겼습니다.만약 로그인이 된 상태에서 userInfo 엔드포인트에 접속 시 매번 새로운 access token을 발급 받아서 요청하는 것일까요? (SecurityContextHolder에 access token이 없어서 조금 헷갈리고 있습니다.)100명의 리소스 오너가 로그인 할 때마다 access token을 발급받으면 총 100개의 access token이 발급되는게 맞을까요?이러한 경우 클라이언트에 100개의 access token을 저장 후 userInfo 엔드포인트 접근 시 사용하며, 만약 만료되었다면 refresh token으로 access token을 재발급 받아 다시 userInfo 엔드포인트에 접근하는게 맞을까요?만약 1개의 클라이언트에 100개의 accessToken을 저장 후 만료 전까지 사용하는 것이라면 클라이언트에 부하가 많이 발생하여 scale-out하는 경우에는 보통 어떻게 해결할 수 있을까요?Google, Naver, Kakao 등의 소셜에서 access token을 발급 받는 것 까지는 스프링 시큐리티가 지원해주지만 이후 다양한 엔드포인트를 호출하거나, access token 만료 시 refresh token으로 재발급 받는 로직 등은 제가 구현해야 하는게 맞을까요? 제가 아직 강의를 절반밖에 듣지 않아서 이후 강의해주시는 부분에 제가 질문드린 내용이 포함되어 있을 수도 있을 것 같습니다. 만약 포함되어 있다면 간략하게나마 어떤 강의 동영상을 참고하면 좋을 지 말씀해주시면 감사하겠습니다.감사합니다.
-
미해결스프링 시큐리티 OAuth2
keyclock page not found 오류
clientFundamentals 클라이언트앱 커리큘럼을 실습하고 있는데요저는 9090 포트를 프로젝트의 서버 포트로 설정하고8080으로 키클락을 띄운상태인데요server: port: 9090 spring: security: oauth2: client: registration: # 클라이언트 설정 keyclock: authorization-grant-type: authorization_code # Oauth 2.0 권한부여타입 client-id: oauth2-client-app # 서비스 공급자에 등록된 클라이언트 아이디 client-name: oauth2-client-app # 클라이언트 이름 client-secret: XkPnnSZ9RLdMX6vJBsgcbTIL7gtYJ8m8 # 서비스 공급자에 등록된 클라이언트 비밀번호 redirect-uri: http:localhost:9090/login/oauth2/code/keyclock # 인가서버에 권한 코드 부여 후 클라이언트로 리다이렉트하는 위치 authorizationGrantType: authorization_code clientAuthenticationMethod: client-secret-basic # 클라이언트 자격증명 전송방식 scope: openid,profile,email # 리소스에 접근 제한 범위 provider: # 공급자 설정 keyclock: authorization-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/auth # oauth 2.0 권한 코드 부여 엔드포인트 issuer-uri: http://localhost:8080/realms/oauth2 # 서비스 공급자 위치 jwk-set-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/certs token-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/token user-info-uri: http://localhost:8080/realms/oauth2/protocol/openid-connect/userinfo user-name-attribute: preferred_username user 아이디로 로그인시 강나님처럼 do you grant these access privilliges? 화면이 뜨지않고http://localhost:8080/realms/oauth2/login-actions/localhost:9090/login/oauth2/code/keyclock?state=kCCPxAYfg3uXfG7M_vmcVzq4FVQIldvt_3viiZlE0U0%3D&session_state=09a4fb12-19eb-4f15-991e-24365d7b5b05&code=6b7b3d33-8f8f-4979-8bfe-e038f7a275a4.09a4fb12-19eb-4f15-991e-24365d7b5b05.2912d929-159d-4403-b7cb-7e7cb0d24f5e 해당 URI 로 이동하면서 we are sorry... page not found가 뜹니다어떤부분이 누락되서 오류가 나는건지 모르겠습니다
-
해결됨스프링 시큐리티 OAuth2
키클록 서버 종료후 realm 삭제
안녕하세요.키클록 종료 후 재기동 하니 만들었던 realm, client, user 가 삭제됩니다. 원래 그런건가요..?
-
미해결스프링 시큐리티 OAuth2
1편을 들고 2편으로 넘어 오긴했는데..
1편을 끝내고 이제 2편으로 들어왔는데 아직 1편을 많이 미흡하게 이해된부분이 많은데.. 2편을 들으면서 복습하는 방식으로 해도될까요?