SpringCloud-OAuth2(四):改造篇

竹根七發表於2021-06-30
本片主要講SpringCloud Oauth2篇的實戰改造,如動態許可權、整合JWT、更改預設url、資料庫載入client資訊等改造。
同時,這應該也是我這系列部落格的完結篇。

關於Oauth2,我也想說幾句:
如果真的要應用到企業級專案當中去,必須要進行充足的準備,因為預設的配置、UI等很多都不是通用的(不適用於各個公司),
但是這套框架還好留了很多適配方法等,因此其需要修改的配置、處理器、方法重寫等邏輯確實很多很多。

話不多說,正文開始。

承接前文:
SpringCloud-OAuth2(一):基礎篇
SpringCloud-OAuth2(二):實戰篇
SpringCloud-OAuth2(三):進階篇

1:動態許可權

常用的許可權校驗機制如以下幾點:

參考:https://zhuanlan.zhihu.com/p/144580287

型別 示例
硬編碼 如在介面上新增註解:@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
HttpSecurity動態增加 在資源服務配置類中配置,如:.authorizeRequests().anyRequest().authenticated()

如果出現使用者所擁有的許可權出現變化時,上述兩種是無法滿足的。


百度了幾天後網上確實有同學給出了不錯的示例,其工作機制如下:
drawingdrawing

1.1:AccessDecisionManager重寫

@Component
public class VipAccessDecisionManager implements AccessDecisionManager {

    /**
     * 決定當前使用者是否有許可權訪問該請求
     **/
    @Override
    public void decide(Authentication authentication, Object object,
                       Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
        for (ConfigAttribute configAttribute : configAttributes) {
            //將訪問所需資源或使用者擁有資源進行比對
            String needAuthority = configAttribute.getAttribute();
            if (needAuthority == null) {
                continue;
            }
            for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
                    return;
                }
            }
        }
        throw new AccessDeniedException("抱歉,您沒有訪問許可權");
    }

    @Override
    public boolean supports(ConfigAttribute configAttribute) {
        return true;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return true;
    }

}

1.2:FilterInvocationSecurityMetadataSource重寫

public class VipSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();

    private Set<PermRoleEntity> permRoleEntitySet;

    private final FilterInvocationSecurityMetadataSource superMetadataSource;
    private final VipSecurityOauthService vipSecurityOauthService;

    public VipSecurityMetadataSource(FilterInvocationSecurityMetadataSource superMetadataSource, 
                                     VipSecurityOauthService vipSecurityOauthService) {
        this.superMetadataSource = superMetadataSource;
        this.vipSecurityOauthService = vipSecurityOauthService;
    }

    private void loadPerms() {
        permRoleEntitySet = vipSecurityOauthService.loadPerms();
    }

    /**
     * 返回能訪問該請求的所有角色集合
     **/
    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        loadPerms();

        FilterInvocation fi = (FilterInvocation) object;
        String access_uri = fi.getRequestUrl();

        for (PermRoleEntity permRoleEntity : permRoleEntitySet) {
            if (ANT_PATH_MATCHER.match(permRoleEntity.getAccessUri(), access_uri))
                return permRoleEntity.getConfigAttributeList();
        }

        return superMetadataSource.getAttributes(object);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        loadPerms();

        Set<ConfigAttribute> attributeSet = new HashSet<>();
        permRoleEntitySet.stream().map(PermRoleEntity::getConfigAttributeList).forEach(attributeSet::addAll);
        return attributeSet;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}

1.3:動態許可權入口(自定義)

VipSecurityOauthService:許可權的動態載入api
這裡寫成靜態的,好參考。

@Component
public class VipSecurityOauthService {

    /**
     * 動態載入許可權-角色資訊
     **/
    public Set<PermRoleEntity> loadPerms() {
        Set<PermRoleEntity> permRoleEntitySet = new HashSet<>();
        permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/demo/admin").setConfigAttributeList(SecurityConfig.createList("admin")));
        permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/auth/**").setConfigAttributeList(SecurityConfig.createList("admin")));

        permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/demo/sp-admin").setConfigAttributeList(SecurityConfig.createList("sp_admin")));

        return permRoleEntitySet;
    }

}

PermRoleEntity:url和角色對應關係

@Data
@Accessors(chain = true)
public class PermRoleEntity {

    /**
     * 訪問的介面
     **/
    private String accessUri;

    /**
     * 可訪問該介面的角色集合
     **/
    private List<ConfigAttribute> configAttributeList;
}

1.4:ResourceServer進行配置

drawing

1.5:測試

獲取使用者角色為admin的token,進行介面訪問


①:使用者admin角色的使用者訪問admin管理的介面
drawing


①:使用者admin角色的使用者訪問sp-admin管理的介面
drawing

2:整合JWT

2.1:需要配置的bean

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices defaultTokenServices(TokenStore tokenStore,JwtAccessTokenConverter jwtAccessTokenConverter) {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setSupportRefreshToken(true);
        defaultTokenServices.setTokenStore(tokenStore);
        defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
        defaultTokenServices.setAccessTokenValiditySeconds(3600);
        defaultTokenServices.setAccessTokenValiditySeconds(7200);

        return defaultTokenServices;
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 簽名金鑰
        jwtAccessTokenConverter.setSigningKey("sign_key");
        // 驗證金鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner("sign_key"));
        return jwtAccessTokenConverter;
    }

2.2:認證服務配置更改

drawing

tokenservice: 建立token、重新整理token的地方。


獲取token
drawing

2.3:OAuth2 Client

在前文中提到 OAuth2 Client Service 處理請求的時候是無法識別token的,需要遠端提交給認證中心(OAuth2 Server)去識別token。
token換成JWT後客戶端服務(OAuth2 Client Service)也可以識別token了,均需做如下配置:

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
        return new JwtTokenStore(jwtAccessTokenConverter);
    }

    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 簽名金鑰
        jwtAccessTokenConverter.setSigningKey("sign_key");
        // 驗證金鑰
        jwtAccessTokenConverter.setVerifier(new MacSigner("sign_key"));
        return jwtAccessTokenConverter;
    }

前文中提到的這項配置也可以丟棄了。
drawing


訪問客戶端服務的介面測試:
drawing

3:更改預設url

3.1:框架提供的URL路徑:

預設url 作用
/oauth/authorize 授權端點
/oauth/token 令牌端點
/oauth/confirm_access 使用者批准授權的端點
/oauth/error 用於渲染授權伺服器的錯誤
/oauth/check_token 資源伺服器解碼access token
/oauth/check_token 當使用JWT的時候,暴露公鑰的端點

3.2:如何更改

可以按照下面這張方法進行重新轉交介面地址:
drawing

自定義處理介面程式碼:

@RestController
public class AuthController extends WebApiController {
    private static final Logger logger = LoggerFactory.getLogger(AuthController.class);

    private final ConsumerTokenServices consumerTokenServices;
    private final TokenEndpoint tokenEndpoint;
    private final AuthorizationEndpoint authorizationRequest;
    private final WhitelabelApprovalEndpoint whitelabelApprovalEndpoint;
    private final WhitelabelErrorEndpoint whitelabelErrorEndpoint;

    public AuthController(ConsumerTokenServices consumerTokenServices, TokenEndpoint tokenEndpoint,
                          AuthorizationEndpoint authorizationRequest, WhitelabelApprovalEndpoint whitelabelApprovalEndpoint,
                          WhitelabelErrorEndpoint whitelabelErrorEndpoint) {
        this.consumerTokenServices = consumerTokenServices;
        this.tokenEndpoint = tokenEndpoint;
        this.authorizationRequest = authorizationRequest;
        this.whitelabelApprovalEndpoint = whitelabelApprovalEndpoint;
        this.whitelabelErrorEndpoint = whitelabelErrorEndpoint;
    }

