Oauth2拓展授权方式的原理分析和设计

需求:

  1. 基于手机+密码+图形验证码
  2. 基于邮箱+密码+图形验证码
  3. 基于用户名+密码+图形验证码
  4. 基于手机+手机验证码

其实无论哪个需求,oauth2自带的授权方式都没办法实现。

看了网上很多都是添加SpringSecurity的过滤器来拦截的,但总感觉这样就脱离了Oauth2,且我这边是通过自定义的登录接口然后通过RestTemplate调用/oauth/token接口申请token,因此不方便通过过滤器直接拦截登录的URL。

较好的方式是拓展Oauth2的授权方式。

首先需要了解调用申请令牌的接口的原理:

即:/oauth/token?username=xxx&password=xxx&grant_type=xxx

//TokenEndpoint.postAccessToken
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException ;

Filter拦截

在请求落入到TokenEndpoint.postAccessToken前,会经过两个拦截器ClientCredentialsTokenEndpointFilterBasicAuthenticationFilter

这两个拦截器会对请求的客户端id和客户端密码进行认证。

ClientCredentialsTokenEndpointFilterallowFormAuthenticationForClients()后才会生效,从请求中获取客户端id和客户端密码进行校验;否则会在BasicAuthenticationFilter中,从请求头中获取Authorization 参数来进行客户端认证,其中Authorization中保存的是Basic Base64.encode("clientId:clientPwd")的编码串。

postAccessToken

Filter通过后就会进入postAccessToken方法进行token申请:

该方法中首先会拿到客户端id,根据客户端id从数据库查询客户端信息,对其scope进行校验,然后对请求参数中的

grant_type等参数进行校验,最后会进行token的申请:

		OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);

这里会调用TokenGranter对象的grant方法进行申请。默认的CompositeTokenGranter中会保存所有的TokenGranter集合:

public class CompositeTokenGranter implements TokenGranter {

	private final List<TokenGranter> tokenGranters;

	public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
		this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
	}
	
	public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
		for (TokenGranter granter : tokenGranters) {
			OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
			if (grant!=null) {
				return grant;
			}
		}
		return null;
	}
	
	public void addTokenGranter(TokenGranter tokenGranter) {
		if (tokenGranter == null) {
			throw new IllegalArgumentException("Token granter is null");
		}
		tokenGranters.add(tokenGranter);
	}

}

然后就会在grant方法中,遍历集合中的TokenGranter的grant方法,该方法会进入其抽象父类AbstractTokenGranter的grant中:

public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
 
   //这里就是判断当前TokenGranter(实现类会传入this.grantType)的授权类型是否和请求的授权类型匹配,如果匹配就会往下走
		if (!this.grantType.equals(grantType)) {
			return null;
		}
		
		String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);

		if (logger.isDebugEnabled()) {
			logger.debug("Getting access token for: " + clientId);
		}

		return getAccessToken(client, tokenRequest);

	}

OK,先到这里,等下再接着这里分析。

再来看保存所有授权类型的Granter集合:tokenGranters

默认注入的TokenEndpoint中会设置默认的tokenGranter:

	@Bean
	public TokenEndpoint tokenEndpoint() throws Exception {
		TokenEndpoint tokenEndpoint = new TokenEndpoint();
		...
		tokenEndpoint.setTokenGranter(tokenGranter());
		....
		return tokenEndpoint;
	}

该方法实现如下:

	private TokenGranter tokenGranter() {
		if (tokenGranter == null) {
			tokenGranter = new TokenGranter() {
				private CompositeTokenGranter delegate;

				@Override
				public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
					if (delegate == null) {
						delegate = new CompositeTokenGranter(getDefaultTokenGranters());
					}
					return delegate.grant(grantType, tokenRequest);
				}
			};
		}
		return tokenGranter;
	}

内部委托了CompositeTokenGranter,在其构造中传入默认的TokenGranter

	private List<TokenGranter> getDefaultTokenGranters() {
		ClientDetailsService clientDetails = clientDetailsService();
		AuthorizationServerTokenServices tokenServices = tokenServices();
		AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
		OAuth2RequestFactory requestFactory = requestFactory();

		List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
		tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
				requestFactory));
		tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
		ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
		tokenGranters.add(implicit);
		tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
		if (authenticationManager != null) {
			tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
					clientDetails, requestFactory));
		}
		return tokenGranters;
	}

