美文网首页服务端开发实战
Spring Security OAuth2实现使用JWT

Spring Security OAuth2实现使用JWT

作者: AaronSimon | 来源:发表于2018-11-14 22:54 被阅读70次

    Spring Security Oauth2-授权码模式(Finchley版本)一文中介绍了OAuth2的授权码模式的实现,本文将在这篇文章的基础上使用JWT生成token。

    一、准备工作

    1. 下载代码
      大家可以在github上下载Spring Security Oauth2-授权码模式(Finchley版本)的源码。
    2. 添加JWT依赖
      授权服务和资源服务是两个分开的服务,需要在两个服务中添加JWT依赖
      <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-jwt</artifactId>
        <version>1.0.7.RELEASE</version>
      </dependency>
      

    二、案例介绍

    JWT认证提供了对称加密和非对称加密的实现。

    2.1 对称加密

    2.1.1 授权服务

    (1) 定义token的生成方式
    AccessToken转换器用来定义token的生成方式,这里使用JWT生成token

      @Bean
      public TokenStore tokenStore(){
        return new JwtTokenStore(accessTokenConverter());
      }
    
      @Bean
      public JwtAccessTokenConverter accessTokenConverter() {
        final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
      }
    

    (2) 告知spring security token的生成方式

    /**
       * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
       * @param endpoints
       * @throws Exception
       */
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //指定认证管理器
        endpoints.authenticationManager(authenticationManager);
        //指定token存储位置
        endpoints.tokenStore(tokenStore());
        // token生成方式
        endpoints.accessTokenConverter(accessTokenConverter());
        endpoints.userDetailsService(userDetailsService);
      }
    

    2.1.2 资源服务

    资源服务的配置与授权服务大致相同

    /**
     * 资源服务器配置
     *
     * @author simon
     * @create 2018-11-14 11:03
     **/
    @Configuration
    @EnableResourceServer
    public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
      @Bean
      public JwtAccessTokenConverter accessTokenConverter(){
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
      }
    
      @Bean
      public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
      }
    
      @Override
      public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        resources.tokenServices(defaultTokenServices;
      }
    }
    

    2.2 非对称加密

    使用非对称密钥(公钥和私钥)来执行签名过程,需要先生成一个证书并导出公钥。

    2.2.1 生成证书

    (1) 生成JKS Java KeyStore文件
    使用命令行工具keytool生成证书

    keytool -genkeypair -alias mytest -keyalg RSA -keypass mypass -keystore mytest.jks -storepass mypass
    

    此命令将生成一个名为mytest.jks的文件,其中包含我们的密钥(公钥和私钥)。

    (2) 导出公钥
    我们可以使用下面的命令从生成的JKS中导出我们的公钥:

    keytool -list -rfc --keystore mytest.jks | openssl x509 -inform pem -pubkey
    

    结果如下:

    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
    OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
    /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
    DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
    xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
    lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
    eQIDAQAB
    -----END PUBLIC KEY-----
    -----BEGIN CERTIFICATE-----
    MIIDCzCCAfOgAwIBAgIEGtZIUzANBgkqhkiG9w0BAQsFADA2MQswCQYDVQQGEwJ1
    czELMAkGA1UECBMCY2ExCzAJBgNVBAcTAmxhMQ0wCwYDVQQDEwR0ZXN0MB4XDTE2
    MDMxNTA4MTAzMFoXDTE2MDYxMzA4MTAzMFowNjELMAkGA1UEBhMCdXMxCzAJBgNV
    BAgTAmNhMQswCQYDVQQHEwJsYTENMAsGA1UEAxMEdGVzdDCCASIwDQYJKoZIhvcN
    AQEBBQADggEPADCCAQoCggEBAICCtlreMdhLQ5eNQu736TrDKrmTMjsrXjtkbFXj
    Cxf4VyHmL4nCq9EkM1ZKHRxAQjIhl0A8+aa4o06t0Rz8tv+ViQQmKu8h4Ey77KTM
    urIr1zezXWBOyOaV6Pyh5OJ8/hWuj9y/Pi/dBP96sH+o9wylpwICRUWPAG0mF7dX
    eRC4iBtf4BKswtH2ZjYYX6wbccFl65aVA09Cn739EFZj0ccQi10/rRHtbHlhhKnj
    iy+b10S6ps2XAXtUWfZEEJuN/mvUJ+YnEkZw30wHrENwq5QFiSpdpHFlNR8CasPn
    WUUmdV+JBFzTMsz3TwWxplOjB3YacsCO0imU+5l+AQ51CnkCAwEAAaMhMB8wHQYD
    VR0OBBYEFOGefUBGquEX9Ujak34PyRskHk+WMA0GCSqGSIb3DQEBCwUAA4IBAQB3
    1eLfNeq45yO1cXNl0C1IQLknP2WXg89AHEbKkUOA1ZKTOizNYJIHW5MYJU/zScu0
    yBobhTDe5hDTsATMa9sN5CPOaLJwzpWV/ZC6WyhAWTfljzZC6d2rL3QYrSIRxmsp
    /J1Vq9WkesQdShnEGy7GgRgJn4A8CKecHSzqyzXulQ7Zah6GoEUD+vjb+BheP4aN
    hiYY1OuXD+HsdKeQqS+7eM5U7WW6dz2Q8mtFJ5qAxjY75T0pPrHwZMlJUhUZ+Q2V
    FfweJEaoNB9w9McPe1cAiE+oeejZ0jq0el3/dJsx3rlVqZN+lMhRJJeVHFyeb3XF
    lLFCUGhA7hxn2xf3x1JW
    -----END CERTIFICATE-----
    

    这里我们只需要复制公钥到资源服务的resources目录下的public.txt 文件中

    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAgIK2Wt4x2EtDl41C7vfp
    OsMquZMyOyteO2RsVeMLF/hXIeYvicKr0SQzVkodHEBCMiGXQDz5prijTq3RHPy2
    /5WJBCYq7yHgTLvspMy6sivXN7NdYE7I5pXo/KHk4nz+Fa6P3L8+L90E/3qwf6j3
    DKWnAgJFRY8AbSYXt1d5ELiIG1/gEqzC0fZmNhhfrBtxwWXrlpUDT0Kfvf0QVmPR
    xxCLXT+tEe1seWGEqeOLL5vXRLqmzZcBe1RZ9kQQm43+a9Qn5icSRnDfTAesQ3Cr
    lAWJKl2kcWU1HwJqw+dZRSZ1X4kEXNMyzPdPBbGmU6MHdhpywI7SKZT7mX4BDnUK
    eQIDAQAB
    -----END PUBLIC KEY-----
    

    2.2.2 授权服务

    将刚刚生成的证书复制到授权服务器的resources目录下。配置JwtAccessTokenConverter使用mytest.jks 中的KeyPair

    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
        return converter;
      }
    

    2.2.3 资源服务

    配置资源服务器使用公钥:

    @Bean
    public JwtAccessTokenConverter accessTokenConverter(){
      JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
      Resource resource =  new ClassPathResource("public.txt");
      String publicKey;
      try {
        publicKey = inputStream2String(resource.getInputStream());
      } catch (final IOException e) {
        throw new RuntimeException(e);
      }
      converter.setVerifierKey(publicKey);
      return converter;
    }
    
    String inputStream2String(InputStream is) throws IOException {
      BufferedReader in = new BufferedReader(new InputStreamReader(is));
      StringBuffer buffer = new StringBuffer();
      String line = "";
      while ((line = in.readLine()) != null) {
        buffer.append(line);
      }
      return buffer.toString();
    }
    

    2.3 添加额外信息

    额外信息的添加与加密方式无关

    2.3.1 自定义生成token携带的信息

    可以自定义一个TokenEnhancer将额外的信息添加到token中。TokenEnhancer 接口提供public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication)方法用于token信息的添加
    (1) 自定义TokenEnhancer

    /**
     * 自定义token生成携带的信息
     *
     * @author simon
     * @create 2018-11-14 10:16
     **/
    public class CustomTokenEnhancer implements TokenEnhancer {
      @Override
      public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        final Map<String, Object> additionalInfo = new HashMap<>();
        //获取登录信息
        UserDetails user = (UserDetails) oAuth2Authentication.getUserAuthentication().getPrincipal();
        additionalInfo.put("userName", user.getUsername());
        additionalInfo.put("authorities", user.getAuthorities());
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(additionalInfo);
        return oAuth2AccessToken;
      }
    }
    

    (2) 将自定义的TokenEnhancer加入到TokenEnhancerChain中

    /**
       * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
       * @param endpoints
       * @throws Exception
       */
      @Override
      public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        //指定认证管理器
        endpoints.authenticationManager(authenticationManager);
        //指定token存储位置
        endpoints.tokenStore(tokenStore());
    
        endpoints.accessTokenConverter(accessTokenConverter());
        endpoints.userDetailsService(userDetailsService);
        //自定义token生成方式
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(customerEnhancer(),accessTokenConverter()));
        endpoints.tokenEnhancer(tokenEnhancerChain);
    

    2.3.2 自定义token中添加的信息

    (1)授权服务自定义JwtAccessTokenConverte
    JwtAccessTokenConverter是我们用来生成token的转换器,所以我们需要配置这里面的部分信息来实现token中携带额外的信息。

    JwtAccessTokenConverter默认使用DefaultAccessTokenConverter来处理token的生成、转换、获取。DefaultAccessTokenConverter中使用UserAuthenticationConverter来处理token与userinfo的获取、转换。因此我们需要重写下UserAuthenticationConverter对应的转换方法就可以

    /**
     * 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
     * 默认使用DefaultAccessTokenConverter 这个装换器
     * DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,默认只是放入user_name
     * <p>
     * 自定义这个方法,加入了额外的信息
     * <p>
     * @author simon
     * @create 2018-11-14 10:26
     **/
    public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {
    
      public CustomerAccessTokenConverter() {
        super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
      }
    
      private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter{
        @Override
        public Map<String, ?> convertUserAuthentication(Authentication authentication) {
          LinkedHashMap <String, Object> response = new LinkedHashMap <>();
          response.put("details", authentication.getDetails());
          response.put("test","hello");
          if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
            response.put("authorities", AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
          }
          return response;
        }
      }
    }
    

    (2) 授权服务告诉JwtAccessTokenConverter替换默认的方式

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
      final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
      KeyStoreKeyFactory keyStoreKeyFactory =
              new KeyStoreKeyFactory(new ClassPathResource("mytest.jks"), "mypass".toCharArray());
      converter.setKeyPair(keyStoreKeyFactory.getKeyPair("mytest"));
      converter.setAccessTokenConverter(new CustomerAccessTokenConverter());
      return converter;
    }
    

    (3)资源服务自定义JwtAccessTokenConverte

    /**
     * 自定义CustomerAccessTokenConverter 这个类的作用主要用于AccessToken的转换,
     * 默认使用DefaultAccessTokenConverter 这个装换器
     * DefaultAccessTokenConverter有个UserAuthenticationConverter,这个转换器作用是把用户的信息放入token中,
     * 默认只是放入username
     * <p>
     * 自定义了下这个方法,加入了额外的信息
     * <p>
     * @author simon
     * @create 2018-11-14 10:26
     **/
    public class CustomerAccessTokenConverter extends DefaultAccessTokenConverter {
    
      public CustomerAccessTokenConverter() {
        super.setUserTokenConverter(new CustomerUserAuthenticationConverter());
      }
    
      private class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter {
    
        // 资源服务获得自定义信息  
        @Override
        public Authentication extractAuthentication(Map<String, ?> map) {
          Collection <? extends GrantedAuthority> authorities = this.getAuthorities(map);
          return new UsernamePasswordAuthenticationToken(map, "N/A", authorities);
        }
    
        private Collection<? extends GrantedAuthority> getAuthorities(Map<String, ?> map) {
          if (!map.containsKey("authorities")) {
            return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.arrayToCommaDelimitedString(new String[]{"USER"}));
          } else {
            Object authorities = map.get("authorities");
            if (authorities instanceof String) {
              return AuthorityUtils.commaSeparatedStringToAuthorityList((String) authorities);
            } else if (authorities instanceof Collection) {
              return AuthorityUtils.commaSeparatedStringToAuthorityList(StringUtils.collectionToCommaDelimitedString((Collection) authorities));
            } else {
              throw new IllegalArgumentException("Authorities must be either a String or a Collection");
            }
          }
        }
    
      }
    }
    

    2.4 测试

    启动服务

    2.4.1 获取code

    浏览器访问http://localhost:8080/oauth/authorize?response_type=code&client_id=client1&redirect_uri=http://baidu.com

    进入登录页面,输入用户名:admin;密码:admin。

    登录成功进入授权页面,点击授权,获得code
    https://www.baidu.com/?code=NW8eB1

    2.4.2 获取token

    使用POSTMAN发送post请求获取token


    postman请求

    2.4.3 访问资源服务获取资源

    使用POSTMAN发送get请求获取资源


    postman请求

    2.4.4 解析token

    新增测试类解析token

    @Test
      public void contextLoads() {
        //填写token
        String token = "";
        Jwt jwt = JwtHelper.decode(token);
        System.err.println(jwt.toString());
      }
    

    解析后的信息如下:

    {"alg":"RS256","typ":"JWT"} {"test":"hello","scope":["test"],"details":{"remoteAddress":"0:0:0:0:0:0:0:1","sessionId":"34B189EA6F1DA4834E5AEA31E91A2460"},"exp":1542277115,"userName":"admin","authorities":[{"authority":"USER"}],"jti":"8e4a72d3-affb-4977-b174-cb9ee4f2e08b","client_id":"client1"} [256 crypto bytes]
    

    结果中包含添加的额外信息

    github源码下载

    相关文章

      网友评论

        本文标题:Spring Security OAuth2实现使用JWT

        本文链接:https://www.haomeiwen.com/subject/gztsfqtx.html