Spring Security OAuth2自定義令牌配置


在前面幾節中,我們獲取到的令牌都是基於Spring Security OAuth2默認配置生成的,Spring Security允許我們自定義令牌配置,比如不同的client_id對應不同的令牌,令牌的有效時間,令牌的存儲策略等;我們也可以使用JWT來替換默認的令牌。

 

自定義令牌配置

我們讓認證服務器AuthorizationServerConfig繼承AuthorizationServerConfigurerAdapter,並重寫它的configure(ClientDetailsServiceConfigurer clients)方法:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

  ......

  @Autowired
  private AuthenticationManager authenticationManager;
  @Autowired
  private UserDetailService userDetailService;

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints.authenticationManager(authenticationManager)
              .userDetailsService(userDetailService);
  }

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      clients.inMemory()
              .withClient("test1")
              .secret("test1111")
              .accessTokenValiditySeconds(3600)
              .refreshTokenValiditySeconds(864000)
              .scopes("all", "a", "b", "c")
              .authorizedGrantTypes("password")
          .and()
              .withClient("test2")
              .secret("test2222")
              .accessTokenValiditySeconds(7200);
  }
}

認證服務器在繼承了AuthorizationServerConfigurerAdapter適配器后,需要重寫configure(AuthorizationServerEndpointsConfigurer endpoints)方法,指定 AuthenticationManagerUserDetailService

創建一個新的配置類SecurityConfig,在里面注冊我們需要的AuthenticationManagerBean:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
  }
}

 

此外,重寫configure(ClientDetailsServiceConfigurer clients)方法主要配置了:

  1. 定義兩個client_id,及客戶端可以通過不同的client_id來獲取不同的令牌;

  2. client_id為test1的令牌有效時間為3600秒,client_id為test2的令牌有效時間為7200秒;

  3. client_id為test1的refresh_token(下面會介紹到)有效時間為864000秒,即10天,也就是說在這10天內都可以通過refresh_token來換取新的令牌;

  4. 在獲取client_id為test1的令牌的時候,scope只能指定為all,a,b或c中的某個值,否則將獲取失敗;

  5. 只能通過密碼模式(password)來獲取client_id為test1的令牌,而test2則無限制。

啟動項目,演示幾個效果。啟動項目后使用密碼模式獲取test1的令牌:

和前面介紹的那樣,頭部需要傳入test1:test1111經過base64加密后的值:

點擊發送后,意外的返回了錯誤!

控制台輸出了 Encoded password does not look like BCrypt 的告警。

查閱資料后發現,在新版本的spring-cloud-starter-oauth2指定client_secret的時候需要進行加密處理:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

  ......

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      clients.inMemory()
              .withClient("test1")
              .secret(new BCryptPasswordEncoder().encode("test1111"))
              .accessTokenValiditySeconds(3600)
              .refreshTokenValiditySeconds(864000)
              .scopes("all", "a", "b", "c")
              .authorizedGrantTypes("password")
          .and()
              .withClient("test2")
              .secret(new BCryptPasswordEncoder().encode("test2222"))
              .accessTokenValiditySeconds(7200);
  }
}

在前面自定義登錄認證獲取令牌一節中,我們在MyAuthenticationSucessHandler判斷了client_secret的值是否正確。由於我們這里client_secret加密了,所以判斷邏輯需要調整為下面這樣:

...

else if (!passwordEncoder.matches(clientSecret, clientDetails.getClientSecret())) {
  throw new UnapprovedClientAuthenticationException("clientSecret不正確");
}
...

言歸正傳,修改后重啟項目,重新使用密碼模式獲取令牌:

{
  "access_token": "c23376b0-efa3-4905-8356-8c9583c2a2a0",
  "token_type": "bearer",
  "expires_in": 3599,
  "scope": "all"
}

 

可以看到expires_in的時間是我們定義的3600秒。

將scope指定為d看看會有什么結果:

由於我們定義了只能通過密碼模式來獲取client_id為test1的令牌,所以我們看看將grant_type改為xxoo會有什么結果:

默認令牌是存儲在內存中的,我們可以將它保存到第三方存儲中,比如Redis。

創建TokenStoreConfig

@Configuration
public class TokenStoreConfig {

  @Autowired
  private RedisConnectionFactory redisConnectionFactory;

  @Bean
  public TokenStore redisTokenStore (){
      return new RedisTokenStore(redisConnectionFactory);
  }
}

 

然后在認證服務器里指定該令牌存儲策略。重寫configure(AuthorizationServerEndpointsConfigurer endpoints)方法:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private TokenStore redisTokenStore;

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints.authenticationManager(authenticationManager)
          .tokenStore(redisTokenStore);
  }

  ......
}

 

