묻고 답해요
161만명의 커뮤니티!! 함께 토론해봐요.
인프런 TOP Writers
-
미해결스프링 시큐리티 OAuth2
Spring security 6 이후 client-authentication-method 설정 방법 공유
yml에client-authentication-method: POST로 설정하면 아마 안될 것 입니다.ㅠㅠclient-authentication-method: client_secret_post로 설정하면 해결됩니다..이것 때문에 한참 고생했네요.....
-
미해결스프링 시큐리티 OAuth2
MSA 방식의 구조
현재 예제로 구성된 방식은 MSA방식이라고 생각해도 될까요? 자원서버 ( 앨범 , 친구 ) , 클라이언트서버 , 인가서버 형식으로 각각 서버를 나누어서 역할이 구분되어 있는방식이라 제가 이해한것이 맞는지 궁금합니다. 항상 좋은강의 해주셔서 감사합니다
-
미해결스프링 시큐리티 OAuth2
리소스 서버와 인가 서버가 동일한 예시
안녕하세요? 먼저 좋은 강의 감사드립니다. 실무에 많은 도움을 받고 있습니다.OAuth2.0 Roles(Resource Owner, Resource Server, Authorization Server, Client)의 개념에 대해 궁금한 점이 생겨서 아래 예시와 함께 질문드립니다.카카오톡과 연동한 게임을 보면 게임내 순위 화면에 카카오톡의 프로필 사진이 사용되는 것을 알 수 있는데요.이 경우 카카오톡의 OAuth2.0 인가 서버가 사용자 인증의 역할과 함께 리소스 서버(프로필 사진 제공)의 역할도 함께 수행하는 것으로 보이는데요 이게 맞을까요?감사합니다.
-
해결됨스프링 시큐리티 OAuth2
모바일앱의 인증을 위해 자체 인가서버와 소셜로그인을 함께 사용하는 경우
우선 좋은 강의 감사하다는 말씀드립니다. 올려주신 강의 2편 모두 잘 보고 있고 실무에서 많은 도움을 받고 있습니다.강사님께 배운 내용을 토대로 인증시스템을 구성해보려던 중 질문 사항이 있어 질문드립니다. 강의에서는 웹애플리케이션 기반 인증에 비중을 두고 설명을 잘 해주셨는데, 모바일 애플리케이션에 대상으로 인증을 구성을 해보려하니 조금 헷갈리는 부분이 있네요.요구사항은 아래와 같은데요,추후 내부 리소스를 외부서비스에 제공하기 위한 자체 OAuth2.0인가 서버 구성모바일 애플리케이션에서 JWT기반 인증을 할 수 있는 인증 및 회원 가입 API자체 OAuth2 인가서버와 연계하여 구성되는 리소스서버 구성소셜로그인 기능인증 시스템을 구성하기 위해 아래와 같은 방법을 생각하고 있습니다.자체 인증 시스템을 통한 로그인의 경우 자체 모바일앱에 별도의 web view를 생성하고 싶지 않으므로 API로 로그인 정보를 바로 받아서 내부적으로 Resource Owner Password Flow 처리하여 토큰 반환유저가 모바일앱 UI에 OAuth2 인가서버 계정정보(ID/PASSWORD)를 직접 입력로그인 버튼 클릭 시, 서버가 계정 정보를 API로 직접 전달받아 내부적으로 Resource Owner Password Flow를 통해 검증하고 토큰 및 인증 필요 정보를 클라이언트에 반환소셜로그인의 경우, 안전한 인증을 제공하기 위해 모바일 UI에 별도의 webView를 띄우고 Authorization Code Grant방식으로 인증진행webView를 통해 외부서비스에 대한 Authorization Code Grant 흐름이 진행.최종적으로 서버가 리다이렉트 주소를 통해 code를 전달받아 소셜 인증이 완료되는 경우(최초 회원인 경우 가입 처리하고), 서버는 자체 리소스서버에서 검증할 수 있는 Access Token을 따로 발급하여 응답하고 이를 모바일앱이 수신하여 인증처리. 질문은 아래와 같습니다.OAuth2.0 인가서버를 운용하는 경우 자사 모바일 앱의 자체로그인을 Resource Owner Password Flow로 직접 처리하는 것이 일반적인 방법인지? 더 좋은 방법은 없는지?외부 소셜 로그인의 경우 패스워드 없이 가입처리를 해야하는데 자체인가서버의 회원과 어떻게 일괄적으로 관리가 가능할지?외부 소셜 로그인의 경우라도 최종 발급되는 토큰은 자체 리소스서버에서 검증가능하도록 자체로그인과 일괄된 방법으로 JWT 발급이 되어야 할 것 같은데, Spring Authorization Server를 통해 효율적으로 구성이 가능할지?소셜로그인의 경우 서버가 리다이렉트 주소를 통해 code를 받아오더라도 OAuth2AuthorizedClient까지만 확인하고 세션에 인증정보를 저장하고 싶지 않은데 어떻게 효율적으로 custom 가능할지?oauth2Client API 및 OAuth2AuthorizedClientManager를 통해 토큰을 받아오는 것을 생각하고 있는데 더 합리적인 방법은 없을지?긴 질문 읽어주셔서 감사합니다. 질문가지수가 많아서 죄송하네요. 답변 주실 수 있는 부분까지만 주셔도 괜찮습니다.혹시 제가 구상하는 방법에서 개선되어야 하는 부분이나 혹시나 제가 잘못 생각하고 있는 부분은 없을 지 이런 부분도 조언해주시면 너무 감사하겠습니다.
-
미해결생활코딩 - Web2 : OAuth 2.0
Spring security oauth2가 Linux 환경과 mac local 환경에서 다르게 동작합니다.
spring security로 oauth2 개발중인데, 아래는 local에서 돌린거고 위에는 ec2 linux에 배포해서 돌린 서버인데요. 똑같은 코드에 똑같이 dev 환경으로 실행되었는데 local에서는 Redirecting to /oauth/login 후 security context에 잘 저장한 뒤 Securing GET /oauth/login 로 넘어가서 로직이 정상적으로 실행되는데linux에서는 Redirecting to /oauth/login 후 security context에 저장하는 과정 없이 SecurityContextHolder가 클리어 되고 그 이후 프로세스가 진행되지 않습니다 ㅜ.ㅜ 왜 이런 차이가 발생하는걸까요..?
-
미해결스프링 시큐리티 OAuth2
소셜로그인 + Jwt 인증 어떻게 하나요?
소셜로그인 성공 이후 JWT 인증하는 방법을 알고 싶어 강의를 수강했습니다.소셜로그인과 JWT 인증을 어떻게 같이 쓰는지 예시를 알려 주실 수 있나요실무에 바로 적용하고 싶은데 강의에서 보여 소셜로그인, JWT 인증 강의만으론 소셜로그인 성공 이후 어떻게 토큰을 발급해 react 또는 모바일 네이티브 앱의 사용자을 로그인, 회원가입시키는지 모르겠네요.샘플 코드를 첨부해주시면 감사하겠습니다.
-
미해결스프링 시큐리티 OAuth2
oauth2clinet api 사용 이유?
시큐리티 필터체인 구성 시 oauth2login api를 사용하면 되는데 굳이 oauth2client를 사용할 이유가 어떤것이 있을까요?
-
미해결스프링 시큐리티 OAuth2
0auth2.0 roles의 이해
키 클락을 로컬 8080으로 띄워서 강의처럼 실습을 하고 있습니다roles1번 을 통해 user 로 로그인 바로 리다이렉트 http:localhost:8081 로 넘어가고grant access 화면이 뜨지 않는데 왜 그런건가요 ?
-
미해결스프링 시큐리티 OAuth2
질문 사항
17:30 ppt를 보면 back chaennel에서 '클라이언트가 최종 사용자를 가지고 있는가?' 에 대한 말씀을 하시면서 '클라이언트가 사용자인 동시에 클라이언트의 역할을 수행하는 경우를 말한다'고 하셨는데 '아니오'일 때 Client Credentials Grant Type의 방식을 사용하게 되는 것이 이해가 잘 가지 않습니다.22:10'token, id_token의 경우 권한 부여 유형에서 지원해야 한다.' 라고 하셨는데 해당 방식의 경우 인가 서버의 구현 여부에 따라 사용할 수 있는지가 달라지는걸 말씀 하신건가요?25:25임시 코드 요청시와 액세스 토큰 요청 시에 같은 uri를 보내야 한다고 하셨는데, 이 값은 리소스 서버에 등록되어 있는 클라이언트의 redirect_uri 값과 항상 동일해야 하나요? 서버에 등록된 값과 다를 수 있는지 궁금합니다.
-
미해결스프링 시큐리티 OAuth2
Spring Authorization Server 1.0.1 state 문의
authorization_code grantType 으로 인가를 진행한다고 가 정하였을때 authorization_code 발급 요청에서 사용한 state 와 token 발급 요청해서 사용하는 state가 다르면 token 발급이 실패 해야 하는데 성공하고 있어서 분석해보니 oauth2_authorization 테이블의 state 필드가 null 로 저장되고 있습니다. JdbcOAuth2AuthorizationService를 사용하지 않고 커스텀한 구현체를 만들어서 사용하고 있는것이 원인일 수 있어 디버깅 해보니 OAuth2Authorization 의 소스를 분석해보면 state 가 포함되고 있지 않아서 저장이 안되고 있는데 아직 Spring Authorization Server는 state 가 구현되지 않은 걸까요? 추가로 소스를 분석해보니 nonce 도 아직 구현되지 않은거 같습니다.Spring Authorization Server 1.0.1 의 OAuth2Authorization 소스package org.springframework.security.oauth2.server.authorization; import java.io.Serializable; import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import org.springframework.lang.Nullable; import org.springframework.security.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.OAuth2AccessToken; import org.springframework.security.oauth2.core.OAuth2RefreshToken; import org.springframework.security.oauth2.core.OAuth2Token; import org.springframework.security.oauth2.server.authorization.client.RegisteredClient; import org.springframework.security.oauth2.server.authorization.util.SpringAuthorizationServerVersion; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * A representation of an OAuth 2.0 Authorization, which holds state related to the authorization granted * to a {@link #getRegisteredClientId() client}, by the {@link #getPrincipalName() resource owner} * or itself in the case of the {@code client_credentials} grant type. * * @author Joe Grandja * @author Krisztian Toth * @since 0.0.1 * @see RegisteredClient * @see AuthorizationGrantType * @see OAuth2Token * @see OAuth2AccessToken * @see OAuth2RefreshToken */ public class OAuth2Authorization implements Serializable { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; private String id; private String registeredClientId; private String principalName; private AuthorizationGrantType authorizationGrantType; private Set<String> authorizedScopes; private Map<Class<? extends OAuth2Token>, Token<?>> tokens; private Map<String, Object> attributes; protected OAuth2Authorization() { } /** * Returns the identifier for the authorization. * * @return the identifier for the authorization */ public String getId() { return this.id; } /** * Returns the identifier for the {@link RegisteredClient#getId() registered client}. * * @return the {@link RegisteredClient#getId()} */ public String getRegisteredClientId() { return this.registeredClientId; } /** * Returns the {@code Principal} name of the resource owner (or client). * * @return the {@code Principal} name of the resource owner (or client) */ public String getPrincipalName() { return this.principalName; } /** * Returns the {@link AuthorizationGrantType authorization grant type} used for the authorization. * * @return the {@link AuthorizationGrantType} used for the authorization */ public AuthorizationGrantType getAuthorizationGrantType() { return this.authorizationGrantType; } /** * Returns the authorized scope(s). * * @return the {@code Set} of authorized scope(s) * @since 0.4.0 */ public Set<String> getAuthorizedScopes() { return this.authorizedScopes; } /** * Returns the {@link Token} of type {@link OAuth2AccessToken}. * * @return the {@link Token} of type {@link OAuth2AccessToken} */ public Token<OAuth2AccessToken> getAccessToken() { return getToken(OAuth2AccessToken.class); } /** * Returns the {@link Token} of type {@link OAuth2RefreshToken}. * * @return the {@link Token} of type {@link OAuth2RefreshToken}, or {@code null} if not available */ @Nullable public Token<OAuth2RefreshToken> getRefreshToken() { return getToken(OAuth2RefreshToken.class); } /** * Returns the {@link Token} of type {@code tokenType}. * * @param tokenType the token type * @param <T> the type of the token * @return the {@link Token}, or {@code null} if not available */ @Nullable @SuppressWarnings("unchecked") public <T extends OAuth2Token> Token<T> getToken(Class<T> tokenType) { Assert.notNull(tokenType, "tokenType cannot be null"); Token<?> token = this.tokens.get(tokenType); return token != null ? (Token<T>) token : null; } /** * Returns the {@link Token} matching the {@code tokenValue}. * * @param tokenValue the token value * @param <T> the type of the token * @return the {@link Token}, or {@code null} if not available */ @Nullable @SuppressWarnings("unchecked") public <T extends OAuth2Token> Token<T> getToken(String tokenValue) { Assert.hasText(tokenValue, "tokenValue cannot be empty"); for (Token<?> token : this.tokens.values()) { if (token.getToken().getTokenValue().equals(tokenValue)) { return (Token<T>) token; } } return null; } /** * Returns the attribute(s) associated to the authorization. * * @return a {@code Map} of the attribute(s) */ public Map<String, Object> getAttributes() { return this.attributes; } /** * Returns the value of an attribute associated to the authorization. * * @param name the name of the attribute * @param <T> the type of the attribute * @return the value of an attribute associated to the authorization, or {@code null} if not available */ @Nullable @SuppressWarnings("unchecked") public <T> T getAttribute(String name) { Assert.hasText(name, "name cannot be empty"); return (T) this.attributes.get(name); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } OAuth2Authorization that = (OAuth2Authorization) obj; return Objects.equals(this.id, that.id) && Objects.equals(this.registeredClientId, that.registeredClientId) && Objects.equals(this.principalName, that.principalName) && Objects.equals(this.authorizationGrantType, that.authorizationGrantType) && Objects.equals(this.authorizedScopes, that.authorizedScopes) && Objects.equals(this.tokens, that.tokens) && Objects.equals(this.attributes, that.attributes); } @Override public int hashCode() { return Objects.hash(this.id, this.registeredClientId, this.principalName, this.authorizationGrantType, this.authorizedScopes, this.tokens, this.attributes); } /** * Returns a new {@link Builder}, initialized with the provided {@link RegisteredClient#getId()}. * * @param registeredClient the {@link RegisteredClient} * @return the {@link Builder} */ public static Builder withRegisteredClient(RegisteredClient registeredClient) { Assert.notNull(registeredClient, "registeredClient cannot be null"); return new Builder(registeredClient.getId()); } /** * Returns a new {@link Builder}, initialized with the values from the provided {@code OAuth2Authorization}. * * @param authorization the {@code OAuth2Authorization} used for initializing the {@link Builder} * @return the {@link Builder} */ public static Builder from(OAuth2Authorization authorization) { Assert.notNull(authorization, "authorization cannot be null"); return new Builder(authorization.getRegisteredClientId()) .id(authorization.getId()) .principalName(authorization.getPrincipalName()) .authorizationGrantType(authorization.getAuthorizationGrantType()) .authorizedScopes(authorization.getAuthorizedScopes()) .tokens(authorization.tokens) .attributes(attrs -> attrs.putAll(authorization.getAttributes())); } /** * A holder of an OAuth 2.0 Token and it's associated metadata. * * @author Joe Grandja * @since 0.1.0 */ public static class Token<T extends OAuth2Token> implements Serializable { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; protected static final String TOKEN_METADATA_NAMESPACE = "metadata.token."; /** * The name of the metadata that indicates if the token has been invalidated. */ public static final String INVALIDATED_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("invalidated"); /** * The name of the metadata used for the claims of the token. */ public static final String CLAIMS_METADATA_NAME = TOKEN_METADATA_NAMESPACE.concat("claims"); private final T token; private final Map<String, Object> metadata; protected Token(T token) { this(token, defaultMetadata()); } protected Token(T token, Map<String, Object> metadata) { this.token = token; this.metadata = Collections.unmodifiableMap(metadata); } /** * Returns the token of type {@link OAuth2Token}. * * @return the token of type {@link OAuth2Token} */ public T getToken() { return this.token; } /** * Returns {@code true} if the token has been invalidated (e.g. revoked). * The default is {@code false}. * * @return {@code true} if the token has been invalidated, {@code false} otherwise */ public boolean isInvalidated() { return Boolean.TRUE.equals(getMetadata(INVALIDATED_METADATA_NAME)); } /** * Returns {@code true} if the token has expired. * * @return {@code true} if the token has expired, {@code false} otherwise */ public boolean isExpired() { return getToken().getExpiresAt() != null && Instant.now().isAfter(getToken().getExpiresAt()); } /** * Returns {@code true} if the token is before the time it can be used. * * @return {@code true} if the token is before the time it can be used, {@code false} otherwise */ public boolean isBeforeUse() { Instant notBefore = null; if (!CollectionUtils.isEmpty(getClaims())) { notBefore = (Instant) getClaims().get("nbf"); } return notBefore != null && Instant.now().isBefore(notBefore); } /** * Returns {@code true} if the token is currently active. * * @return {@code true} if the token is currently active, {@code false} otherwise */ public boolean isActive() { return !isInvalidated() && !isExpired() && !isBeforeUse(); } /** * Returns the claims associated to the token. * * @return a {@code Map} of the claims, or {@code null} if not available */ @Nullable public Map<String, Object> getClaims() { return getMetadata(CLAIMS_METADATA_NAME); } /** * Returns the value of the metadata associated to the token. * * @param name the name of the metadata * @param <V> the value type of the metadata * @return the value of the metadata, or {@code null} if not available */ @Nullable @SuppressWarnings("unchecked") public <V> V getMetadata(String name) { Assert.hasText(name, "name cannot be empty"); return (V) this.metadata.get(name); } /** * Returns the metadata associated to the token. * * @return a {@code Map} of the metadata */ public Map<String, Object> getMetadata() { return this.metadata; } protected static Map<String, Object> defaultMetadata() { Map<String, Object> metadata = new HashMap<>(); metadata.put(INVALIDATED_METADATA_NAME, false); return metadata; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } Token<?> that = (Token<?>) obj; return Objects.equals(this.token, that.token) && Objects.equals(this.metadata, that.metadata); } @Override public int hashCode() { return Objects.hash(this.token, this.metadata); } } /** * A builder for {@link OAuth2Authorization}. */ public static class Builder implements Serializable { private static final long serialVersionUID = SpringAuthorizationServerVersion.SERIAL_VERSION_UID; private String id; private final String registeredClientId; private String principalName; private AuthorizationGrantType authorizationGrantType; private Set<String> authorizedScopes; private Map<Class<? extends OAuth2Token>, Token<?>> tokens = new HashMap<>(); private final Map<String, Object> attributes = new HashMap<>(); protected Builder(String registeredClientId) { this.registeredClientId = registeredClientId; } /** * Sets the identifier for the authorization. * * @param id the identifier for the authorization * @return the {@link Builder} */ public Builder id(String id) { this.id = id; return this; } /** * Sets the {@code Principal} name of the resource owner (or client). * * @param principalName the {@code Principal} name of the resource owner (or client) * @return the {@link Builder} */ public Builder principalName(String principalName) { this.principalName = principalName; return this; } /** * Sets the {@link AuthorizationGrantType authorization grant type} used for the authorization. * * @param authorizationGrantType the {@link AuthorizationGrantType} * @return the {@link Builder} */ public Builder authorizationGrantType(AuthorizationGrantType authorizationGrantType) { this.authorizationGrantType = authorizationGrantType; return this; } /** * Sets the authorized scope(s). * * @param authorizedScopes the {@code Set} of authorized scope(s) * @return the {@link Builder} * @since 0.4.0 */ public Builder authorizedScopes(Set<String> authorizedScopes) { this.authorizedScopes = authorizedScopes; return this; } /** * Sets the {@link OAuth2AccessToken access token}. * * @param accessToken the {@link OAuth2AccessToken} * @return the {@link Builder} */ public Builder accessToken(OAuth2AccessToken accessToken) { return token(accessToken); } /** * Sets the {@link OAuth2RefreshToken refresh token}. * * @param refreshToken the {@link OAuth2RefreshToken} * @return the {@link Builder} */ public Builder refreshToken(OAuth2RefreshToken refreshToken) { return token(refreshToken); } /** * Sets the {@link OAuth2Token token}. * * @param token the token * @param <T> the type of the token * @return the {@link Builder} */ public <T extends OAuth2Token> Builder token(T token) { return token(token, (metadata) -> {}); } /** * Sets the {@link OAuth2Token token} and associated metadata. * * @param token the token * @param metadataConsumer a {@code Consumer} of the metadata {@code Map} * @param <T> the type of the token * @return the {@link Builder} */ public <T extends OAuth2Token> Builder token(T token, Consumer<Map<String, Object>> metadataConsumer) { Assert.notNull(token, "token cannot be null"); Map<String, Object> metadata = Token.defaultMetadata(); Token<?> existingToken = this.tokens.get(token.getClass()); if (existingToken != null) { metadata.putAll(existingToken.getMetadata()); } metadataConsumer.accept(metadata); Class<? extends OAuth2Token> tokenClass = token.getClass(); this.tokens.put(tokenClass, new Token<>(token, metadata)); return this; } protected final Builder tokens(Map<Class<? extends OAuth2Token>, Token<?>> tokens) { this.tokens = new HashMap<>(tokens); return this; } /** * Adds an attribute associated to the authorization. * * @param name the name of the attribute * @param value the value of the attribute * @return the {@link Builder} */ public Builder attribute(String name, Object value) { Assert.hasText(name, "name cannot be empty"); Assert.notNull(value, "value cannot be null"); this.attributes.put(name, value); return this; } /** * A {@code Consumer} of the attributes {@code Map} * allowing the ability to add, replace, or remove. * * @param attributesConsumer a {@link Consumer} of the attributes {@code Map} * @return the {@link Builder} */ public Builder attributes(Consumer<Map<String, Object>> attributesConsumer) { attributesConsumer.accept(this.attributes); return this; } /** * Builds a new {@link OAuth2Authorization}. * * @return the {@link OAuth2Authorization} */ public OAuth2Authorization build() { Assert.hasText(this.principalName, "principalName cannot be empty"); Assert.notNull(this.authorizationGrantType, "authorizationGrantType cannot be null"); OAuth2Authorization authorization = new OAuth2Authorization(); if (!StringUtils.hasText(this.id)) { this.id = UUID.randomUUID().toString(); } authorization.id = this.id; authorization.registeredClientId = this.registeredClientId; authorization.principalName = this.principalName; authorization.authorizationGrantType = this.authorizationGrantType; authorization.authorizedScopes = Collections.unmodifiableSet( !CollectionUtils.isEmpty(this.authorizedScopes) ? new HashSet<>(this.authorizedScopes) : new HashSet<>() ); authorization.tokens = Collections.unmodifiableMap(this.tokens); authorization.attributes = Collections.unmodifiableMap(this.attributes); return authorization; } } }
-
미해결스프링 시큐리티 OAuth2
로그아웃관련하여 질문드립니다!
안녕하세요!제가 강의를 듣고 직접 구현을 해보고 있는 중에 궁금한 점이 있습니다. 저는 인가서버에다가 로그인 페이지를 구성해서 아이디와 비밀번호를 입력하면 동의화면이 나와서 SCOPE 를 부여한 뒤 리소스 서버에 접근하는 로직의 흐름으로 구현을 생각해보고 있습니다.로그아웃을 하고나면 인가서버를 통해서 동의 받은 SCOPE도 다 날려서 인가서버 로그인화면으로 리다이렉트 하고 싶습니다. 제 바람과는 달리 계속 이미 인가가 된 상태가 되어서 마지막 강의 예제 코드를 기반으로 연구하고 있는데 127.0.0.1:8081/home 에서 로그아웃을 하고난 뒤 127.0.0.1:8081 로 리다이렉트 되도록 했는데 로그인링크를 누르면 127.0.0.1:9000/login 으로 가는 것이 아니라 바로 127.0.0.1:8081 로 이동해서 access token 버튼이 있는 화면으로 넘어갑니다. 제가 질문드리고 싶은 것은 로그아웃을 하고나서 로그인 하면 인가서버에 구현된 로그인 화면으로 이동해서 다시 로그인해서 동의화면을 받도록 하려면 어떤식으로 로직을 구성해야 할까요?ㅠ 몇 주 고민하다가 방법을 모르겠어서 질문드립니다.
-
미해결스프링 시큐리티 OAuth2
사용자 관리에 대해서 문의드립니다.
강의에서는 네이버나 구글을 통해 로그인을 진행하면 access token 을 받고 이를 통해 사용자정보를 받아오면 이를 인증완료로 보는 것 같은데요.일반적으로는 받아온 사용자정보를 가지고 OAuth Client 자체 DB 에 사용자마스터를 등록하고 별도의 사용자 식별키 관리를 해야하지 않는지요? 이러한 처리가 생략되어 있는 것 같은데 이 부분이 궁금합니다.
-
미해결스프링 시큐리티 OAuth2
정수원님이 기술을 학습하시는 방법이 궁금합니다
안녕하세요 정수원님 우선 좋은 강의 만들어주셔서 감사합니다.강의와는 조금 다른 질문이지만 듣다보니 궁금한점이 생겨서 질문드립니다.예를 들어 Spring Security를 처음 학습하신다고 하면 어떤식으로 학습하시고, 내부구조를 디버깅해보시는지 궁금합니다.강좌에서보면 특정 클래스들에 break point를 걸고 디버깅하시는 모습을 보여주십니다.하지만 처음 Spring Security를 학습한다고 가정하면 어떤 클래스들이 핵심 역할을 하는지 파악하기 힘들것 같다고 느껴졌습니다. 개인적인 생각으로는 2가지 방법을 생각해 보았습니다.Spring Security Docs를 보고 주요 클래스들을 파악해본다WebMvcAutoConfiguration, SecurityAutoConfiguration등 클래스 하위로 파악해나간다 이에 대해 수원님은 어떤 방식으로 접근하시는지 궁금해서 질문 드립니다
-
미해결스프링 시큐리티 OAuth2
OAuth2 클라이언트를 어디에 구성하는 것이 좋은지 고민이 되어 질문드립니다
안녕하세요 정수원님~!현재 front가 react로 되어있다보니 OAuth2 클라이언트 서버를 node 서버냐(프론트에서 처리) spring boot 서버(백엔드에서 처리)냐에서 고민이 되는데 보통 이럴경우 OAuth2 클라이언트 서버를 어디에 구성하는게 좋을지 궁금합니다~!
-
미해결스프링 시큐리티 OAuth2
고도의 신뢰성을 가지고있는 클라이언트
안녕하세요 클라이언트가 고도의 신뢰성을 가지고있는가? 에 대해 No인경우 Authorization Code 방식을, Yes인경우에는 Resource Owner 방식을 선택한다는 내용이있었는데 여기서 말하는 고도의 신뢰성을 가진 클라이언트는 구글,네이버,페이스북 이런 곳들이 해당되는걸까요?흔히 생각했을때 인프런 로그인을할때 구글로 로그인하는경우에는 Authorization code 방식이지만, 구글자체에 로그인한다고하면 Resource Owner 방식으로 하고있는건가? 라는 생각이 들어서요
-
미해결스프링 시큐리티 OAuth2
OAuth 2.0 Authorization Grant Code 네이버 로그인 구현
안녕하세요 정수원님 강의 내용과 조금 다른 부분이긴한데 질문드리고자 합니다.Vue와 Spring Boot(Spring Security OAuth Client로 활용)로 네이버 로그인을 구현해보고 있습니다.웹, 안드로이드, iOS 등에 대한 소셜로그인을 Authorization Grant Code방식으로 구현하려고 하고 있으며 REST API 방식으로 동작하도록 하고싶습니다.하지만 Spring Security의 org.springframework.boot:spring-boot-starter-oauth2-client 의존성을 추가하여 OAuth2UserService를 구현하게되면 백엔드에 your-server-host/ oauth2/authorization/naver로 요청을 보내면 사용자에게 로그인을 받고 Authorization Grant Code를 받아와 AccessToken을 반환받고 이를 통해 UserInfo를 Resource Server로부터 받아오는 과정을 처리해줍니다.이 과정속에서 프론트에게 로그인 성공시 실행되는 AuthenticationSuccessHandler 구현 과정에서 sendRedirect를 통해 accessToken과 refreshToken을 전달하려고 했으나 이런 방식은 REST API에 적합하지 않은것 같다고 느껴집니다.심지어 front에서 버튼을 클릭하면 GET 요청으로 /oauth2/authorization/naver를 호출하도록 해보려고 했으나 이 경우에 네이버 로그인 호출하는 과정에서 CORS에러가 발견되어 하이퍼링크로 호출하도록 구현하였습니다. 위의 배경으로 궁금한 점은 다음과 같습니다.프론트엔드와 Authorization Grant Code를 REST API 방식으로 구현하기 위해서는 어떻게 해야할까요? 제가 생각한 방식과 굉장히 유사한 블로그 포스팅을 발견했는데 Spring Security는 사용하지 않는 것 같습니다.(https://blog.naver.com/PostView.naver?blogId=varkiry05&logNo=222295764870) Spring Security를 활용하는 방안이 있다면 추천 부탁드립니다.. 혹은 굳이 Spring Security를 사용하지 않고 구현해도 된다고 생각하시는경우에도 의견주시면 감사하겠습니다.위의 경우에는 client-id를 프론트에서 관리하며 강좌에서 이런 경우에는 보안상의 이슈가 발생할 수 있다고 하셨는데 어떻게 대안점이 있을까요? (https://www.rfc-editor.org/rfc/rfc6749#section-2.3.1) rfc6749에서는 client-id는 secret이 아니라고 하는것 같습니다. 제가 처음 구현하려던 방식과 유사한 tech blog를 찾았습니다. 내부적인 커스터마이징을 통해 redirection을 구현한 것 같습니다.(https://tech.kakao.com/2023/01/19/social-login/) 이때 accessToken과 refreshToken을 redirect시킬 때 param으로 넘긴다면 url주소로 토근값들이 보일것인데 보안상으로 괜찮을까요? ps.실제로 https://olive.kakao.com/login (카카오 테크 블로그에 소개된 redirection이 구현된 페이지)의 js 코드를 까보니 <a> 태그 형식의 하이퍼링크로 호출함을 확인했으며, redirect되는 callback url에서 token값을 확인할 수 있음을 크롬 개발자도구 네트워크탭에서 확인했습니다. 감사합니다.
-
미해결스프링 시큐리티 OAuth2
subject type을 pairwise 변경 문의
subject_type을 pairwise로 변경 하려고 Spring Authorization Server 공식 문서(현재 1.0.1)와 소스(1.0.1과 강의에 사용된 0.31) 소스를 봐도 subject_type 를 변경하는 API는 찿지 못했는데 Spring Authorization Server 에서는 지원하지 않는 걸까요?https://docs.spring.io/spring-authorization-server/docs/current/reference/html/core-model-components.html#oauth2-token-customizer문서를 참조하여 acesss token 과 id token 의 cliams 정보를 커스터마이징한 소스에서 pairwise 유형으로 설정하고 providerConfigurationEndpoint을 커스텀 하여 openid-configuration discovery metadata 의 정보를 변경하면 될까요?@Bean public OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) { JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource); JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder); jwtGenerator.setJwtCustomizer(jwtCustomizer()); OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator(); OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator(); return new DelegatingOAuth2TokenGenerator( jwtGenerator, accessTokenGenerator, refreshTokenGenerator); } @Bean public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() { return context -> {; JwtClaimsSet.Builder claims = context.getClaims(); OidcUserResponseDto user = (OidcUserResponseDto)context.getPrincipal().getPrincipal(); claims.subject(user.getEmpId()); claims.claim("dept_id", user.getDeptId()); claims.claim("locale", user.getLocale()); }; } ......... authorizationServerConfigurer.tokenGenerator(tokenGenerator); 참고 현재 설정된 openid-configuration의 subject_types_supported 정보
-
해결됨스프링 시큐리티 OAuth2
Authentication를 받아오는 차이가 뭔가요?
파라미터로 Authentication 객체를 받아오면 Null이고 직접 SecurityContextHolder에서 꺼내오면 anonymousUser가 들어있는 이유가 뭔가요?Authentication 파라미터도 관련 ArgumentResolver에서 SpringSecurityContextHolder.getContext().getAuthentication()으로 꺼내오는 것이 아닌가요?
-
미해결스프링 시큐리티 OAuth2
Resource Owner Password grant 방식 문의
안녕하세요.oauth 를 사용하여 마이크로서비스 인증/인가를 구현하는 경우가 꽤 많은 것으로 알고 있습니다.구글링을 해보면 사용자가 최초 로그인 시, 인증서버에서 jwt 토큰을 발행해주고 사용자는 jwt 토큰을 사용하여 각 마이크로서비스의 api 를 요청하는 컨셉으로 확인됩니다.이때 각각의 마이크로서비스는 oauth 에서 리소스 서버의 역할로 볼 수 있고요.그래서 최초 토큰을 발행하는 인증 방식은 단순히 id/password 를 넘겨서 토큰을 발행받는 Resource Owner Password grant 방식을 사용했던 것 같은데요.하지만 강의에서 설명해 주신대로 최근 스프링 시큐리티의 Authorization 서버에서는 Resoure Owner Password grant 방식을 더 이상 사용할 수 없다고 하셨어요.그렇다면 마이크로서비스를 위해 Authorization 서버를 구현할 때, 토큰 발행을 어떤 방식으로 구현하는 것이 바람직할까요? 제3의 앱에서 로그인을 하는 것도 아닌데 Authorization code 방식을 적용할 필요가 있는 것인지 모르겠네요.
-
해결됨스프링 시큐리티 OAuth2
Ajax 인증시 인가코드가 발급 되지 않는 원인 문의
Spring Authorization 1.0,1 기반으로 개발을 하고 있습니다. 인가코드를 발급 할떄 FormLogin 기본 설정을 사용하면 인가코드가 발급이 되는데 Ajax 로 로그인을 하면 인가코드가 발급되지 않고 있습니다. 디버깅을 해보면 로그인인 후 OAuth2AuthorizationEndpointFilter 는 실행되는데 @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (!this.authorizationEndpointMatcher.matches(request)) { filterChain.doFilter(request, response); return; } try { Authentication authentication = this.authenticationConverter.convert(request); if (authentication instanceof AbstractAuthenticationToken) { ((AbstractAuthenticationToken) authentication) .setDetails(this.authenticationDetailsSource.buildDetails(request)); } Authentication authenticationResult = this.authenticationManager.authenticate(authentication); if (!authenticationResult.isAuthenticated()) { // If the Principal (Resource Owner) is not authenticated then // pass through the chain with the expectation that the authentication process // will commence via AuthenticationEntryPoint filterChain.doFilter(request, response); return; } if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) { if (this.logger.isTraceEnabled()) { this.logger.trace("Authorization consent is required"); } sendAuthorizationConsent(request, response, (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication, (OAuth2AuthorizationConsentAuthenticationToken) authenticationResult); return; } this.authenticationSuccessHandler.onAuthenticationSuccess( request, response, authenticationResult); } catch (OAuth2AuthenticationException ex) { if (this.logger.isTraceEnabled()) { this.logger.trace(LogMessage.format("Authorization request failed: %s", ex.getError()), ex); } this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex); } } FormLogin 적용시에는 authenticationResult의 principal 에 UsernamePasswordAuthenticationToken이 설정되어 인가 코드가 정상적으로 발급되는데 AjaxLogin 적용시에는 authenticationResult의 principal 에 AnonymousAuthenticationToken이 설정되어 인가 코드가 정상적으로 발급되지 않고 403 예외가 발생합니다.AuthenticationProvider 구현체에서는 정상적으로 토큰을 저장하고 있습니다. AuthenticationProvider 구현체 소스@Component @RequiredArgsConstructor public class CustomAuthenticationProvider implements AuthenticationProvider { private final CustomUserDetailsService customUserDetailsService; private final PasswordEncoder passwordEncoder; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { if(authentication == null){ throw new InternalAuthenticationServiceException("Authentication is null"); } LoginRequestDto loginRequestDto = (LoginRequestDto)authentication.getPrincipal(); String password = loginRequestDto.getLoginPassword(); UserAdapter userAdapter = (UserAdapter) customUserDetailsService.loadUserByLoinRequestDto(loginRequestDto); if (!passwordEncoder.matches(password, userAdapter.getCurrentUser().getLoginPwd())) { throw new BadCredentialsException("BadCredentialsException"); } CustomAuthenticationToken result = CustomAuthenticationToken.authenticated(userAdapter.getCurrentUser(), authentication.getCredentials(), userAdapter.getAuthorities()); result.setDetails(authentication.getDetails()); return result; } @Override public boolean supports(Class<?> authentication) { return CustomAuthenticationToken.class.isAssignableFrom(authentication); } } 이외 Custom 소스Spring Security 설정@EnableWebSecurity @RequiredArgsConstructor @Configuration public class DefaultSecurityConfig { private final CustomAuthenticationProvider customAuthenticationProvider; // private final CustomUserDetailsService customUserDetailsService; @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public CustomAuthenticationProcessingFilter customAuthenticationProcessingFilter() throws Exception { CustomAuthenticationProcessingFilter filter = new CustomAuthenticationProcessingFilter(); // filter.setAuthenticationManager(authenticationManager(null)); filter.setAuthenticationManager(new ProviderManager(customAuthenticationProvider)); // filter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler()); // filter.setAuthenticationFailureHandler(customAuthenticationFailureHandler()); return filter; } // @formatter:off @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorizeRequests ->authorizeRequests .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() .requestMatchers(new AntPathRequestMatcher("/")).permitAll() .requestMatchers(new AntPathRequestMatcher("/login/**")).permitAll() .requestMatchers("/api/login/**").permitAll() .requestMatchers("/api/registered-client/**").permitAll() .anyRequest().authenticated() ); http.addFilterBefore(customAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class); http.exceptionHandling(httpSecurityExceptionHandlingConfigurer -> httpSecurityExceptionHandlingConfigurer .authenticationEntryPoint(new CustomLoginAuthenticationEntryPoint()) .accessDeniedHandler(customAccessDeniedHandler()) ); // http.userDetailsService(customUserDetailsService); // http.formLogin(); http.csrf().disable(); return http.build(); } // @formatter:on @Bean public AccessDeniedHandler customAccessDeniedHandler() { return new CustomAccessDeniedHandler(); } @Bean public AuthenticationSuccessHandler customAuthenticationSuccessHandler() { return new CustomAuthenticationSuccessHandler(); } @Bean public AuthenticationFailureHandler customAuthenticationFailureHandler() { return new CustomAuthenticationFailureHandler(); } } Ajax 로그인 처리 필터 소스public class CustomAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { private final ObjectMapper objectMapper = new ObjectMapper(); private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/api/login", HttpMethod.POST.name()); public CustomAuthenticationProcessingFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } public CustomAuthenticationProcessingFilter(AuthenticationManager authenticationManager) { super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { if (!request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } LoginRequestDto loginRequestDto = objectMapper.readValue(request.getReader(), LoginRequestDto.class); if(StringUtils.isEmpty(loginRequestDto.getLoginId())||StringUtils.isEmpty(loginRequestDto.getLoginPassword())) { throw new IllegalStateException("Username or Password is empty"); } CustomAuthenticationToken authRequest = CustomAuthenticationToken.unauthenticated(loginRequestDto, loginRequestDto.getLoginPassword()); authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); return getAuthenticationManager().authenticate(authRequest); } } CustomAuthenticationToken 소스public class CustomAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; private Object credentials; public CustomAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } public CustomAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override } public static CustomAuthenticationToken unauthenticated(Object principal, Object credentials) { return new CustomAuthenticationToken(principal, credentials); } public static CustomAuthenticationToken authenticated(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { return new CustomAuthenticationToken(principal, credentials, authorities); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } }