[SPRING] OAuth2 인증 서버 / 사용자 엔드 포인트에서 맞춤 사용자 정보를 얻는 방법
SPRINGOAuth2 인증 서버 / 사용자 엔드 포인트에서 맞춤 사용자 정보를 얻는 방법
@EnableResourceServer 주석으로 구성된 자원 서버가 있으며 다음과 같이 user-info-uri 매개 변수를 통해 권한 서버를 참조합니다.
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/user
Authorization server / user endpoint는 org.springframework.security.core.userdetails.User의 확장자를 반환합니다. 이메일:
{
"password":null,
"username":"myuser",
...
"email":"me@company.com"
}
일부 리소스 서버 엔드 포인트에 액세스 할 때마다 Spring은 인증 서버 / 사용자 엔드 포인트를 호출하여 장면 뒤에서 액세스 토큰을 검증하고 실제 사용자 정보 (예 : 이메일 정보 포함, Wireshark로 확인한 정보)를 가져옵니다.
따라서 문제는 인증 서버 / 사용자 엔드 포인트에 대한 명시 적 두 번째 호출없이이 사용자 정의 사용자 정보를 얻는 방법입니다. Spring은 권한 부여 후 리소스 서버에 로컬 어딘가에 저장할 것인가? 아니면 아무 것도 사용할 수 없다면이 종류의 사용자 정보 저장을 구현하는 가장 좋은 방법은 무엇입니까?
해결법
-
==============================
1.해결책은 맞춤형 UserInfoTokenServices를 구현하는 것입니다.
해결책은 맞춤형 UserInfoTokenServices를 구현하는 것입니다.
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java
커스텀 구현을 Bean으로 제공하면, 디폴트 구현 대신에 사용됩니다.
이 UserInfoTokenServices 내부에서 원하는대로 교장을 만들 수 있습니다.
이 UserInfoTokenServices는 권한 서버의 / usersendpoint 응답에서 UserDetails를 추출하는 데 사용됩니다. 에서 볼 수 있듯이
private Object getPrincipal(Map<String, Object> map) { for (String key : PRINCIPAL_KEYS) { if (map.containsKey(key)) { return map.get(key); } } return "unknown"; }
기본적으로 PRINCIPAL_KEYS에 지정된 속성 만 추출됩니다. 그리고 정확히 당신 문제입니다. 사용자 이름 또는 속성 이름이 무엇이든 그 이상을 추출해야합니다. 그래서 더 많은 열쇠를 찾으십시오.
private Object getPrincipal(Map<String, Object> map) { MyUserDetails myUserDetails = new myUserDetails(); for (String key : PRINCIPAL_KEYS) { if (map.containsKey(key)) { myUserDetails.setUserName(map.get(key)); } } if( map.containsKey("email") { myUserDetails.setEmail(map.get("email")); } //and so on.. return myUserDetails; }
배선:
@Autowired private ResourceServerProperties sso; @Bean public ResourceServerTokenServices myUserInfoTokenServices() { return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); }
!! Spring Boot 1.4 업데이트는 점점 더 쉬워졌습니다 !!
Spring Boot 1.4.0에서는 PrincipalExtractor가 소개되었습니다. 이 클래스는 사용자 정의 principal을 추출하기 위해 구현되어야합니다 (Spring Boot 1.4 Release Notes 참조).
-
==============================
2.모든 데이터가 이미 Principal 객체에 있으므로 두 번째 요청이 필요하지 않습니다. 필요한 것만 반환하십시오. Facebook 로그인을 위해 아래의 방법을 사용합니다.
모든 데이터가 이미 Principal 객체에 있으므로 두 번째 요청이 필요하지 않습니다. 필요한 것만 반환하십시오. Facebook 로그인을 위해 아래의 방법을 사용합니다.
@RequestMapping("/sso/user") @SuppressWarnings("unchecked") public Map<String, String> user(Principal principal) { if (principal != null) { OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal; Authentication authentication = oAuth2Authentication.getUserAuthentication(); Map<String, String> details = new LinkedHashMap<>(); details = (Map<String, String>) authentication.getDetails(); logger.info("details = " + details); // id, email, name, link etc. Map<String, String> map = new LinkedHashMap<>(); map.put("email", details.get("email")); return map; } return null; }
-
==============================
3.리소스 서버에서 다음과 같이 CustomPrincipal 클래스를 만들 수 있습니다.
리소스 서버에서 다음과 같이 CustomPrincipal 클래스를 만들 수 있습니다.
public class CustomPrincipal { public CustomPrincipal(){}; private String email; //Getters and Setters public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
다음과 같이 CustomUserInfoTokenServices를 구현하십시오.
public class CustomUserInfoTokenServices implements ResourceServerTokenServices { protected final Log logger = LogFactory.getLog(getClass()); private final String userInfoEndpointUrl; private final String clientId; private OAuth2RestOperations restTemplate; private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE; private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor(); private PrincipalExtractor principalExtractor = new CustomPrincipalExtractor(); public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) { this.userInfoEndpointUrl = userInfoEndpointUrl; this.clientId = clientId; } public void setTokenType(String tokenType) { this.tokenType = tokenType; } public void setRestTemplate(OAuth2RestOperations restTemplate) { this.restTemplate = restTemplate; } public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) { Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null"); this.authoritiesExtractor = authoritiesExtractor; } public void setPrincipalExtractor(PrincipalExtractor principalExtractor) { Assert.notNull(principalExtractor, "PrincipalExtractor must not be null"); this.principalExtractor = principalExtractor; } @Override public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException { Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken); if (map.containsKey("error")) { if (this.logger.isDebugEnabled()) { this.logger.debug("userinfo returned error: " + map.get("error")); } throw new InvalidTokenException(accessToken); } return extractAuthentication(map); } private OAuth2Authentication extractAuthentication(Map<String, Object> map) { Object principal = getPrincipal(map); List<GrantedAuthority> authorities = this.authoritiesExtractor .extractAuthorities(map); OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null, null, null, null, null); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( principal, "N/A", authorities); token.setDetails(map); return new OAuth2Authentication(request, token); } /** * Return the principal that should be used for the token. The default implementation * delegates to the {@link PrincipalExtractor}. * @param map the source map * @return the principal or {@literal "unknown"} */ protected Object getPrincipal(Map<String, Object> map) { CustomPrincipal customPrincipal = new CustomPrincipal(); if( map.containsKey("principal") ) { Map<String, Object> principalMap = (Map<String, Object>) map.get("principal"); customPrincipal.setEmail((String) principalMap.get("email")); } //and so on.. return customPrincipal; /* Object principal = this.principalExtractor.extractPrincipal(map); return (principal == null ? "unknown" : principal); */ } @Override public OAuth2AccessToken readAccessToken(String accessToken) { throw new UnsupportedOperationException("Not supported: read access token"); } @SuppressWarnings({ "unchecked" }) private Map<String, Object> getMap(String path, String accessToken) { if (this.logger.isDebugEnabled()) { this.logger.debug("Getting user info from: " + path); } try { OAuth2RestOperations restTemplate = this.restTemplate; if (restTemplate == null) { BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails(); resource.setClientId(this.clientId); restTemplate = new OAuth2RestTemplate(resource); } OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext() .getAccessToken(); if (existingToken == null || !accessToken.equals(existingToken.getValue())) { DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken( accessToken); token.setTokenType(this.tokenType); restTemplate.getOAuth2ClientContext().setAccessToken(token); } return restTemplate.getForEntity(path, Map.class).getBody(); } catch (Exception ex) { this.logger.warn("Could not fetch user details: " + ex.getClass() + ", " + ex.getMessage()); return Collections.<String, Object>singletonMap("error", "Could not fetch user details"); } } }
사용자 정의 PrincipalExtractor :
public class CustomPrincipalExtractor implements PrincipalExtractor { private static final String[] PRINCIPAL_KEYS = new String[] { "user", "username", "principal", "userid", "user_id", "login", "id", "name", "uuid", "email"}; @Override public Object extractPrincipal(Map<String, Object> map) { for (String key : PRINCIPAL_KEYS) { if (map.containsKey(key)) { return map.get(key); } } return null; } @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider(); daoAuthenticationProvider.setForcePrincipalAsString(false); return daoAuthenticationProvider; } }
@Configuration 파일에서 이와 같은 bean을 정의하십시오.
@Bean public ResourceServerTokenServices myUserInfoTokenServices() { return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); }
그리고 리소스 서버 구성 :
@Configuration public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer config) { config.tokenServices(myUserInfoTokenServices()); } //etc....
모든 것이 올바르게 설정되면 컨트롤러에서 다음과 같이 할 수 있습니다.
String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();
희망이 도움이됩니다.
-
==============================
4.JWT 토큰을 사용할 수 있습니다. 토큰 자체에 추가 정보를 인코딩 할 수있는 대신 모든 사용자 정보가 저장되는 데이터 저장소가 필요하지 않습니다. 토큰이 디코드되면 앱에서 Principal 객체를 사용하여이 모든 정보에 액세스 할 수 있습니다.
JWT 토큰을 사용할 수 있습니다. 토큰 자체에 추가 정보를 인코딩 할 수있는 대신 모든 사용자 정보가 저장되는 데이터 저장소가 필요하지 않습니다. 토큰이 디코드되면 앱에서 Principal 객체를 사용하여이 모든 정보에 액세스 할 수 있습니다.
-
==============================
5.userdetails 끝점에서 반환 된 JSON 객체의 Map 표현은 Principal을 나타내는 Authentication 객체에서 사용할 수 있습니다.
userdetails 끝점에서 반환 된 JSON 객체의 Map 표현은 Principal을 나타내는 Authentication 객체에서 사용할 수 있습니다.
Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();
로깅, 저장 또는 캐시 용으로 캡처하려는 경우 ApplicationListener를 구현하여 캡처하는 것이 좋습니다. 예 :
@Component public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> { private Logger log = LoggerFactory.getLogger(this.getClass()); @Override public void onApplicationEvent(AuthenticationSuccessEvent event) { Authentication auth = event.getAuthentication(); log.debug("Authentication class: "+auth.getClass().toString()); if(auth instanceof OAuth2Authentication){ OAuth2Authentication oauth2 = (OAuth2Authentication)auth; @SuppressWarnings("unchecked") Map<String, Object> details = (Map<String, Object>)oauth2.getUserAuthentication().getDetails(); log.info("User {} logged in: {}", oauth2.getName(), details); log.info("User {} has authorities {} ", oauth2.getName(), oauth2.getAuthorities()); } else { log.warn("User authenticated by a non OAuth2 mechanism. Class is "+auth.getClass()); } } }
JSON 또는 당국에서 보안 주체 추출을 사용자 정의하려는 경우 org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor 및 / org.springframework.boot.autoconfigure.security.oauth2를 구현할 수 있습니다. resource.AuthoritiesExtractor 각각.
그런 다음 @Configuration 클래스에서 구현을 bean으로 노출합니다.
@Bean public PrincipalExtractor merckPrincipalExtractor() { return new MyPrincipalExtractor(); } @Bean public AuthoritiesExtractor merckAuthoritiesExtractor() { return new MyAuthoritiesExtractor(); }
-
==============================
6.우리는 정적 인 SecurityContextHolder의 getContext 메소드에서 그것을 검색하고, 따라서 어느 곳에서나 검색 될 수 있습니다.
우리는 정적 인 SecurityContextHolder의 getContext 메소드에서 그것을 검색하고, 따라서 어느 곳에서나 검색 될 수 있습니다.
// this is userAuthentication's principal Map<?, ?> getUserAuthenticationFromSecurityContextHolder() { Map<?, ?> userAuthentication = new HashMap<>(); try { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (!(authentication instanceof OAuth2Authentication)) { return userAuthentication; } OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication; Authentication userauthentication = oauth2Authentication.getUserAuthentication(); if (userauthentication == null) { return userAuthentication; } Map<?, ?> details = (HashMap<?, ?>) userauthentication.getDetails(); //this effect in the new RW OAUTH2 userAuthentication Object principal = details.containsKey("principal") ? details.get("principal") : userAuthentication; //this should be effect in the common OAUTH2 userAuthentication if (!(principal instanceof Map)) { return userAuthentication; } userAuthentication = (Map<?, ?>) principal; } catch (Exception e) { logger.error("Got exception while trying to obtain user info from security context.", e); } return userAuthentication; }
from https://stackoverflow.com/questions/35056169/how-to-get-custom-user-info-from-oauth2-authorization-server-user-endpoint by cc-by-sa and MIT license
'SPRING' 카테고리의 다른 글
[SPRING] 스프링 부트의 핫 스와핑 (0) | 2018.12.15 |
---|---|
[SPRING] 스프링 웹, 보안 + web.xml + mvc 디스패처 + Bean이 두 번 생성됨 (0) | 2018.12.15 |
[SPRING] 요소 'beans'의 선언을 찾을 수 없습니다. (0) | 2018.12.14 |
[SPRING] spring-context.xml의 위치 (0) | 2018.12.14 |
[SPRING] json 콘텐츠를 반환하지 않는 봄 mvc - 오류 406 (0) | 2018.12.14 |