30、oauth2.0认证服务器与资源服务器分离成不同服务(进程)或既是认证服务又是资源服务器

 

一、问题

    

    对于oauth2.0的诟病我真是受够了,要不是时间问题,真他妈想理解原理后自己开发一套,对于oauth2.0的使用过程中出现的问题,一直困扰着我,网络有资料,几十份中基本没有出现我出现过的问题,最终还是经过调试源码一步一步找到对应问题。最终得以解决改问题,这位后续使用oauth2.0扫清了障碍。

 

使用过程中,总结问题如下:

(1)所有URL访问,无法使用权限认证控制,也就是ResourceServer或WebSecurity配置不起作用;

(2)资源服务器与认证服务器不在同一进程(各自分离属于不同服务)情况下,资源服务器无法使用认证服务器进行权限控制;

(3)资源服务器application.yml配置资源服务器问题

(4)资源服务器不能配置在配置文件中配置资源服务器id

(5)使用四种方式获取的token可以使用url后带上access_token或添加Authorization的http头bearer方式向资源服务器发出访问资源请求,但是如果认证服务器也是资源服务器,无法使用像资源服务器一样访问认证服务器;

 

二、解决方案

 

1、所有URL访问,无法使用权限认证控制,也就是ResourceServer或WebSecurity配置不起作用

 

这是配置问题,针对资源的访问保护配置,主要通过WebSecurity的重载方法(configure(HttpSecurity http))进行配置或资源服务器的重载方法(configure(HttpSecurity http))进行配置,配置方式

 

如果要使得某些url允许所有人访问,网络上方法如下:

http.antMatcher("/login").authorizeRequests().anyRequest().authenticated()

如果是这么使用可能会导致所有的拦截都失效,这里要注意antMatcher与authorizeRequest两个方法的使用。

 

(1)其中:antMatcher方法具体说明如:

 

public HttpSecurity antMatcher(String antPattern)

Allows configuring the HttpSecurity to only be invoked when matching the provided ant pattern. If more advanced configuration is necessary, consider using requestMatchers() or requestMatcher(RequestMatcher).

Invoking antMatcher(String) will override previous invocations of requestMatchers(), antMatcher(String), regexMatcher(String), and requestMatcher(RequestMatcher).

Parameters:

antPattern - the Ant Pattern to match on (i.e. "/admin/**")

Returns:

the HttpSecurity for further customizations

See Also:

AntPathRequestMatcher

表示只允许与ant表达式匹配的路径才能被调用或访问。

 

(2)authorizeRequests则表示要对后续的url做权限限制

http.authorizeRequests()

            .antMatchers("/home", "/login").permitAll()

            .anyRequest().authenticated();

表示对/login和/home的访问不做权限校验(人人都可以访问),剩余的都需要通过认证后才能访问。

 

2、资源服务器与认证服务器不在同一进程(各自分离属于不同服务)情况下,资源服务器无法使用认证服务器进行权限控制;

 

说起这个问题,我倒是没有一直深究,也没去调试(喜欢问为什么的朋友一定要调试寻根问底),具体问题是:

 

有问题的配置(这个要调试才知道,我没做):

security: 

  oauth2:

    resource:

      filter-order: 3

      id: test_resource_id

      user-info-uri: http://localhost:7006/user/principal

      prefer-token-info: false

 

这个问题引起的原因就是红色标注的prefer-token-info(当然可以不用‘-’,用大写去掉其后字母),这里如果使用false,表示不使用token进行验证。使用用户信息进行验证,这里原因是因为我们已经获取到了token,所以这里应该使用token进行验证。修改如下:

 

纠正配置:

security: 

  oauth2:

    resource:

      filter-order: 3

      id: test_resource_id

      tokenInfoUri:http://localhost:7006/oauth/check_token

      preferTokenInfo: true

 

这样就可以使用token访问资源服务器了,提到如何使用access_token访问资源服务器,这里也遇到一个问题,顺便一块提一下:

(1)如果认证服务器不是资源服务器的情况下(也就是只存在AuthorizationServer和WebSecurity的情况下,没有ResourceServer),此时使用token访问只能通过http头部携带Authentication字段,且值必须为bearer + 空格 + token的方式,如http头包含: authentication: bearer 12323435-4354

(2)如果认证服务器同时也是资源服务器的情况下(也就是即存在AuthorizationServer、WebSecurity也存在ResourceServer),此时可以使用如下两种方式访问资源服务器:

a、http头部携带authentication,如: authentication: bearer 1234    (中间有空格)

b、http头部不存在authentication,直接将access_token作为url参数,如  url?access_token=1234

 

 

还有一个大问题就是如果授权服务也是资源服务,势必导致WebSecurity与ResourceServer冲突问题,这里必须必须记住的是WebSecurity的bean的order要高于ResourceServer

也就是如下配置,WebSecurity配置

 

package com.donwait.config;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import org.springframework.core.annotation.Order;

import org.springframework.security.authentication.AuthenticationManager;

import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.config.annotation.web.builders.WebSecurity;

import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import org.springframework.security.crypto.password.PasswordEncoder;

 

import com.donwait.service.impl.UserDetailsServiceImpl;

 

@Configuration

@Order(2)

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired

    private UserDetailsServiceImpl userDetailsService;

            

    @Bean

    public PasswordEncoder passwordEncoder() {

        return new BCryptPasswordEncoder();

    }

 

    @Override

    @Bean

    public AuthenticationManager authenticationManagerBean() throws Exception {

        return super.authenticationManagerBean();

    }

    @Override

    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userDetailsService)

            .passwordEncoder(passwordEncoder());

    }

 

    @Override

    public void configure(WebSecurity web) throws Exception {

        web.ignoring().antMatchers("/favor.ico", "/favicon.ico");

    }

 

    @Override

    protected void configure(HttpSecurity http) throws Exception {

        http

            .authorizeRequests()

                .anyRequest().authenticated()

            .and()

                .formLogin().and()

                .csrf().disable()

                .httpBasic();

    }

}

 

资源服务器配置:

package com.donwait.config;

 

import org.springframework.context.annotation.Configuration;

import org.springframework.core.annotation.Order;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;

import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;

import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;

import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;

 

 

@Configuration

@EnableResourceServer

@Order(6)

public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

private static final String DEMO_RESOURCE_ID = "oauth2-resource";

    

    /**

     * 以代码形式配置资源服务器id,配置文件配置不生效

     */

    @Override

    public void configure(ResourceServerSecurityConfigurer resources) {

        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);

    }

    

    @Override

    public void configure(HttpSecurity http) throws Exception {    

        http.

            csrf().disable()

            .authorizeRequests()

                .anyRequest().authenticated()

            .and()

            .httpBasic();

    }

}

 

3、资源服务器不能配置在配置文件中配置资源服务器id

 

这个问题可能是oauth2.0的一个bug,一个网友也曾描述过该问题,这里不在赘述,配置如下:

security: 

  oauth2:

    resource:

      filter-order: 3

      id: test_resource_id

      tokenInfoUri:http://localhost:7006/oauth/check_token

      preferTokenInfo: true

这里的resource服务的id(可以配置用户可以访问哪些资源服务器,如果数据库为null,也就是不配置,可以访问所有资源服务)不生效,需要用代码配置,配置只需要在资源服务器中配置即可:

private static final String DEMO_RESOURCE_ID = "oauth2-resource";

     

     /**

      * 以代码形式配置资源服务器id,配置文件配置不生效

      */

     @Override

    public void configure(ResourceServerSecurityConfigurer resources) {

        resources.resourceId(DEMO_RESOURCE_ID).stateless(true);

    }

     

     @Override

    public void configure(HttpSecurity http) throws Exception {   

          http.

             csrf().disable()

             .authorizeRequests()

               .anyRequest().authenticated()

             .and()

             .httpBasic();

    }

}

 

