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_type为group_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认证。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
实现OAuth2自定义令牌增强,您可以遵循以下步骤: 1. 创建一个自定义TokenEnhancer类,实现Spring Security的TokenEnhancer接口。这个类将负责生成或修改令牌中的附加信息。 2. 在您的授权服务器配置类中,通过重写configure(AuthorizationServerEndpointsConfigurer endpoints)方法,将自定义TokenEnhancer添加到TokenEnhancerChain中。 ```java @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private TokenEnhancer tokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) { TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); enhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer)); endpoints.tokenEnhancer(enhancerChain); } // 其他配置... } ``` 3. 在自定义TokenEnhancer类中,实现enhance(TokenRequest tokenRequest, OAuth2AccessToken accessToken)方法,根据需要修改或添加令牌的附加信息。 ```java @Component public class CustomTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, TokenRequest tokenRequest) { // 获取当前用户信息或其他需要的信息 // 修改或添加令牌的附加信息 Map<String, Object> additionalInfo = new HashMap<>(); additionalInfo.put("custom_key", "custom_value"); ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo); return accessToken; } } ``` 4. 在授权服务器配置类中,通过重写configure(ClientDetailsServiceConfigurer clients)方法,设置您的客户端详情服务,包括客户端ID、密钥、授权类型等。 ```java @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { // 其他配置... @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client_id") .secret("client_secret") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("scope") .accessTokenValiditySeconds(3600); } } ``` 这样,当您的客户端通过授权服务器进行认证和授权时,自定义TokenEnhancer将会被调用,从而实现自定义令牌增强。请根据您的具体需求,对自定义TokenEnhancer进行进一步的实现

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值