接入简单,支持多租户,可配置,低代码式的快速实现一套自己的认证鉴权逻辑

包含用户管理、部门管理、角色管理、权限管理,未来还可以支持更多模块,比如数据字典管理、...

预计节省每个应用相关模块研发时间5人天

对于oauth2.0协议来说,有几个概念

  1. 授权服务器
  2. 客户端
  3. 资源服务器

这偏文章主要讲客户端sdk实现,资源服务器主要就是统一存放用户信息的,授权服务器使用springsecurity官方的进行小定制。

首先使用springboot的自动配置功能,主要配置类有SecurityConfig,通过编写META-INF/spring.factories来实现自动加载配置bean


@Configuration(proxyBeanMethods = false)
@EnableWebSecurity
@EnableConfigurationProperties(OauthProperties.class)
public class SecurityConfig {

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http,
                                                         IntrospectorFilter filter,
                                                         CustomLoginUrlAuthenticationEntryPoint entryPoint,
                                                         ClientRegistrationRepository clientRegistrationRepository,
                                                         CustomRequestCache customRequestCache) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = http.authorizeRequests();
        if(!CollectionUtils.isEmpty(oauthProperties.getIgnoreUris())) {
            expressionInterceptUrlRegistry.requestMatchers(oauthProperties.getIgnoreUris().stream().map(AntPathRequestMatcher::new).toArray(RequestMatcher[]::new)).permitAll();
        }
        expressionInterceptUrlRegistry.anyRequest().authenticated().and()
                .oauth2Login().authorizationEndpoint().authorizationRequestResolver(authorizationRequestResolver(clientRegistrationRepository))
                .and()
                // .defaultSuccessUrl("/", true);
                .successHandler(authenticationSuccessHandler());
            http.oauth2Client().and()
                .cors().and()
                .exceptionHandling().authenticationEntryPoint(entryPoint).and()
                    .csrf().disable()
                    .requestCache(requestCacheCustomizer->{
                        requestCacheCustomizer.requestCache(customRequestCache.getRequestCache(http));
                    });
        http.addFilterBefore(filter, AnonymousAuthenticationFilter.class);
        return http.build();
    }

    @Bean
    public CustomRequestCache customRequestCache() {
        return new CustomRequestCache();
    };

    private AuthenticationSuccessHandler authenticationSuccessHandler() {
        SavedRequestAwareAuthenticationSuccessHandler successHandler = new CustomSavedRequestAwareAuthenticationSuccessHandler();
        successHandler.setUseReferer(true);
        return successHandler;
    }

    private OAuth2AuthorizationRequestResolver authorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
        DefaultOAuth2AuthorizationRequestResolver authorizationRequestResolver =
                new DefaultOAuth2AuthorizationRequestResolver(
                        clientRegistrationRepository, "/oauth2/authorization");
        authorizationRequestResolver.setAuthorizationRequestCustomizer(
                authorizationRequestCustomizer());

        return  authorizationRequestResolver;
    }

    private Consumer<OAuth2AuthorizationRequest.Builder> authorizationRequestCustomizer() {
        return customizer -> customizer
                .additionalParameters(params -> params.put("tenantId", oauthProperties.getClientId()));
    }

    @Bean
    public CustomLoginUrlAuthenticationEntryPoint customLoginUrlAuthenticationEntryPoint(ObjectMapper objectMapper) {
        CustomLoginUrlAuthenticationEntryPoint customLoginUrlAuthenticationEntryPoint = new CustomLoginUrlAuthenticationEntryPoint("/oauth2/authorization/auth_server");
        customLoginUrlAuthenticationEntryPoint.setObjectMapper(objectMapper);
        customLoginUrlAuthenticationEntryPoint.setOauthProperties(oauthProperties);
        return customLoginUrlAuthenticationEntryPoint;
    };

    @Bean
    public IntrospectorFilter introspectorFilter(OpaqueTokenIntrospector opaqueTokenIntrospector,
                                                 OAuth2AuthorizedClientService oAuth2AuthorizedClientService) {
        IntrospectorFilter introspectorFilter = new IntrospectorFilter();
        introspectorFilter.setOpaqueTokenIntrospector(opaqueTokenIntrospector);
        introspectorFilter.setoAuth2AuthorizedClientService(oAuth2AuthorizedClientService);
        return introspectorFilter;
    }

    @Bean
    public NimbusOpaqueTokenIntrospector opaqueTokenIntrospector() {
        String introspectUri = oauthProperties.getIssuerUri() + "/oauth2/introspect";
        OAuth2ResourceServerProperties.Opaquetoken opaqueToken = new OAuth2ResourceServerProperties.Opaquetoken();
        opaqueToken.setIntrospectionUri(introspectUri);
        opaqueToken.setClientId(oauthProperties.getClientId());
        opaqueToken.setClientSecret(oauthProperties.getClientSecret());
        return new NimbusOpaqueTokenIntrospector(opaqueToken.getIntrospectionUri(), opaqueToken.getClientId(),
                opaqueToken.getClientSecret());
    }

    //@RefreshScope 动态刷新
    @Bean
    @ConditionalOnMissingBean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(this.clientRegistration());
    }

    @Bean
    public OidcUserService oidcUserService(DefaultOAuth2UserService defaultOAuth2UserService) {
        CustomOidcUserService oidcUserService = new CustomOidcUserService();
        oidcUserService.setAccessibleScopes0(scopes);
        oidcUserService.setOauth2UserService0(defaultOAuth2UserService);
        return oidcUserService;
    }

    @ConditionalOnMissingBean
    @Bean
    public ParameterizedTypeReference<Result<OpenAppUser<Map<String, Object>>>> userInfoTypeReference () {
        return new ParameterizedTypeReference<Result<OpenAppUser<Map<String, Object>>>>() {};
    }

    @Bean
    public DefaultOAuth2UserService defaultOAuth2UserService(ParameterizedTypeReference userInfoTypeReference) {
        CustomDefaultOAuth2UserService defaultOAuth2UserService = new CustomDefaultOAuth2UserService();
        defaultOAuth2UserService.setPARAMETERIZED_RESPONSE_TYPE(userInfoTypeReference);
        return defaultOAuth2UserService;
    }

    @Autowired
    OauthProperties oauthProperties;

    private Set<String> scopes = new HashSet(){
        {
            //add("openid");
            add("user");
        }
    };

    private ClientRegistration clientRegistration() {
        if (oauthProperties.getScopes()!=null) scopes.addAll(oauthProperties.getScopes());
        ClientRegistration.Builder auth_server = ClientRegistrations.fromIssuerLocation(oauthProperties.getIssuerUri()).registrationId("auth_server");
        return auth_server.clientId(oauthProperties.getClientId())
                .clientSecret(oauthProperties.getClientSecret())
                .clientName(oauthProperties.getClientName())
                //.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                //.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .redirectUri((StringUtils.hasText(oauthProperties.getServerUrl())?oauthProperties.getServerUrl():"{baseUrl}")+"/login/oauth2/code/{registrationId}")
                .scope(scopes)
                .userInfoUri(oauthProperties.getIssuerUri()+"/client/userinfo")
                .userNameAttributeName(IdTokenClaimNames.SUB)
                .build();
    }

}

springsecurity对于oauth2.0的支持还是比较完善的,但是不完全符合我们对认证授权系统的要求,所以我这里做了很多自定义,可以说把大部分的拦截器都实现了一些,发现security的代码质量也不是很高,很多地方没有做到扩展性,只能重写类。

然后需要接入应用进行配置,所以定义了一个properties


@ConfigurationProperties(prefix = "oauth")
@Data
public class OauthProperties {

    /**
     * 客户端id
     */
    private String clientId;

    /**
     * 客户端密钥
     */
    private String clientSecret;

    /**
     * 客户端名称
     */
    private String clientName;

    /**
     * 接入授权服务器配置uri
     */
    private String issuerUri;

    /**
     * 权限 默认有openid,user
     */
    private Collection<String> scopes;

    /**
     * 过滤url白名单
     */
    private Collection<String> ignoreUris;

    /**
     * 服务部署地址(可空,默认读取数据库对应的回调地址)
     */
    private String serverUrl;

}

这样接入应用就可以通过配置文件自定义了。

03-05 15:24