oauth2自定义granter与provider实现自定义身份认证

Posted slxz001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了oauth2自定义granter与provider实现自定义身份认证相关的知识,希望对你有一定的参考价值。

Oauth2自定义Granter与Provider实现自定义身份验证

需求描述

公司的软件开发平台基于Oauth2实现身份认证,但今年某地区用户提出特殊需求——他们的系统必须使用集团公司认证平台登录,而后利用返回的token进入我公司系统。

为了以最小代价实现该需求,我们决定自定义一个认证模式,解析用户传入的token以获得员工编号,进而发放我方token以便应用端后续调用资源服务。

实现思路

Oauth提供几种基本的认证模式,如密码模式、客户端模式、授权码模式和几乎不用的简易模式。同时,还提供了认证模式的扩展机制,以便于我们在遇到特殊情况时根据自己的需求来完成身份验证。因此,我们决定实现一个自定义的集团公司凭据验证模式来校验用户身份信息。

自定义Token

在Oauth中,我们最常见的Token类型非要数UsernamePasswordAuthenticationToken不可了,所有基于用户名和密码进行验证的模式,最终都要返回一个UsernamePasswordAuthenticationToken的实例。但我们的需求中没有用户名和密码,所以我们自定义一个集团公司认证票据GroupCompanyAuthenticationToken

public class GroupCompanyAuthenticationToken extends AbstractAuthenticationToken 

    private final Object principal;

	/** 认证未通过时的初始化方法 */
    public GroupCompanyAuthenticationToken(String token)
        super(null);
        this.principal = token;
        setAuthenticated(false);
    

	/** 认证通过后的初始化方法 */
    public GroupCompanyAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities)
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true);
    

    @Override
    public Object getCredentials() 
        return null;
    

    @Override
    public Object getPrincipal() 
        return this.principal;
    

    @Override
    public void setAuthenticated(boolean isAuthenticated) 
        if (isAuthenticated) 
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        
        super.setAuthenticated(false);
    

可能有一些小伙伴不知道这个东西是干嘛使的。我也没法给你一个非常标准正规的说法,只能按我的理解来简单解释一下:

首先,我们自定义了一个类,集成自AbstractAuthenticationToken,因此它拥有AbstractAuthenticationToken 的特性,可以被oauth识别和调用。

当认证开始进行的时候,利用第一个构造方法来创建一个实例,此时传入一个token值作为principal,注意此时构造方法中setAuthenticated(false)表示当前未通过认证。

当我们确认传入的token是有效的,再调用第二个构造方法来创建一个新实例,此时setAuthenticated(true)表示已通过认证。

这两次创建实例的操作分别位于Granter和Provider,在后面会看到。

oauth通过读取该实例的属性来判断是否通过认证,是否可以颁发令牌。

自定义Granter

什么是Granter?说白了,它就是授权模式。在oauth中, ResourceOwnerPasswordTokenGranter定义出了我们常见的密码模式,AuthorizationCodeTokenGranter定义出了授权码模式。在此我们定义一个GroupCompanyTokenGranter来实现我们自己的集团公司凭据认证模式。

public class GroupCompanyTokenGranter extends AbstractTokenGranter 

	//我们授权模式注册到oauth中的名称
    private static final String GRANT_TYPE = "group_token_authentication";
 
    private final AuthenticationManager authenticationManager;
 
    public GroupCompanyTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices
            , ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) 
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    
 
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) 
        Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
        //接收传入的参数
        String token = parameters.get("group_company_token");
        //利用第一个构造方法来创建一个实例
        Authentication userAuth = new GroupCompanyAuthenticationToken(token);
        //把用户传入的参数交给自定义的Token
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);
        userAuth = authenticationManager.authenticate(userAuth);
        if (userAuth == null || !userAuth.isAuthenticated()) 
            throw new InvalidGrantException("Could not authenticate group company token: " + token);
        
 
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    

