복붙노트

[SPRING] OAuth2 인증 서버 / 사용자 엔드 포인트에서 맞춤 사용자 정보를 얻는 방법

SPRING

OAuth2 인증 서버 / 사용자 엔드 포인트에서 맞춤 사용자 정보를 얻는 방법

@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. ==============================

    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. ==============================

    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. ==============================

    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. ==============================

    4.JWT 토큰을 사용할 수 있습니다. 토큰 자체에 추가 정보를 인코딩 할 수있는 대신 모든 사용자 정보가 저장되는 데이터 저장소가 필요하지 않습니다. 토큰이 디코드되면 앱에서 Principal 객체를 사용하여이 모든 정보에 액세스 할 수 있습니다.

    JWT 토큰을 사용할 수 있습니다. 토큰 자체에 추가 정보를 인코딩 할 수있는 대신 모든 사용자 정보가 저장되는 데이터 저장소가 필요하지 않습니다. 토큰이 디코드되면 앱에서 Principal 객체를 사용하여이 모든 정보에 액세스 할 수 있습니다.

  5. ==============================

    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. ==============================

    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;
    }
    
  7. 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