    /**
     * 自定義登入介面
     */
    @PostMapping(value = "/login")
    public ResponseEntity<String> login(@RequestBody UserLoginDto userLoginDto, Principal principal) throws HttpRequestMethodNotSupportedException {
        Map<String, String> mapDto = ObjectMapperUtil.str2Obj(userLoginDto, new TypeReference<Map<String, String>>() {
        });
        mapDto.put(GrantTypeConstants.GRANT_TYPE, GrantTypeConstants.PASSWORD);

        OAuth2AccessToken token;
        try {
            token = tokenEndpoint.postAccessToken(principal, mapDto).getBody();
            if (token == null) {
                throw new ServiceException("登入異常");
            }
        } catch (Exception e) {
            if (e instanceof InvalidGrantException) {
                throw new ServiceException("使用者名稱密碼錯誤");
            } else {
                throw e;
            }
        }
        return response(WebApiResponse.ok(AuthToken.build(token)));
    }
}

3.3:測試


drawing

4:資料庫載入client資訊

前文中以ProcessOn舉例的QQ第三方登入就提到ProcessOn會向QQ申請一個client_id(客戶端憑證),那麼QQ第三方登入配置申請的入口必須將資料存放在資料庫中,這樣才能做到動態的新增、刪除等,程式碼配置寫死是不可能的。
注意:需要引入資料來源、mysql驅動的依賴,並配置好資料來源。

4.1:schema.sql

因為官方給的sql是hql的,我用的mysql8,因此做了一些型別上的修改。

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for clientdetails
-- ----------------------------
DROP TABLE IF EXISTS `clientdetails`;
CREATE TABLE `clientdetails`  (
  `appId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `resourceIds` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `appSecret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `grantTypes` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `redirectUrl` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(0) NULL DEFAULT NULL,
  `refresh_token_validity` int(0) NULL DEFAULT NULL,
  `additionalInformation` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `autoApproveScopes` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`appId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of clientdetails
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_access_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_access_token`;
CREATE TABLE `oauth_access_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `token` binary(1) NULL DEFAULT NULL,
  `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authentication` binary(1) NULL DEFAULT NULL,
  `refresh_token` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_access_token
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_approvals
-- ----------------------------
DROP TABLE IF EXISTS `oauth_approvals`;
CREATE TABLE `oauth_approvals`  (
  `userId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `clientId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `expiresAt` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
  `lastModifiedAt` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_approvals
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(0) NULL DEFAULT NULL,
  `refresh_token_validity` int(0) NULL DEFAULT NULL,
  `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES ('admin', 'admin', 'tgMj02VG9dkpeKUN5lSWSsKotIt2yTIElkFcvsqLnwGOspNYhe+Teg==', 'test,all', 'authorization_code,client_credentials,password,implicit,refresh_token', 'http://www.baidu.com', 'admin', NULL, NULL, NULL, 'all');

-- ----------------------------
-- Table structure for oauth_client_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_token`;
CREATE TABLE `oauth_client_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `token` binary(1) NULL DEFAULT NULL,
  `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
  `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`authentication_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_client_token
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_code
-- ----------------------------
DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `authentication` binary(1) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_code
-- ----------------------------

-- ----------------------------
-- Table structure for oauth_refresh_token
-- ----------------------------
DROP TABLE IF EXISTS `oauth_refresh_token`;
CREATE TABLE `oauth_refresh_token`  (
  `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `token` binary(1) NULL DEFAULT NULL,
  `authentication` binary(1) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of oauth_refresh_token
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

在這個表中配置資料即可,參考程式碼靜態配置:
drawingdrawing

4.2:配置

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

        clients.jdbc(dataSource);

    }

做好以上配置,認證服務就可以從資料庫載入客戶端憑證資訊了。


本文參考部落格:https://www.cnblogs.com/cjsblog/p/9184173.html

相關文章