这段代码比较简单,说白了,当用户进行身份校验时,如果传入的grant_typegroup_token_authentication,那么则自动进入这段逻辑创建GroupCompanyAuthenticationToken对象的实例,并将request接收到的参数传入。

自定义Provider

oauth接收到了你传入的grant_type,并把你的请求转发到了你自己的Granter,但谁来进行真正的用户信息合法性校验呢?自然就是接下来要用到的Provider。我们新建一个GroupCompanyAuthenticationProvider类,继承oauth的AuthenticationProvider,重写其中的部分代码即可。

@Setter
public class GroupCompanyAuthenticationProvider implements AuthenticationProvider 

	//我们自己获取用户信息的服务
    private IUserService userService;

	//spring security提供的UserDetailsService
    private UserDetailsService userDetailsService;

	//集团公司认证平台提供的获取用户信息接口地址
    private String getUserInfoUri;

    private RestTemplate restTemplate;

    public GroupCompanyAuthenticationProvider()
        this.restTemplate = new RestTemplate();
    

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException 
        //此时principal是传进来的token,根据token查询用户编号
        Object principal = authentication.getPrincipal();
        if (principal == null || "".equals(principal.toString()))
            throw new PrincipalNotFoundException("未传入principal。");
        

		//使用RestTemplate调用集团公司接口,利用他们下发的token来获取用户信息
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("token", principal.toString());

        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(getUserInfoUri);
        URI uri = builder.queryParams(params).build().encode().toUri();

        ResponseEntity<Map> responseEntity = restTemplate.getForEntity(uri, Map.class);
        Map<String, String> responseData = responseEntity.getBody();
        if (responseData.get("code") != null && "500".equals(responseData.get("code")))
        	//懒得自定义异常了。抛个密码错误给前面自己体会……
            throw new BadCredentialsException("Invalid Token");
        

        //获取员工编号
        String userNo = responseData.get("userName");

		//UserDetailsService有一个默认的loadUserByUsername方法,我自己写了一个loadUserByUserNo方法,表示利用员工编号获取用户信息。返回的UserDetails是我们最终需要的那个Principal
        UserDetails userDetails = ((UserDetailsServiceImpl)userDetailsService).loadUserByUserNo(userNo);
        //注意,这里用到了自定义Token的第二个构造方法,它将告诉oauth此时已经通过认证。
        //但是,如果UserDetails对象为null,后面的逻辑将抛出异常,认证还是过不去。
        GroupCompanyAuthenticationToken authenticationToken = new GroupCompanyAuthenticationToken(userDetails, userDetails.getAuthorities());

		//返回一个通过认证的自定义token对象,大功告成
        return authenticationToken;
    

	/**
		这个方法用于判断,用户发送过来的认证请求是否适用于当前provider来处理。
		参考上面自定义Granter的代码,它创建了一个GroupCompanyAuthenticationToken的实例,因此这里会返回true。
	*/
    @Override
    public boolean supports(Class<?> aClass) 
        return GroupCompanyAuthenticationToken.class.isAssignableFrom(aClass);
    


此时,我们自定义认证过程的大部分工作都已经完成了。接下来我们需要把自己写的这些东西告诉oauth,否则它怎么知道多了这些操作呢?

配置Provider

在SecurityConfig中,初始化provider,代码如下:

@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter 
	@Autowired
    private UserDetailsService userDetailsService;

	@Autowired
    private IUserService userService;

	@Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
    	//实例化provider,把需要的东西set进去
        GroupCompanyAuthenticationProvider provider = new GroupCompanyAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setUserService(userService);
        provider.setGetUserInfoUri('http://127.0.0.1/getUserUri');
        auth.authenticationProvider(provider);
    

配置自定义Granter,代码如下