这五个XXXGranter(即TokenGranter的实现类)就对应了Oauth2的五种授权方式。

其实到这里,就明了了,只要扩展TokenGranter就可以实现自定义的授权方式。

这里以拓展手机+密码+验证码的授权方式为例,其他都一个道理。

一、自定义Granter

public class MobilePwdCodeTokenGranter extends AbstractPwdCodeTokenGranter {

  	//授权类型:mobile_pwd_code
    private static final String GRANT_TYPE = GrantTypeEnum.MOBILE_PWD_CODE.getType();
    private static final int CODE_LENGTH = 5;
    private final AuthenticationManager authenticationManager;

    public MobilePwdCodeTokenGranter(AuthorizationServerTokenServices tokenServices,
                                     ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, final AuthenticationManager authenticationManager) {
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.authenticationManager = authenticationManager;
    }


    @Override
    protected Integer getCodeLength() {
        return CODE_LENGTH;
    }

    @Override
    protected Authentication createAuthenticationToken(String username, String password, String code) {
        return new MobilePwdCodeAuthenticationToken(username,password,code);
    }

    @Override
    protected AuthenticationManager authenticationManager() {
        return this.authenticationManager;
    }


}

对于带图形验证码的授权方式,所需传入的参数都一样,因此封装一个抽象父类,抽象类来继承AbstractTokenGranter,后面自定义Granter继承该抽象类即可。

/**
 * @author xiaoyunshi
 * @date 2020/12/14 5:14 下午
 */
public abstract class AbstractPwdCodeTokenGranter extends AbstractTokenGranter {

    protected AbstractPwdCodeTokenGranter(AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
        super(tokenServices, clientDetailsService, requestFactory, grantType);
    }

    protected abstract Integer getCodeLength();

    protected abstract Authentication createAuthenticationToken(String username, String password, String code,String uuid);

    protected abstract AuthenticationManager authenticationManager();

    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
        Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
      //这里获取传入的参数
        String username = parameters.get("username");
        String password = parameters.get("password");
        String code = parameters.get("code");
 
        if (StringUtils.isEmpty(code) || code.length() != getCodeLength()) {
            throw new InvalidGrantException("验证码错误");
        }
        //创建未认证的Authentication 『子类实现』
        Authentication userAuth = createAuthenticationToken(username, password, code);
        ((AbstractAuthenticationToken) userAuth).setDetails(parameters);

        try {
            userAuth = authenticationManager().authenticate(userAuth);
        } catch (AccountStatusException ase) {
            throw new InvalidGrantException(ase.getMessage());
        } catch (BadCredentialsException e) {
            throw new InvalidGrantException(e.getMessage());
        }
        if (userAuth == null || !userAuth.isAuthenticated()) {
            throw new InvalidGrantException("Could not authenticate user : " + username);
        }
        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }
}

authenticationManager().authenticate(userAuth)中,会调用ProviderManagerauthenticate方法。

具体的认证就是在Provider中处理的,因此我们也要自定义一个Provider进行认证,可以参考默认的密码模式的DaoAuthenticationProvider类进行重写:

二、自定义AuthenticationProvider

public class MobilePwdCodeAuthenticationProvider implements AuthenticationProvider {
    private BaseUserDetailsService baseUserDetailsService;
    private RedisTemplate redisTemplate;
    private PasswordEncoder passwordEncoder;


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      //自定义的AuthenticationToken,保存认证的信息
        MobilePwdCodeAuthenticationToken authToken = (MobilePwdCodeAuthenticationToken) authentication;
      //调用userdetailService获取认证信息(按自己的业务实现)返回封装好的SysAuthUser
        SysAuthUser authUser = (SysAuthUser) baseUserDetailsService.loadUserByMobile((String) authToken.getPrincipal());

        if (authUser == null) {
            throw new UsernameNotFoundException("手机号未注册");
        }
        String mobile = (String) authToken.getPrincipal();
        String cachedCode = getCacheCode();
        String password = (String) authToken.getCredentials();