4、使用四种方式获取的token可以使用url后带上access_token或添加Authorization的http头bearer方式向资源服务器发出访问资源请求,但是如果认证服务器也是资源服务器,无法使用像资源服务器一样访问认证服务器。

 

这个问题困扰我很久,命名单独的资源服务可以使用http携带authentication头(bearer + token)或在访问url后面+?access_token方式访问对应资源,那么既然认证服务器也是资源服务器,按道理讲应该是一样的(这里有却别就是认证服务器不需要配置资源服务器相关信息,因为在同一进程),但是实际上不行,通过调试发现如下代码(BasicAuthenticationFilter):

@Override

     protected void doFilterInternal(HttpServletRequest request,

              HttpServletResponse response, FilterChain chain)

                        throws IOException, ServletException {

          final boolean debug = this.logger.isDebugEnabled();

          String header = request.getHeader("Authorization");

          if (header == null || !header.startsWith("Basic ")) {

              chain.doFilter(request, response);

              return;

          }

          try {

              String[] tokens = extractAndDecodeHeader(header, request);

              assert tokens.length == 2;

              String username = tokens[0];

              if (debug) {

                   this.logger

                             .debug("Basic Authentication Authorization header found for user '"

                                      + username + "'");

              }

              if (authenticationIsRequired(username)) {

                   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(

                             username, tokens[1]);

                   authRequest.setDetails(

                             this.authenticationDetailsSource.buildDetails(request));

                   Authentication authResult = this.authenticationManager

                             .authenticate(authRequest);

                   if (debug) {

                        this.logger.debug("Authentication success: " + authResult);

                   }

                    SecurityContextHolder.getContext().setAuthentication(authResult);

                   this.rememberMeServices.loginSuccess(request, response, authResult);

                   onSuccessfulAuthentication(request, response, authResult);

              }

          }

          catch (AuthenticationException failed) {

              SecurityContextHolder.clearContext();

              if (debug) {

                   this.logger.debug("Authentication request for failed: " + failed);

              }

              this.rememberMeServices.loginFail(request, response);

              onUnsuccessfulAuthentication(request, response, failed);

              if (this.ignoreFailure) {

                   chain.doFilter(request, response);

              }

              else {

                   this.authenticationEntryPoint.commence(request, response, failed);

              }

              return;

          }

          chain.doFilter(request, response);

     }

这里直接获取http请求头中是否存在authentication资源,如果不存在直接退出!!为啥一定要特立独行,搞出和资源服务器不一样的验证方式?(这个问题有待后续查证),这里验证发现要访问认证服务器的资源,必须使用http请求头携带头authentication方式,且对应的值为Basic值(Basic的值必须为用户名和密码的组合值-md5吧),否则无法通过校验!

 

 

核心过滤器 OAuth2AuthenticationProcessingFilter

 

    回顾一下我们之前是如何携带token访问受限资源的: http://localhost:7006/order/1?access_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0 

