MrBird 5 năm trước cách đây
mục cha
commit
2ef7d81b8b

+ 19 - 1
febs-common/febs-common-security-starter/src/main/java/cc/mrbird/febs/common/security/starter/configure/FebsCloudSecurityAutoConfigure.java

@@ -9,12 +9,18 @@ import feign.RequestInterceptor;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Primary;
 import org.springframework.http.HttpHeaders;
+import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
 import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.oauth2.provider.expression.OAuth2MethodSecurityExpressionHandler;
+import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
 import org.springframework.util.Base64Utils;
 
 /**
@@ -23,7 +29,7 @@ import org.springframework.util.Base64Utils;
 @EnableGlobalMethodSecurity(prePostEnabled = true)
 @EnableConfigurationProperties(FebsCloudSecurityProperties.class)
 @ConditionalOnProperty(value = "febs.cloud.security.enable", havingValue = "true", matchIfMissing = true)
-public class FebsCloudSecurityAutoConfigure {
+public class FebsCloudSecurityAutoConfigure extends GlobalMethodSecurityConfiguration {
 
     @Bean
     @ConditionalOnMissingBean(name = "accessDeniedHandler")
@@ -48,6 +54,13 @@ public class FebsCloudSecurityAutoConfigure {
         return new FebsCloudSecurityInteceptorConfigure();
     }
 
+    @Bean
+    @Primary
+    @ConditionalOnMissingBean(DefaultTokenServices.class)
+    public FebsUserInfoTokenServices febsUserInfoTokenServices(ResourceServerProperties properties) {
+        return new FebsUserInfoTokenServices(properties.getUserInfoUri(), properties.getClientId());
+    }
+
     @Bean
     public RequestInterceptor oauth2FeignRequestInterceptor() {
         return requestTemplate -> {
@@ -59,4 +72,9 @@ public class FebsCloudSecurityAutoConfigure {
             }
         };
     }
+
+    @Override
+    protected MethodSecurityExpressionHandler createExpressionHandler() {
+        return new OAuth2MethodSecurityExpressionHandler();
+    }
 }

+ 145 - 0
febs-common/febs-common-security-starter/src/main/java/cc/mrbird/febs/common/security/starter/configure/FebsUserInfoTokenServices.java

@@ -0,0 +1,145 @@
+package cc.mrbird.febs.common.security.starter.configure;
+
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.fastjson.TypeReference;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.*;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.OAuth2RestOperations;
+import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
+import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
+import org.springframework.security.oauth2.common.OAuth2AccessToken;
+import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
+import org.springframework.security.oauth2.provider.OAuth2Authentication;
+import org.springframework.security.oauth2.provider.OAuth2Request;
+import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
+import org.springframework.util.Assert;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * 重写UserInfoTokenServices
+ * {@link UserInfoTokenServices#loadAuthentication(String)}
+ *
+ * @author MrBird
+ */
+public class FebsUserInfoTokenServices implements ResourceServerTokenServices {
+
+    protected final Log logger = LogFactory.getLog(this.getClass());
+
+    private final String userInfoEndpointUrl;
+    private final String clientId;
+    private OAuth2RestOperations restTemplate;
+    private String tokenType = "Bearer";
+    private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
+    private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor();
+
+    public FebsUserInfoTokenServices(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 = this.getMap(this.userInfoEndpointUrl, accessToken);
+        String error = "error";
+        if (map.containsKey(error)) {
+            if (this.logger.isDebugEnabled()) {
+                this.logger.debug("userinfo returned error: " + map.get(error));
+            }
+
+            throw new InvalidTokenException(accessToken);
+        } else {
+            return this.extractAuthentication(map);
+        }
+    }
+
+    private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
+        Object principal = this.getPrincipal(map);
+        List<GrantedAuthority> authorities = this.authoritiesExtractor.extractAuthorities(map);
+
+        String oauth2RequestString = JSONObject.toJSONString(map.get("oauth2Request"));
+        JSONObject oauth2Request = JSONObject.parseObject(oauth2RequestString);
+        TypeReference<Set<String>> setTypeReference = new TypeReference<Set<String>>() {
+        };
+
+        Map<String, String> requestParameters = JSONObject.parseObject(oauth2Request.getString("requestParameters"), new TypeReference<Map<String, String>>() {
+        });
+        boolean approved = oauth2Request.getBooleanValue("approved");
+        Set<String> scope = JSONObject.parseObject(oauth2Request.getString("scope"), setTypeReference);
+        Set<String> resourceIds = JSONObject.parseObject(oauth2Request.getString("resourceIds"), setTypeReference);
+        String redirectUri = oauth2Request.getString("redirectUri");
+        Set<String> responseTypes = JSONObject.parseObject(oauth2Request.getString("responseTypes"), setTypeReference);
+        Map<String, Serializable> extensions = JSONObject.parseObject(oauth2Request.getString("extensions"), new TypeReference<Map<String, Serializable>>() {
+        });
+
+        OAuth2Request request = new OAuth2Request(requestParameters, this.clientId, authorities, approved, scope, resourceIds, redirectUri, responseTypes, extensions);
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
+        token.setDetails(map);
+        return new OAuth2Authentication(request, token);
+    }
+
+    protected Object getPrincipal(Map<String, Object> map) {
+        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("all")
+    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 (Map) restTemplate.getForEntity(path, Map.class, new Object[0]).getBody();
+        } catch (Exception e) {
+            this.logger.warn("Could not fetch user details: " + e.getClass() + ", " + e.getMessage());
+            return Collections.singletonMap("error", "Could not fetch user details");
+        }
+    }
+}

+ 9 - 0
febs-server/febs-server-system/src/main/java/cc/mrbird/febs/server/system/controller/TestController.java

@@ -1,9 +1,12 @@
 package cc.mrbird.febs.server.system.controller;
 
+import cc.mrbird.febs.common.core.entity.FebsResponse;
 import cc.mrbird.febs.common.core.entity.system.TradeLog;
 import cc.mrbird.febs.server.system.service.ITradeLogService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RestController;
@@ -22,4 +25,10 @@ public class TestController {
     public void packageSend(@RequestBody TradeLog tradeLog) {
         this.tradeLogService.packageAndSend(tradeLog);
     }
+
+    @GetMapping("scope/test")
+    @PreAuthorize("#oauth2.hasScope('write')")
+    public FebsResponse testScope() {
+        return new FebsResponse().message("当前client包含write scope");
+    }
 }