        String inputCode = authToken.getCode();
        if (StringUtils.isEmpty(cachedCode)) {
            throw new InvalidGrantException("验证码已失效");
        } else if (!inputCode.equals(cachedCode)) {
            throw new InvalidGrantException("验证码错误");

        } else {
            removeCode(mobile);
        }
        if (!passwordEncoder.matches(password, authUser.getPassword())) {
            throw new BadCredentialsException("账户或密码错误");
        }
        //认证成功后构造一个新的AuthenticationToken,传入认证好的用户信息和权限信息等
        MobilePwdCodeAuthenticationToken authenticationResult = new MobilePwdCodeAuthenticationToken(authUser, authUser.getPassword(), authUser.getAuthorities());
        authenticationResult.setDetails(authToken.getDetails());

        return authenticationResult;
    }

    private void removeCode(String username) {
        redisTemplate.delete(RedisKeys.LOGIN_PREFIX + username);
    }

    private String getCacheCode() {
      //当然这里图形验证码需要进行唯一标识,不能使用单单的一个key
        String code = (String) redisTemplate.opsForValue().get(RedisKeys.LOGIN_PREFIX);
        return code;
    }


    public void setBaseUserDetailsService(BaseUserDetailsService baseUserDetailsService) {
        this.baseUserDetailsService = baseUserDetailsService;
    }

    public void setRedisTemplate(RedisTemplate jsonRedisTemplate) {
        this.redisTemplate = jsonRedisTemplate;
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

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

}


最后,就是设计认证信息的AuthenticationToken类,这里可以根据UsernamePasswordAuthenticationToken进行编写。

三、自定义AuthenticationToken

public class MobilePwdCodeAuthenticationToken extends AbstractAuthenticationToken {

  
    private Object principal;
    /**
     * 图片验证码
     */
    private String code;

    private Object credentials;


    public MobilePwdCodeAuthenticationToken(String mobile, String password, String code) {
        super(null);
        //第一次在Granter的createAuthenticationToken中创建,这时传入的是手机号
        this.principal = mobile;
 
        this.code = code;
        this.credentials=password;
        //第一次未认证
        this.setAuthenticated(false);
    }

    /**
     * 认证通过后走这个构造
     * @param principal 认证后的对象
     * @param authorities 认证后查询到的权限
     */
    public MobilePwdCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal=principal;
        this.credentials=credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return this.credentials;
    }

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

    public String getCode() {
        return code;
    }

   
}

OK,这样就可以了,最后来的配置类配置Provider:

@Component
public class MobilePwdCodeAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private BaseUserDetailsService baseUserDetailsService;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    public PasswordEncoder bCryptPasswordEncoder;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        // 构建一个MobileAuthenticationProvider实例
        MobilePwdCodeAuthenticationProvider provider = new MobilePwdCodeAuthenticationProvider();
        provider.setBaseUserDetailsService(baseUserDetailsService);
        provider.setRedisTemplate(stringRedisTemplate);
        provider.setPasswordEncoder(bCryptPasswordEncoder);
        http.authenticationProvider(provider);

    }
}

这里最终会将自定义的Provider添加到AuthenticationManagerBuilderprivate List<AuthenticationProvider> authenticationProviders中。

最后重新配置 下TokenGranter:

四、配置TokenGranter

@Configuration
public class TokenGranterConfig {
    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private UserDetailsService baseUserDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenStore tokenStore;

    @Autowired(required = false)
    private List<TokenEnhancer> tokenEnhancer;


    private RandomValueAuthorizationCodeServices authorizationCodeServices;

    private boolean reuseRefreshToken = true;

    private AuthorizationServerTokenServices tokenServices;

    private TokenGranter tokenGranter;

    /**
     * 授权模式
     */
    @Bean
    public TokenGranter tokenGranter() {
        if (tokenGranter == null) {
            tokenGranter = new CompositeTokenGranter(getAllTokenGranters());
        }
        return tokenGranter;
    }

    /**
     * 所有授权模式:默认的5种模式 + 自定义的模式
     */
    private List<TokenGranter> getAllTokenGranters() {
        AuthorizationServerTokenServices tokenServices = tokenServices();
        AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
        OAuth2RequestFactory requestFactory = requestFactory();
        //获取默认的授权模式
        List<TokenGranter> tokenGranters = getDefaultTokenGranters(tokenServices, authorizationCodeServices, requestFactory);
        if (authenticationManager != null) {
          
            // 添加自定义的一些TokenGranter
          
            tokenGranters.add(new MobilePwdCodeTokenGranter(tokenServices, clientDetailsService, requestFactory, authenticationManager));
           
        }
        return tokenGranters;
    }

