本文用来说明vue如何使用PKCE模式访问受oauth2保护的资源。Demo示例代码在github里

概念介绍

什么是PKCE? 这里有详细的介绍。PKCE是Proof Key for Code Exchange的缩写。简单来说就是获取Token过程中,利用两个数值比较来验证请求者是否是最终用户,防止攻击者盗用Token。 它提高了公有云环境下的单页面运用或APP获取Token的安全性,也能应用在私有云环境里。我们可利用它来实现SSO。

具体步骤简单描述如下:

第一步:先随机生成一个32位长的code_verifier,然后运用hash算法s256加密得到一个code_challenge。js算法实现可参考这里

第二步:客户端向oauth2认证服务器申请认证码code, 并带着code_challenge;

第三步:客户端利用返回的code和code_verifier来向oauth2服务器申请token;

第四步:客户端通过在header里加bearer token就能访问受限资源了;

Demo使用说明

代码分两部分:authentication和html。authentication是oauth2认证和资源服务,利用spring security实现; html利用spring boot+thymleaf+vue来模拟一个客户端。

  • 准备
    需1.8(含)以上的JDK和Maven。 将Maven的命令mvn配到机器环境的path里,同时在环境里配置JAVA_HOME指向JDK所在目录。

  • 启动

  • 运行认证和资源服务

cd authentication
mvn -DskipTests clean package
java -jar target/authentication-0.0.1-SNAPSHOT.jar
  • 运行客户端
  cd html
  mvn -DskipTests clean package 
  java -jar target/ui-0.0.1-SNAPSHOT.jar
  • 执行
    authentication服务端口是30000, 客户端端口是30010. 在chrome浏览器输入http://127.0.0.1:30010, 即进入客户端。如下图所示:
    客户端示例
    “非认证点击” 按钮演示未获token访问接口的效果,上面会有出错提示。
    “认证点击” 按钮演示PKCE获取token后访问接口的效果,如成功,上面会有一段json信息。
    备注】获取code需表单认证,用户名和密码为tom和sonia 这在authentication里配置

这两个按钮调用的是authentication服务里的接口,该接口受oauth2保护。接口定义如下:

    //这是在authentication的com.ghy.vo.authentication.controller.DemoController里
    @RequestMapping(value = "/res/showData")
    public Object showData2(){
        Map<String,String> map = new HashMap<String,String>();
        map.put("t1","this is test1");
        map.put("t2","this is test2");
        return map;
    }

    //这是在authentication的资源配置,在com.ghy.vo.authentication.config.OAuth2Server里将/res/*定义为受oauth2保护
    @Configuration
    @EnableResourceServer
    protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
        ......

        @Override
        public void configure(HttpSecurity http) throws Exception {
            final String[] urlPattern = {"/res/**"};
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .requestMatchers().antMatchers(urlPattern)
                .and()
                .authorizeRequests()
                .antMatchers(urlPattern)
                .access("#oauth2.hasScope('read') and hasRole('ROLE_USER')")
            ;
        }

下图是成功的显示:
认证点击效果截图
如图所示,标红的地方表明了获取code, token和调用接口的network过程。

实现过程

authentication工程

认证服务

见OAuth2Server类,通过@EnableAuthorizationServer定义认证服务器,将oauth2认证模式"authorization_code"加载到内存里。如下:

      @Override
      public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
          clients.inMemory()
                  .withClient("vo_client_id")
                  .secret("{noop}")
                  .redirectUris("http://127.0.0.1:30010")
                  .authorizedGrantTypes("authorization_code")
                  .scopes("read")
                  .autoApprove(true)
                  ;
      }
引入PKCE认证模式

在OAuth2Server类里进行配置,引入相关服务和tokenGranter,如下:

      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
          endpoints
                  .tokenStore(tokenStore)
                  .authorizationCodeServices(new PkceAuthorizationCodeServices(endpoints.getClientDetailsService(), passwordEncoder))
                  .tokenGranter(tokenGranter(endpoints));
PKCE实现

见pkce包里

  • CodeChallengeMethod类用来codeVerifier和codeChallenge比较
  • PkceAuthorizationCodeServices类用于调度
  • PkceAuthorizationCodeTokenGranter类用于从request请求中获取token所需信息
  • PkceProtectedAuthentication类定义PKCE认证对象信息
全局跨域访问和Token调用设置

见CrosFilter类

      @Configuration
      @Order(5)
      public class CrosFilter implements Filter {
      
          @Override
          public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
              HttpServletRequest request = (HttpServletRequest) req;
              HttpServletResponse response = (HttpServletResponse) res;
      
              response.setHeader("Access-Control-Allow-Origin", "*");
              response.setHeader("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, DELETE, OPTIONS");
              response.setHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-Type, Authorization");
              response.setHeader("Access-Control-Max-Age", "3600");
              response.setHeader("Access-Control-Allow-Credentials", "true");
      
              String method = request.getMethod();
              if ("OPTIONS".equals(method)) {
                  response.setStatus(HttpStatus.OK.value());
              } else {
                  chain.doFilter(req, res);
              }
          }
      }

跨域设置遇到的一些坑:曾配过CorsConfig类和SecurityConfiguration类的corsConfigurationSource()方法,都没效果,特别是从client调用token时都不行。将代码放在Demo里,是为了记录过程。已将注解注释掉,代码没有使用。

最后经过摸索,最终发现用filter方式是可行的。另外一种方式是用spring拦截器模式。Demo里没有描述。我在下面参考列出来,供学习。

filter实现参见CrosFilter类。分两部分,第一部分是跨域设置和对Header等控制,第二部分是当请求是options时,则返回200。这是配合axios获取token时用的

html工程

主要实现见test.html, 其中

  • hash加密算法通过crypto-js.min.js来引入实现
  • pkce.js用来生成code_vefivier和code_challenge
  • 获取token,通过Qs实现options的简单调用
  • "认证点击"按钮的调用流程如下图,主要用到了js的异步回调技术;
    点击认证

可改进的地方

  • 认证信息从缓存挪到DB或redis里
  • client端还要加logout等清理token等方法

参考

跨域设置过滤器方案
服务器端PKCE代码参考实现

Logo

前往低代码交流专区

更多推荐