重啟項目獲取令牌后,查看Redis中是否存儲了令牌相關信息:

可以看到,令牌信息已經存儲到Redis里了。

使用JWT替換默認令牌

使用JWT替換默認的令牌(默認令牌使用UUID生成)只需要指定TokenStore為JwtTokenStore即可。

創建一個JWTokenConfig配置類:

@Configuration
public class JWTokenConfig {

  @Bean
  public TokenStore jwtTokenStore() {
      return new JwtTokenStore(jwtAccessTokenConverter());
  }

  @Bean
  public JwtAccessTokenConverter jwtAccessTokenConverter() {
      JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
      accessTokenConverter.setSigningKey("test_key"); // 簽名密鑰
      return accessTokenConverter;
  }
}

 

簽名密鑰為test_key。在配置類里配置好JwtTokenStore后,我們在認證服務器里指定它:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
  @Autowired
  private TokenStore jwtTokenStore;
  @Autowired
  private JwtAccessTokenConverter jwtAccessTokenConverter;

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      endpoints.authenticationManager(authenticationManager)
              .tokenStore(jwtTokenStore)
              .accessTokenConverter(jwtAccessTokenConverter);
  }

  ......
}

 

重啟服務獲取令牌,系統將返回如下格式令牌:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjE1MzI1MDEsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6IjJkZjY4MGNhLWFmN2QtNGU4Ni05OTdhLWI1ZmVkYzQxZmYwZSIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOltdfQ.dZ4SeuU3VWnSJKy5vELGQ0YkVRddcEydUlJAVovlycg",
  "token_type": "bearer",
  "expires_in": 3599,
  "scope": "all",
  "jti": "2df680ca-af7d-4e86-997a-b5fedc41ff0e"
}

 

access_token中的內容復制到https://jwt.io/網站解析下:

使用這個token訪問/index可以成功獲取到信息,這里就不演示了。

拓展JWT

上面的Token解析得到的PAYLOAD內容為:

{
"exp": 1561532501,
"user_name": "mrbird",
"authorities": [
  "admin"
],
"jti": "2df680ca-af7d-4e86-997a-b5fedc41ff0e",
"client_id": "test1",
"scope": ["all"]
}

 

如果想在JWT中添加一些額外的信息,我們需要實現TokenEnhancer(Token增強器):

public class JWTokenEnhancer implements TokenEnhancer {
  @Override
  public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
      Map<String, Object> info = new HashMap<>();
      info.put("message", "hello world");
      ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
      return oAuth2AccessToken;
  }
}

 

我們在Token中添加了message: hello world信息。然后在JWTokenConfig里注冊該Bean:

@Configuration
public class JWTokenConfig {
  ......

  @Bean
  public TokenEnhancer tokenEnhancer() {
      return new JWTokenEnhancer();
  }
}

 

最后在認證服務器里配置該增強器:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

  @Autowired
  private TokenStore jwtTokenStore;
  @Autowired
  private JwtAccessTokenConverter jwtAccessTokenConverter;
  @Autowired
  private TokenEnhancer tokenEnhancer;

  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
      TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
      List<TokenEnhancer> enhancers = new ArrayList<>();
      enhancers.add(tokenEnhancer);
      enhancers.add(jwtAccessTokenConverter);
      enhancerChain.setTokenEnhancers(enhancers);

      endpoints.tokenStore(jwtTokenStore)
              .accessTokenConverter(jwtAccessTokenConverter)
              .tokenEnhancer(enhancerChain);
  }
  ......
}

 

重啟項目,再次獲取令牌,系統返回:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6W10sImV4cCI6MTU2MTUzNDQ1MCwibWVzc2FnZSI6ImhlbGxvIHdvcmxkIiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiY2E1NDA3ZTEtMzBmZS00MzA3LThiYmItYjU2NGY5Y2ViOWUzIiwiY2xpZW50X2lkIjoidGVzdDEifQ.qW92ssifRKi_rxX2XIH2u4D5IUPVcKECv812hTpuUuA",
  "token_type": "bearer",
  "expires_in": 3599,
  "message": "hello world",
  "jti": "ca5407e1-30fe-4307-8bbb-b564f9ceb9e3"
}

 

可以看到,在返回的JSON內容里已經多了我們添加的message信息,此外將access_token復制到jwt.io網站解析,內容如下:

{
"user_name": "mrbird",
"scope": [],
"exp": 1561534450,
"message": "hello world",
"authorities": [
  "admin"
],
"jti": "ca5407e1-30fe-4307-8bbb-b564f9ceb9e3",
"client_id": "test1"
}

 