    /**
     * 默认的授权模式
     */
    private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerTokenServices tokenServices
            , AuthorizationCodeServices authorizationCodeServices, OAuth2RequestFactory requestFactory) {
        List<TokenGranter> tokenGranters = new ArrayList<>();
        // 添加授权码模式
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory));
        // 添加刷新令牌的模式
        tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, requestFactory));
        // 添加隐士授权模式
        tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, requestFactory));
        // 添加客户端模式
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, requestFactory));
        if (authenticationManager != null) {
            // 添加密码模式
            tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, requestFactory));
        }
        return tokenGranters;
    }

    private AuthorizationServerTokenServices tokenServices() {
        if (tokenServices != null) {
            return tokenServices;
        }
        this.tokenServices = createDefaultTokenServices();
        return tokenServices;
    }

    private AuthorizationCodeServices authorizationCodeServices() {
        if (authorizationCodeServices == null) {
            authorizationCodeServices = new InMemoryAuthorizationCodeServices();
        }
        return authorizationCodeServices;
    }

    private OAuth2RequestFactory requestFactory() {
        return new DefaultOAuth2RequestFactory(clientDetailsService);
    }

    private DefaultTokenServices createDefaultTokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(tokenStore);
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setReuseRefreshToken(reuseRefreshToken);
        tokenServices.setClientDetailsService(clientDetailsService);
        tokenServices.setTokenEnhancer(tokenEnhancer());
        addUserDetailsService(tokenServices, this.baseUserDetailsService);
        return tokenServices;
    }

    private TokenEnhancer tokenEnhancer() {
        if (tokenEnhancer != null) {
            TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(tokenEnhancer);
            return tokenEnhancerChain;
        }
        return null;
    }

    private void addUserDetailsService(DefaultTokenServices tokenServices, UserDetailsService userDetailsService) {
        if (userDetailsService != null) {
            PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
            provider.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(userDetailsService));
            tokenServices.setAuthenticationManager(new ProviderManager(Collections.singletonList(provider)));
        }
    }
}

在这里插入图片描述

五、添加到EndPoints

然后在AuthorizationServerConfigurerAdapter配置类中将新的授权类添加到endPoints中:

@Configuration
@EnableAuthorizationServer
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
 		@Autowired
    private TokenGranter tokenGranter;
 
  .................................................
  	 @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints.accessTokenConverter(jwtAccessTokenConverter)
                //认证管理器
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore)
                //用户信息service
                .userDetailsService(baseUserDetailsService)
                //添加重新注入的tokenGranter
                .tokenGranter(tokenGranter)
        ;
    }
}

这样就OK了,申请令牌时,只需将grant_type改为granter中设置的类型即可,当前如果添加了其他参数,比如这里我们添加了验证码 code,那也要把code作为参数传入进来。

六、流程分析

重新回到上面暂停的流程。

CompositeTokenGrantergrant()方法中打个断点:

在这里插入图片描述

可以看到已经加载了5个默认授权类型和自定义的授权类型,当然还有我添加的其他几个,这里第6个就是手机+密码+验证码的授权方式类。

接着来到抽象类AbstractTokenGranter的grant方法中,

if (!this.grantType.equals(grantType)) {
			return null;
		}

这里匹配授权方式通过,就会进行后面的流程。

	String clientId = tokenRequest.getClientId();
		ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
		validateGrantType(grantType, client);

		if (logger.isDebugEnabled()) {
			logger.debug("Getting access token for: " + clientId);
		}

		return getAccessToken(client, tokenRequest);

接着会到数据库查询当前的客户端信息,validateGrantType中拿到数据库中该客户端配置的所有授权方式,看看是否有当前的授权类型(所以别忘了在对应的客户端中把自定义的授权方式添加信息),有就通过,否则抛异常。

protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
		return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
	}

这里就会调用我们自定义的子类MobileCodeTokenGrantergetOAuth2Authentication方法:

完成自定义的校验工作,最后返回:

		OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);

userAuth这里就是MobilePwdCodeAuthenticationToken对象。

最后调用tokenServices.createAccessToken就完成了令牌的申请。

over。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值