唯一的身份凭证,便是这个access_token,携带它进行访问,会进入OAuth2AuthenticationProcessingFilter之中,其核心代码如下:

 

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain){

 

    final HttpServletRequest request = (HttpServletRequest) req;

    final HttpServletResponse response = (HttpServletResponse) res;

 

    try {

        // 从请求中取出身份信息,即access_token

        Authentication authentication = tokenExtractor.extract(request);

 

        if (authentication == null) {

            ...

        }

        else {

            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());

            if (authentication instanceof AbstractAuthenticationToken) {

                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;

                needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));

            }

            // 认证身份

            Authentication authResult = authenticationManager.authenticate(authentication);

            ...

            eventPublisher.publishAuthenticationSuccess(authResult);

            // 将身份信息绑定到SecurityContextHolder中

            SecurityContextHolder.getContext().setAuthentication(authResult);

        }

    }

    catch (OAuth2Exception failed) {

        ...

        return;

    }

 

    chain.doFilter(request, response);

}

 

 

    其中涉及到了两个关键的类TokenExtractor,AuthenticationManager。相信后者这个接口大家已经不陌生,但前面这个类之前还未出现在我们的视野中。在之前的OAuth2核心过滤器中出现的AuthenticationManager其实在我们意料之中,携带access_token必定得经过身份认证,但是在我们debug进入其中后,发现了一个出乎意料的事,AuthenticationManager的实现类并不是我们在前面文章中聊到的常用实现类ProviderManager,而是OAuth2AuthenticationManager。

 

    回顾我们第一篇文章的配置,压根没有出现过这个OAuth2AuthenticationManager,并且它脱离了我们熟悉的认证流程(第二篇文章中的认证管理器UML图是一张经典的spring security结构类图),它直接重写了容器的顶级身份认证接口,内部维护了一个ClientDetailService和ResourceServerTokenServices,这两个核心类在 Re:从零开始的Spring Security Oauth2(二)有分析过。在ResourceServerSecurityConfigurer的小节中我们已经知晓了它是如何被框架自动配置的,这里要强调的是OAuth2AuthenticationManager是密切与token认证相关的,而不是与获取token密切相关的,判别身份的关键代码如下:

 

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

 

    ...

    String token = (String) authentication.getPrincipal();

    // 最终还是借助tokenServices根据token加载身份信息

    OAuth2Authentication auth = tokenServices.loadAuthentication(token);

    ...

 

    checkClientDetails(auth);

 

    if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {

        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();

        ...

    }

    auth.setDetails(authentication.getDetails());

    auth.setAuthenticated(true);

    return auth;

}

 

说到tokenServices这个密切与token相关的接口,这里要强调下,避免产生误解。tokenServices分为两类,一个是用在AuthenticationServer端,第二篇文章中介绍的

 

public interface AuthorizationServerTokenServices {

    //创建token

    OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;

    //刷新token

    OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)

            throws AuthenticationException;

    //获取token

    OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);

}

 

而在ResourceServer端有自己的tokenServices接口:

 

public interface ResourceServerTokenServices {

 

    //根据accessToken加载客户端信息

    OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

 

    //根据accessToken获取完整的访问令牌详细信息。

    OAuth2AccessToken readAccessToken(String accessToken);

 

}

 

具体内部如何加载,和AuthorizationServer大同小异,只是从tokenStore中取出相应身份的流程有点区别,不再详细看实现类了。

 

TokenExtractor(了解)

这个接口只有一个实现类,而且代码非常简单:

public class BearerTokenExtractor implements TokenExtractor {

 

    private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);

 

    @Override

    public Authentication extract(HttpServletRequest request) {

        String tokenValue = extractToken(request);

        if (tokenValue != null) {

            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");

            return authentication;

        }

        return null;

    }

 

    protected String extractToken(HttpServletRequest request) {

        // 首先从头部获取token

        String token = extractHeaderToken(request);

 

        // bearer type allows a request parameter as well

        if (token == null) {

            ...

            // 从requestParameter中获取token

        }

 

        return token;

    }

 

    /**

     * Extract the OAuth bearer token from a header.

     */

    protected String extractHeaderToken(HttpServletRequest request) {

        Enumeration<String> headers = request.getHeaders("Authorization");

        while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)

            ...

            //从Header中获取token

        }

        return null;

    }

 

}

 

 

它的作用在于分离出请求中包含的token。也启示了我们可以使用多种方式携带token。

1 在Header中携带

http://localhost:8080/order/1

Header:

Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135

 

2 拼接在url中作为requestParam

http://localhost:8080/order/1?access_token=f732723d-af7f-41bb-bd06-2636ab2be135

 

3 在form表单中携带

http://localhost:8080/order/1

form param:

access_token=f732723d-af7f-41bb-bd06-2636ab2be135

 

快来成为我的朋友或合作伙伴,一起交流,一起进步!
QQ群:961179337
微信:lixiang6153
邮箱:lixx2048@163.com
公众号:IT技术快餐
更多资料等你来拿!

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝壳里的沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值