@Configuration
@EnableAuthorizationServer
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = 28800)
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter 

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception 
    	//灵魂在这里
        endpoints.tokenGranter(new CompositeTokenGranter(initGranters(endpoints)));
    

	//你要一股脑把所有自定义的和原有的认证模式都加进去,除非你确保其他的模式永远都用不到
    private List<TokenGranter> initGranters(AuthorizationServerEndpointsConfigurer endpoints) 
        AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
        ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();
        OAuth2RequestFactory oAuth2RequestFactory = endpoints.getOAuth2RequestFactory();
        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();

        //自定义Granter
        List<TokenGranter> customTokenGranters = new ArrayList<>();
        customTokenGranters.add(new GroupCompanyAuthenticationGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));
        //添加密码模式
        customTokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));
        //刷新模式
        customTokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
        //简易模式
        customTokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
        //客户端模式
        customTokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
        //授权码模式
        customTokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, oAuth2RequestFactory));
        return customTokenGranters;
    


修改Client_details表

最后,你还需要为你的应用客户端增加自定义认证模式的支持,否则还是用不了。
在数据库中找到OAUTH_CLIENT_DETAILS表,在AUTHORIZED_GRANT_TYPES中增加我们自定义的授权模式:

authorization_code,password,refresh_token
修改为
authorization_code,password,refresh_token,group_token_authentication

测试

postman我就不截图了。
调用 http://localhost/oauth/token,传参
grant_type: group_token_authentication
group_company_token: 获取到的jwt token

但这里有个前提,还是要利用clientId和clientSecret生成Basic token放到header中,否则过不了Basic认证。

Spring security oauth2 - 从 OAuth2 主体获取自定义数据

【中文标题】Spring security oauth2 - 从 OAuth2 主体获取自定义数据【英文标题】:Spring security oauth2 - getting custom data from OAuth2 principal 【发布时间】:2013-04-17 00:00:42 【问题描述】:

我有一个使用 Spring 安全性并具有用户(用户名和密码)和标准表单身份验证的站点。我允许用户生成与他们的帐户相关联的客户端 ID 和客户端密码,以便与 OAuth2 安全的 rest API 一起使用。

我为 API 使用单独的客户端 ID 和客户端密码,而不是用户名和密码,因此用户可以在不破坏 API 凭据、设置特定范围、禁用 API 访问等的情况下更改密码等。

我使用 spring-security-oauth2(提供者)来保护其余的 API,并且我允许客户端凭据流。我已经为 api 设置了客户端身份验证,以便检查客户端 ID 和密码。

从一个单独的应用程序中,我使用客户端 ID 和客户端密码来检索访问令牌并开始将其与 api 一起使用。在大多数情况下,我使用简单的 @PreAuthorize 表达式,通常基于客户端角色和范围,并且这些表达式似乎可以正常工作。

以上一切似乎都可以正常工作

但是...我现在有一些 API 端点,但是我需要根据来自底层用户的一些详细信息来实现一些更复杂的规则 - 在本例中是生成客户端 ID 和密码的用户。

作为一个简单的例子,考虑一个消息传递应用程序,其中我有一个端点,允许用户发布可以变化的特定类型的新“消息”。我有每个用户和每种类型的允许收件人表,我想检查已发布消息的收件人是否与该类型的允许收件人匹配。 (用户允许的收件人数据通常不大,并且很少更改,因此我很乐意在生成访问令牌时存储它的副本)

我在这些端点获得了主体,我看到它是 OAuth2Authentication 的一个实例,其中包含:

userAuthentication - null - 这对于客户端凭据流来说似乎是可以预期的。 clientAuthentication 已填充,authorizationParameters 包含授权类型和 client_id

我想我可以使用 clientAuthentication.authorizationParameters 中的客户端 ID 来查找用户和我需要的详细信息,但这将是每个 api 调用的一些查询,这似乎没有意义。

我猜想 Spring OAuth2 库中有一个不错的地方/方式,在授予访问令牌的同时,我可以添加一些额外的细节,以便稍后我可以从 OAuth2Authentication(主体)对象(或扩展它的东西? )

或者是否有更好的方法完全解决此类问题?

谢谢!

【问题讨论】:

你看过 JWT(JSON 网络令牌)吗?它几乎是一个包含所有有用信息的令牌。 【参考方案1】:

您可以覆盖令牌创建类,以便令牌本身包含用户名。 (可能使用 client_secret 加密)。 这样,您可以从令牌本身获取与令牌相关的用户,而无需额外的数据库访问。 你应该覆盖

<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">

使用您自己的课程,或使用TokenEnhancer

【讨论】:

谢谢,最初听起来很有希望。我会检查更多,并在第二天进行一些测试。我还没有找到一篇文章/帖子/示例,所以如果你知道,请告诉我:) 嗨,我检查了一下,似乎 TokenEnhancer 有助于“增强”访问令牌响应和/或添加一些额外的信息。虽然那不是我想要的。应该不需要将额外的数据发送回客户端。虽然我理解您尝试将用户名填充到令牌中的想法(我通常不需要它),但在我看来,必须有更好的方法使令牌等保持不变并且只发生在服务器端。 您可以在使用ClientDetails.addAdditionalInformation 注册每个客户时将所需的信息添加到您的客户详细信息中,然后您可以使用您获得的clientId 检索此信息。 (这可能需要每个请求一个查询,这似乎是合理的) 好点。是的,检索很遗憾,我想避免。看起来扩展点/字段还没有。我有自己的暗示。 TokenStore,ClientDetail,ClientDetailService,但似乎我必须修改太多才能创建完整的链。【参考方案2】:

在我看来,正确的方法是使用 oauth2 “密码”流程而不是客户端凭据。您可以创建一个通用的客户端 ID 和客户端密码,并将它们提供给您的所有用户。每个用户都可以使用这些客户详细信息以及他自己的用户名和密码来请求他自己的令牌。 这样,每个令牌都将连接到一个 UserDetails 对象,这将解决您的问题。 你可以在这里看到这是如何完成的:https://labs.hybris.com/2012/06/18/trying-out-oauth2-via-curl/

【讨论】:

感谢您的想法,但使用客户端凭据的目的是使 API 通常独立于用户。只有在少数情况下(少数)我有一些更复杂的规则。我已经更新了问题,以说明客户端凭据的原因。 此处的链接已损坏。你能带来这个特定答案的相关sn-p吗? 链接又被破坏了。【参考方案3】:

为什么您不使用可以在客户端详细信息中定义的范围,然后使用 ScopeVoter 验证此令牌是否具有访问此资源的适当范围。

请查看我的演示了解更多详情

https://github.com/bassemZohdy/Spring_REST_OAuth_Demo

更新 1: 我没有回答您的问题“从 OAuth2 主体获取自定义数据”的第 2 部分,您可以在授权服务器上添加休息服务,使用 OAuth 令牌访问该服务,为您提供有关此令牌用户的所需信息,因此您不必关心“OAuth2 主体”,您也将授权服务器作为资源服务器处理。

【讨论】:

Thx,我已经在使用一些简单的范围,但避免了该解决方案,因为我担心范围的数量会爆炸性地达到我想要的规则。我会用一个例子来更新这个问题。 在另一个解决方案中,我使用每组资源的范围,我认为范围的数量没有限制,您也可以将范围与角色混合使用。 谢谢,是的,我已经使用了这么一点。仅就我提到的情况而言,从用户那里制定范围然后对其进行处理以解释它们可能会令人费解:-)我希望有更好的方法。

以上是关于oauth2自定义granter与provider实现自定义身份认证的主要内容,如果未能解决你的问题,请参考以下文章

django-oauth2-provider与自定义用户模型?

如何在 Flask-Appbuilder 中为 OAuth2.0 使用自定义提供程序 [keycloak]?

Spring security oauth2 - 从 OAuth2 主体获取自定义数据

angularJs 自定义服务 provide 与 factory 的区别

Google Cloud Endpoints Security (OAuth2) 和自定义用户架构

微信公众号用户与网站用户的绑定-自定义菜单获取Openid