解析后的JWT也包含了我們添加的message信息。

Java中解析JWT

要在Java代碼中解析JWT,需要添加如下依賴:

<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>

 

修改/index

@GetMapping("index")
public Object index(@AuthenticationPrincipal Authentication authentication, HttpServletRequest request) {
  String header = request.getHeader("Authorization");
  String token = StringUtils.substringAfter(header, "bearer ");

  return Jwts.parser().setSigningKey("test_key".getBytes(StandardCharsets.UTF_8)).parseClaimsJws(token).getBody();
}

 

signkey需要和JwtAccessTokenConverter中指定的簽名密鑰一致。重啟項目,獲取令牌后訪問/index,輸出內容如下:

{
  "exp": 1561557893,
  "user_name": "mrbird",
  "authorities": [
      "admin"
  ],
  "jti": "3c29f89a-1344-40d8-bcfd-1b9c45fb8b89",
  "client_id": "test1",
  "scope": [
      "all"
  ]
}

 

刷新令牌

令牌過期后我們可以使用refresh_token來從系統中換取一個新的可用令牌。但是從前面的例子可以看到,在認證成功后返回的JSON信息里並沒有包含refresh_token,要讓系統返回refresh_token,需要在認證服務器自定義配置里添加如下配置:

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

......

  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
      clients.inMemory()
              .withClient("test1")
              .secret(new BCryptPasswordEncoder().encode("test1111"))
              .authorizedGrantTypes("password", "refresh_token")
              .accessTokenValiditySeconds(3600)
              .refreshTokenValiditySeconds(864000)
              .scopes("all", "a", "b", "c")
          .and()
              .withClient("test2")
              .secret(new BCryptPasswordEncoder().encode("test2222"))
              .accessTokenValiditySeconds(7200);
  }
}

 

授權方式需要加上refresh_token,除了四種標准的OAuth2獲取令牌方式外,Spring Security OAuth2內部把refresh_token當作一種拓展的獲取令牌方式。

通過上面的配置,使用test1這個client_id獲取令牌時將返回refresh_token,refresh_token的有效期為10天,即10天之內都可以用它換取新的可用令牌。

重啟項目,認證成功后,系統返回如:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjE1NTgwOTcsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6Ijg2NTdhMDBlLTFiM2MtNDA5NS1iMjNmLTJlMjUxOWExZmUwMiIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOlsiYWxsIl19.hrxKOz3NKY6Eq8k5QeOqKhXUQ4aAbicrb6J5y-LBRA0",
  "token_type": "bearer",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiODY1N2EwMGUtMWIzYy00MDk1LWIyM2YtMmUyNTE5YTFmZTAyIiwiZXhwIjoxNTYyNDE4NDk3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2MTNjMDVlNS1hNzUzLTRmM2UtOWViOC1hZGE4MTJmY2IyYWQiLCJjbGllbnRfaWQiOiJ0ZXN0MSJ9.efw9OePFUN9X6UGMF3h9BF_KO3zqyIfpvfmE8XklBDs",
  "expires_in": 3599,
  "scope": "all",
  "jti": "8657a00e-1b3c-4095-b23f-2e2519a1fe02"
}

 

假設現在access_token過期了,我們用refresh_token去換取新的令牌。使用postman發送如下請求:

 

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjE1NTgyMzEsInVzZXJfbmFtZSI6Im1yYmlyZCIsImF1dGhvcml0aWVzIjpbImFkbWluIl0sImp0aSI6ImFmNjU5MTE3LWJkMTItNDNmZS04YzE2LTM0MDQxMTMyZDFlOCIsImNsaWVudF9pZCI6InRlc3QxIiwic2NvcGUiOlsiYWxsIl19.4ZD5bXxsXjSw62_1wVl2QpHUKYcC8_1phdNRP02Iihs",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJtcmJpcmQiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiYWY2NTkxMTctYmQxMi00M2ZlLThjMTYtMzQwNDExMzJkMWU4IiwiZXhwIjoxNTYyNDE4NDk3LCJhdXRob3JpdGllcyI6WyJhZG1pbiJdLCJqdGkiOiI2MTNjMDVlNS1hNzUzLTRmM2UtOWViOC1hZGE4MTJmY2IyYWQiLCJjbGllbnRfaWQiOiJ0ZXN0MSJ9.e4p3CRyk_cZ82cGzjCBOb4p_0bqRqXElczJjf0nB58o",
"expires_in": 3599,
"scope": "all",
"jti": "af659117-bd12-43fe-8c16-34041132d1e8"
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM