博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
spring security oauth2 allowFormAuthenticationForClients原理解析
阅读量:5871 次
发布时间:2019-06-19

本文共 15818 字,大约阅读时间需要 52 分钟。

  hot3.png

本文主要解析一下spring security oauth2中AuthorizationServerConfigurerAdapter的allowFormAuthenticationForClients的原理

allowFormAuthenticationForClients的作用

主要是让/oauth/token支持client_id以及client_secret作登录认证

AuthorizationServerSecurityConfiguration

spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configuration/AuthorizationServerSecurityConfiguration.java

@Configuration@Order(0)@Import({ ClientDetailsServiceConfiguration.class, AuthorizationServerEndpointsConfiguration.class })public class AuthorizationServerSecurityConfiguration extends WebSecurityConfigurerAdapter {	@Autowired	private List
configurers = Collections.emptyList(); @Autowired private ClientDetailsService clientDetailsService; @Autowired private AuthorizationServerEndpointsConfiguration endpoints; @Autowired public void configure(ClientDetailsServiceConfigurer clientDetails) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(clientDetails); } } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // Over-riding to make sure this.disableLocalConfigureAuthenticationBldr = false // This will ensure that when this configurer builds the AuthenticationManager it will not attempt // to find another 'Global' AuthenticationManager in the ApplicationContext (if available), // and set that as the parent of this 'Local' AuthenticationManager. // This AuthenticationManager should only be wired up with an AuthenticationProvider // composed of the ClientDetailsService (wired in this configuration) for authenticating 'clients' only. } @Override protected void configure(HttpSecurity http) throws Exception { AuthorizationServerSecurityConfigurer configurer = new AuthorizationServerSecurityConfigurer(); FrameworkEndpointHandlerMapping handlerMapping = endpoints.oauth2EndpointHandlerMapping(); http.setSharedObject(FrameworkEndpointHandlerMapping.class, handlerMapping); configure(configurer); http.apply(configurer); String tokenEndpointPath = handlerMapping.getServletPath("/oauth/token"); String tokenKeyPath = handlerMapping.getServletPath("/oauth/token_key"); String checkTokenPath = handlerMapping.getServletPath("/oauth/check_token"); if (!endpoints.getEndpointsConfigurer().isUserDetailsServiceOverride()) { UserDetailsService userDetailsService = http.getSharedObject(UserDetailsService.class); endpoints.getEndpointsConfigurer().userDetailsService(userDetailsService); } // @formatter:off http .authorizeRequests() .antMatchers(tokenEndpointPath).fullyAuthenticated() .antMatchers(tokenKeyPath).access(configurer.getTokenKeyAccess()) .antMatchers(checkTokenPath).access(configurer.getCheckTokenAccess()) .and() .requestMatchers() .antMatchers(tokenEndpointPath, tokenKeyPath, checkTokenPath) .and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); // @formatter:on http.setSharedObject(ClientDetailsService.class, clientDetailsService); } protected void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { for (AuthorizationServerConfigurer configurer : configurers) { configurer.configure(oauthServer); } }}

这里有几个关键点:

  • 扩展了WebSecurityConfigurerAdapter方法
  • 指定了Order顺序为0,该顺序是值越小优先级别越高
  • 配置HttpSecurity的requestMatchers、filter以及相应的AuthenticationManager

AuthorizationServerSecurityConfigurer

spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java

public final class AuthorizationServerSecurityConfigurer extends		SecurityConfigurerAdapter
{ private AuthenticationEntryPoint authenticationEntryPoint; private AccessDeniedHandler accessDeniedHandler = new OAuth2AccessDeniedHandler(); private PasswordEncoder passwordEncoder; // for client secrets private String realm = "oauth2/client"; private boolean allowFormAuthenticationForClients = false; private String tokenKeyAccess = "denyAll()"; private String checkTokenAccess = "denyAll()"; private boolean sslOnly = false; /** * Custom authentication filters for the TokenEndpoint. Filters will be set upstream of the default * BasicAuthenticationFilter. */ private List
tokenEndpointAuthenticationFilters = new ArrayList
(); @Override public void init(HttpSecurity http) throws Exception { registerDefaultAuthenticationEntryPoint(http); if (passwordEncoder != null) { ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService()); clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder()); http.getSharedObject(AuthenticationManagerBuilder.class) .userDetailsService(clientDetailsUserDetailsService) .passwordEncoder(passwordEncoder()); } else { http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService())); } http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable() .httpBasic().realmName(realm); } @SuppressWarnings("unchecked") private void registerDefaultAuthenticationEntryPoint(HttpSecurity http) { ExceptionHandlingConfigurer
exceptionHandling = http .getConfigurer(ExceptionHandlingConfigurer.class); if (exceptionHandling == null) { return; } if (authenticationEntryPoint==null) { BasicAuthenticationEntryPoint basicEntryPoint = new BasicAuthenticationEntryPoint(); basicEntryPoint.setRealmName(realm); authenticationEntryPoint = basicEntryPoint; } ContentNegotiationStrategy contentNegotiationStrategy = http.getSharedObject(ContentNegotiationStrategy.class); if (contentNegotiationStrategy == null) { contentNegotiationStrategy = new HeaderContentNegotiationStrategy(); } MediaTypeRequestMatcher preferredMatcher = new MediaTypeRequestMatcher(contentNegotiationStrategy, MediaType.APPLICATION_ATOM_XML, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_XML, MediaType.MULTIPART_FORM_DATA, MediaType.TEXT_XML); preferredMatcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); exceptionHandling.defaultAuthenticationEntryPointFor(postProcess(authenticationEntryPoint), preferredMatcher); } @Override public void configure(HttpSecurity http) throws Exception { // ensure this is initialized frameworkEndpointHandlerMapping(); if (allowFormAuthenticationForClients) { clientCredentialsTokenEndpointFilter(http); } for (Filter filter : tokenEndpointAuthenticationFilters) { http.addFilterBefore(filter, BasicAuthenticationFilter.class); } http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); if (sslOnly) { http.requiresChannel().anyRequest().requiresSecure(); } } private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) { ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter( frameworkEndpointHandlerMapping().getServletPath("/oauth/token")); clientCredentialsTokenEndpointFilter .setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint(); authenticationEntryPoint.setTypeName("Form"); authenticationEntryPoint.setRealmName(realm); clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint); clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter); http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class); return clientCredentialsTokenEndpointFilter; } //......}

使用了SecurityConfigurerAdapter来进行HttpSecurity的配置,这里主要做的事情,就是如果开启了allowFormAuthenticationForClients,那么就在BasicAuthenticationFilter之前添加clientCredentialsTokenEndpointFilter,使用ClientDetailsUserDetailsService来进行client端登录的验证

AbstractAuthenticationProcessingFilter

spring-security-web-4.2.3.RELEASE-sources.jar!/org/springframework/security/web/authentication/AbstractAuthenticationProcessingFilter.java

/**	 * Invokes the	 * {@link #requiresAuthentication(HttpServletRequest, HttpServletResponse)	 * requiresAuthentication} method to determine whether the request is for	 * authentication and should be handled by this filter. If it is an authentication	 * request, the	 * {@link #attemptAuthentication(HttpServletRequest, HttpServletResponse)	 * attemptAuthentication} will be invoked to perform the authentication. There are	 * then three possible outcomes:	 * 
    *
  1. An Authentication object is returned. The configured * {@link SessionAuthenticationStrategy} will be invoked (to handle any * session-related behaviour such as creating a new session to protect against * session-fixation attacks) followed by the invocation of * {@link #successfulAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, Authentication)} * method
  2. *
  3. An AuthenticationException occurs during authentication. The * {@link #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) * unsuccessfulAuthentication} method will be invoked
  4. *
  5. Null is returned, indicating that the authentication process is incomplete. The * method will then return immediately, assuming that the subclass has done any * necessary work (such as redirects) to continue the authentication process. The * assumption is that a later request will be received by this method where the * returned Authentication object is not null. *
*/ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } if (logger.isDebugEnabled()) { logger.debug("Request is to process authentication"); } Authentication authResult; try { authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { logger.error( "An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { // Authentication failed unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authResult); } /** * Indicates whether this filter should attempt to process a login request for the * current invocation. *

* It strips any parameters from the "path" section of the request URL (such as the * jsessionid parameter in http://host/myapp/index.html;jsessionid=blah) * before matching against the filterProcessesUrl property. *

* Subclasses may override for special requirements, such as Tapestry integration. * * @return true if the filter should attempt authentication, * false otherwise. */ protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { return requiresAuthenticationRequestMatcher.matches(request); }

这里先调用了requiresAuthentication来判断是否需要拦截

2.0.14.RELEASE/spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/provider/client/ClientCredentialsTokenEndpointFilter.java#ClientCredentialsRequestMatcher

protected static class ClientCredentialsRequestMatcher implements RequestMatcher {		private String path;		public ClientCredentialsRequestMatcher(String path) {			this.path = path;		}		@Override		public boolean matches(HttpServletRequest request) {			String uri = request.getRequestURI();			int pathParamIndex = uri.indexOf(';');			if (pathParamIndex > 0) {				// strip everything after the first semi-colon				uri = uri.substring(0, pathParamIndex);			}			String clientId = request.getParameter("client_id");			if (clientId == null) {				// Give basic auth a chance to work instead (it's preferred anyway)				return false;			}			if ("".equals(request.getContextPath())) {				return uri.endsWith(path);			}			return uri.endsWith(request.getContextPath() + path);		}	}

而这个filter只会拦截url中带有client_id和client_secret的请求,而会把使用basic认证传递的方式交给BasicAuthenticationFilter来做。

因此,如果是这样调用,是走这个filter

curl -H "Accept: application/json" http://localhost:8080/oauth/token -d "grant_type=client_credentials&client_id=demoApp&client_secret=demoAppSecret"

如果是这样调用,则是走basic认证

curl -i -X POST -H "Accept: application/json" -u "demoApp:demoAppSecret" http://localhost:8080/oauth/token

而之前spring-security-oauth2-2.0.14.RELEASE-sources.jar!/org/springframework/security/oauth2/config/annotation/web/configurers/AuthorizationServerSecurityConfigurer.java已经设置了ClientDetailsUserDetailsService,因而是支持client_id和client_secret作为用户密码登录(这样就支持不了普通用户的账号密码登录,例外:password方式支持,但前提也需要经过client_id和client_secret认证)

@Override	public void init(HttpSecurity http) throws Exception {		registerDefaultAuthenticationEntryPoint(http);		if (passwordEncoder != null) {			ClientDetailsUserDetailsService clientDetailsUserDetailsService = new ClientDetailsUserDetailsService(clientDetailsService());			clientDetailsUserDetailsService.setPasswordEncoder(passwordEncoder());			http.getSharedObject(AuthenticationManagerBuilder.class)					.userDetailsService(clientDetailsUserDetailsService)					.passwordEncoder(passwordEncoder());		}		else {			http.userDetailsService(new ClientDetailsUserDetailsService(clientDetailsService()));		}		http.securityContext().securityContextRepository(new NullSecurityContextRepository()).and().csrf().disable()				.httpBasic().realmName(realm);	}

WebSecurityConfigurerAdapter实例

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)public class SecurityConfig extends WebSecurityConfigurerAdapter {	@Override    public void configure(HttpSecurity http) throws Exception {        http.csrf().disable();        http                .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**")                .and()                .authorizeRequests()                .antMatchers("/oauth/**").authenticated()                .and()                .formLogin().permitAll(); //新增login form支持用户登录及授权    }	@Bean    @Override    protected UserDetailsService userDetailsService(){        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();        manager.createUser(User.withUsername("demoUser1").password("123456").authorities("USER").build());        manager.createUser(User.withUsername("demoUser2").password("123456").authorities("USER").build());        return manager;    }    /**     * support password grant type     * @return     * @throws Exception     */    @Override    @Bean    public AuthenticationManager authenticationManagerBean() throws Exception {        return super.authenticationManagerBean();    }}

AuthorizationServerSecurityConfiguration的配置,order为0,则无论后面的WebSecurityConfigurerAdapter怎么配置,只要优先级不比它高,他们针对/oauth/**相关的配置都不生效,都会优先被这里的ClientCredentialsTokenEndpointFilter拦截处理。

小结

这里使用order来提升优先级。没有配置order的话,则不能生效。

比如ResourceServerConfigurerAdapter中配置拦截了/api/**,但是没有配置优先级,最后的WebSecurityConfigurerAdapter如果也有相同的/api/**认证配置的话,则会覆盖前者。

使用多个WebSecurityConfigurerAdapter的话,一般是每个配置分别拦截各自的url,互补重复。如果有配置order的话,则order值小的配置会优先使用,会覆盖后者。

doc

转载于:https://my.oschina.net/go4it/blog/1587455

你可能感兴趣的文章
乌克兰基辅一世遗修道院起火 现场火光照亮夜空
查看>>
[iOS 10 day by day] Day 2:线程竞态检测工具 Thread Sanitizer
查看>>
Centos/Ubuntu下安装nodejs
查看>>
关于浏览器的cookie
查看>>
Hyper-V 2016 系列教程30 机房温度远程监控方案
查看>>
国内先进的智能移动广告聚合平台-KeyMob聚合
查看>>
我的友情链接
查看>>
我的友情链接
查看>>
PHP - 如何打印函数调用树
查看>>
js闭包
查看>>
寒假。3.3.G - Common Child (最大公共子序)
查看>>
设计模式学习笔记--原型模式
查看>>
.Net 通过MySQLDriverCS操作MySQL
查看>>
JS Cookie
查看>>
ubuntu Unable to locate package sysv-rc-conf
查看>>
笔记:认识.NET平台
查看>>
cocos2d中CCAnimation的使用(cocos2d 1.0以上版本)
查看>>
【吉光片羽】短信验证
查看>>
MacBook如何用Parallels Desktop安装windows7/8
查看>>
gitlab 完整部署实例
查看>>