深入理解Spring Cloud Security OAuth2身份认证

spring security权限拦截

OAuth2的概念请参考深入理解OAuth2协议和使用场景。Servlet 过滤器可以动态地拦截Http请求和响应,在真正执行servlet逻辑之前自定义请求的处理。Servlet 过滤器Filter的接口定义如下:

public interface Filter {


    public default void init(FilterConfig filterConfig) throws ServletException {}

    /**
     * 过滤请求
     */
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException;


    public default void destroy() {}
}

在spring security 中,主要是通过FilterChainProxy(Servlet Filter过滤器)实现了Filter接口,对Http请求进行拦截和控制的,定义了一组过滤器(filterChains),实现了对认证和授权的各种拦截,FilterChainProxy源码如下:

public class FilterChainProxy extends GenericFilterBean {
	
   /**
	 * 权限过滤器链
	 */    
	private List<SecurityFilterChain> filterChains;

	/**
	 * 请求拦截
	 */  
	@Override
	public void doFilter(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {
		boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
		if (clearContext) {
			try {
				request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
				doFilterInternal(request, response, chain);
			}
			finally {
				SecurityContextHolder.clearContext();
				request.removeAttribute(FILTER_APPLIED);
			}
		}
		else {
			doFilterInternal(request, response, chain);
		}
	}

	private void doFilterInternal(ServletRequest request, ServletResponse response,
			FilterChain chain) throws IOException, ServletException {

		FirewalledRequest fwRequest = firewall
				.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse fwResponse = firewall
				.getFirewalledResponse((HttpServletResponse) response);
        // 匹配请求的过滤器列表
		List<Filter> filters = getFilters(fwRequest);

		if (filters == null || filters.size() == 0) {
			if (logger.isDebugEnabled()) {
				logger.debug(UrlUtils.buildRequestUrl(fwRequest)
						+ (filters == null ? " has no matching filters"
								: " has an empty filter list"));
			}

			fwRequest.reset();
            // 过滤器链过滤
			chain.doFilter(fwRequest, fwResponse);

			return;
		}

		VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
		vfc.doFilter(fwRequest, fwResponse);
	}
}

spring security对权限拦截的大致流程如下:

的在spring security 中常见的Filter如下:

  • SecurityContextPersistenceFilter,用于在认证前后设置SecurityContext值,认证通过以后,会把认证的用户信息保存保存到SecurityContext。
  • UsernamePasswordAuthenticationFilter,用户名和密码验证过滤器,拦截/login请求,对用户进行身份验证。
  • FilterSecurityInterceptor,拦截资源服务器的请求,进行资源授权。

spring security的认证逻辑,通过AuthenticationManager接口实现,源码如下:

public interface AuthenticationManager {

	/**
	 * 身份认证
	 */
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;
}

 ProviderManager为AuthenticationManager,定义了AuthenticationProvider列表,扩展了身份认证逻辑,AuthenticationProvider的接口定义如下:

public interface AuthenticationProvider {

	/**
	 * 身份认证
	 */
	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

ProviderManager的部分源码如下:

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

    // 认证逻辑列表
	private List<AuthenticationProvider> providers = Collections.emptyList();
	
	/**
	 * 身份认证逻辑
	 */
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
        ......

        // 遍历身份认证列表
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
                // 身份认证
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
		}
        ......
}

在 AuthenticationProvider的子类实现中丰富了身份认证逻辑,常见的实现类的如下:

  • DaoAuthenticationProvider,用户信息认证,最常使用
  • PreAuthenticatedAuthenticationProvider,身份预先认证逻辑
  • RememberMeAuthenticationProvider,记住我身份认证

spring security OAuth2拦截流程

spring security OAuth2作为spring security的子项目,对于web安全的控制也是通过Servlet 过滤器调用链实现的,在spring security的基础上,新增了自己特有的过滤器和服务端点,实现OAuth2中定义的客户端,授权服务器,资源服务器,访问授权码等等概念的逻辑。例如,新增的服务端点:

  • /oauth/authorize GET,获取客户端的授权码
  • /oauth/authorize POST,获取隐式授权类型(Implicit)的获取访问令牌
  • /oauth/token,用于获取除隐式类型以外的授权类型的访问令牌,以及刷新访问令牌
  • /oauth/check_token,用于验证访问令牌

新增ClientCredentialsTokenEndpointFilter,拦截/oauth/token http请求,用户对客户端进行验证,源码如下:

public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {


	public ClientCredentialsTokenEndpointFilter() {
        // 拦截/oauth/token url
		this("/oauth/token");
	}


	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException, IOException, ServletException {

		if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
			throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
		}

        // 客户端信息
		String clientId = request.getParameter("client_id");
		String clientSecret = request.getParameter("client_secret");

		// If the request is already authenticated we can assume that this
		// filter is not needed
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		if (authentication != null && authentication.isAuthenticated()) {
			return authentication;
		}

		if (clientId == null) {
			throw new BadCredentialsException("No client credentials presented");
		}

		if (clientSecret == null) {
			clientSecret = "";
		}

		clientId = clientId.trim();
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
				clientSecret);
        // 验证客户端
		return this.getAuthenticationManager().authenticate(authRequest);

	}

}

以下用户密码类型授权方式,讲述下spring security OAuth2的身份认证,大致流程如下:

  1. 在客户端直接访问/oauth/token 发起获取访问授权码的请求。
  2. 请求会被ClientCredentialsTokenEndpointFilter拦截,在该过滤器中,会对客户端信息进行认证。
  3. 认证通过后会访问到在TokenEndpoint中定义的/oauth/token 逻辑,通过TokenGranter对用户身份进行认证,生成访问授权码。

TokenGranter生成器

在spring security OAuth2,定义了适用于OAuth2认证类型的TokenGranter子类,实现类如下:

  • ClientCredentialsTokenGranter,客户端授权类型的TokenGranter
  • ImplicitTokenGranter,简化授权类型的TokenGranter
  • AuthorizationCodeTokenGranter,授权码授权类型的TokenGranter
  • ResourceOwnerPasswordTokenGranter,密码授权类型的TokenGranter
  • RefreshTokenGranter,刷新token
  • CompositeTokenGranter,组合TokenGranter,根据类型生成访问授权码

以上,以密码授权类型(Resource Owner Password Credentials)进行用户身份认证。所以,TokenEndpoint会调用ResourceOwnerPasswordTokenGranter密码授权类型的TokenGranter。在该类中,首先对用户信息(username,password)进行认证,认证通过生成访问令牌access token,用户身份的认证逻辑基于spring security身份认证调用DaoAuthenticationProvider实现。ResourceOwnerPasswordTokenGranter的源码如下:

public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {

    // 生成token的授权类型 password
	private static final String GRANT_TYPE = "password";


   /**
    * 对用户信息username和password进行验证
    * 生成OAuth2Authentication
    */
	@Override
	protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

		Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
        
        // 获取username和password信息
		String username = parameters.get("username");
		String password = parameters.get("password");
		// Protect from downstream leaks of password
		parameters.remove("password");

		Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
		((AbstractAuthenticationToken) userAuth).setDetails(parameters);
		try {
            // 使用AuthenticationManager进行验证
			userAuth = authenticationManager.authenticate(userAuth);
		}
		catch (AccountStatusException ase) {
			//covers expired, locked, disabled cases (mentioned in section 5.2, draft 31)
			throw new InvalidGrantException(ase.getMessage());
		}
		catch (BadCredentialsException e) {
			// If the username/password are wrong the spec says we should send 400/invalid grant
			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);
	}
}

搭建认证服务器

在spring security OAuth2中,通过@EnableAuthorizationServer注解开启了授权服务器的默认配置,通过继承AuthorizationServerConfigurerAdapter实现了对于授权服务器的自定义。配置包括三方面的定义:

  • ClientDetailsServiceConfigurer,定义了对OAuth2客户端信息的配置。
  • AuthorizationServerEndpointsConfigurer,定义了对OAuth2端点的配置,例如对用户服务,访问授权的存储,以及转换的配置。
  • AuthorizationServerSecurityConfigurer,定义了安全过滤器链FilterChainProxy的配置,例如设置允许所有人访问令牌,已验证的客户端才能请求check_token。

在实现认证服务器的过程中,使用了redis作为tokenStore;使用jwt对token进行转换;使用了jdbc模式存储客户端信息。源码如下:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;
    /**
     * redis连接工厂
     */
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    /**
     * 用户服务
     */
    @Autowired
    private UserDetailsService userDetailsService;
    /**
     * 数据源
     */
    @Autowired
    private DataSource dataSource;
    /**
     * jwt
     */
    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthorizationServerTokenServices tokenServices;

    /**
     * oauth2客户端服务配置
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 配置基于jdbc的ClientDetailsService,用于加载客户端信息
        clients.withClientDetails(ClientDetailsService());
    }

    /**
     * 使用jdbc方式,配置客户端服务
     */
    @Bean
    public ClientDetailsService ClientDetailsService() {

        JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
        // 设置自定义加密规则,默认不加密
        jdbcClientDetailsService.setPasswordEncoder(passwordEncoder);
        return jdbcClientDetailsService;
    }

    /**
     * 授权服务端点配置
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        endpoints
                // 配置tokenStore,使用redis 存储token
                .tokenStore(new RedisTokenStore(redisConnectionFactory))
                .authenticationManager(authenticationManager)
                // 用户管理服务
                .userDetailsService(userDetailsService)
                // 配置令牌转化器
                .accessTokenConverter(jwtAccessTokenConverter)
                // 允许 GET、POST 请求获取 token,即访问端点:oauth/token
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
                .reuseRefreshTokens(true);
    }


    /**
     * 授权服务器安全认证的相关配置,生成对应的安全过滤器链,主要是控制oauth/**端点的相关配置,保证授权服务器端点的安全访问
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {

        security
                // 允许所有人访问令牌
                .tokenKeyAccess("permitAll()")
                // 已验证的客户端才能请求check_token
                .checkTokenAccess("isAuthenticated()")
                // 允许表单认证
                .allowFormAuthenticationForClients();
    }

    /**
     * jwt token 配置
     */
    @Configuration
    public static class JwtTokenConfig {

        /**
         * JwtAccessTokenConverter
         */
        @Bean
        public JwtAccessTokenConverter jwtAccessTokenConverter() {
            JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
            converter.setSigningKey("kuqi-mall");
            return converter;
        }
    }
}
WebSecurityConfig配置源码如下:
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 用户服务
     */
    @Autowired
    private MallUserDetailsService userDetailsService;

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    /**
     * 配置HTTP安全
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {

        httpSecurity
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated();
    }

    /**
     * 配置密码解码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }
}

在db中,配置了客户端信息(client_id为meituan,client_secret为123456),配置了用户信息(username为admin,password为123456).通过客户端密码类型操作,获取到了access token,使用postman访问成功的截图如下:

不足与优化之处

 以上,是基于spring security OAuth2的默认实现配置的。如果,项目中,要求通过电话号码和密码的方式获取access token该如何实现呢?又或者使用电话号码和验证码的方式呢?请关注后续的认证自定义章节。

发布了37 篇原创文章 · 获赞 2 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/new_com/article/details/104706005