1. 程式人生 > >Spring Security OAuth2實現使用JWT

Spring Security OAuth2實現使用JWT

Spring Security Oauth2-授權碼模式(Finchley版本)一文中介紹了OAuth2的授權碼模式的實現,本文將在這篇文章的基礎上使用JWT生成token。關於JWT的介紹可以參考JWT詳解

一、準備工作

  1. 新增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。

2.4.2 獲取token

使用POSTMAN傳送post請求獲取token
postman請求

2.4.3 訪問資源服務獲取資源

使用POSTMAN傳送get請求獲取資源
get請求

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]

結果中包含新增的額外資訊