当前位置 博文首页 > 竹根七:SpringCloud-OAuth2(四):改造篇

    竹根七:SpringCloud-OAuth2(四):改造篇

    作者:竹根七 时间:2021-06-30 18:32

    本片主要讲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,
                                                                     ClientDetailsService clientDetailsService,
                                                                     AuthenticationManager authenticationManager) {
            DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setSupportRefreshToken(true);
            defaultTokenServices.setTokenStore(tokenStore);
            defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
            defaultTokenServices.setAccessTokenValiditySeconds(3600);
            defaultTokenServices.setRefreshTokenValiditySeconds(7200);
            defaultTokenServices.setClientDetailsService(clientDetailsService);
            defaultTokenServices.setAuthenticationManager(authenticationManager);
    
            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

    bk
    下一篇:没有了