上一節咱們已經講解了token的基本配置,包括過時時間,密匙等。這一節主要講解使用JWT替換默認的Token配置。前端
JWT的全稱是Json Web Token;Json開放的Token標準。 JWT的特色:web
最後返回給接口的串是無心義的字符串,自己並不包含任何有意義信息,信息是單獨存起來的,就像:
redis
當你用令牌去訪問的時候,咱們還須要根據這個令牌從存儲redis裏面讀取出來信息,而後知道用戶信息。這種令牌機制存在的特色會依賴一個存儲。一旦存儲出現問題,redis或者數據庫掛掉了的話,你這個存儲就毫無用處了。由於這個令牌自己不包含任何信息的。而JWT他的特色是自包含,這個令牌自己裏面是有信息的。你拿到這個令牌以後,直接解析令牌就能夠知道用戶信息。而不須要從存儲裏面去讀取任何信息。spring
咱們在app工程的TokenStoreConfig裏面配置咱們的JWT。 咱們不是加一個靜態方法而是加一個靜態類:JwtTokenConfig。咱們須要在裏面配置一系列bean。 Token存儲bean。Jwt令牌Token轉換bean數據庫
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只負責token存儲,無論token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 負責token生成過程當中的處理的 * JwtAccessTokenConverter功能能夠進行密籤,就是進行簽名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.設置簽名祕鑰 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } } }
咱們將祕鑰配置放到:properties文件裏面。json
public class OAuth2Properties { /** * jwt密籤,加密時候使用這個密籤,解密時候也是使用這個密籤 * 本身必定要保存好,別人一旦知道你的祕鑰,就能夠簽發你的jwt令牌,就能夠隨意進入你係統 */ private String jwtSigningKey = "yxm"; private OAuth2ClientProperties[] clients = {};//默認空數組 public OAuth2ClientProperties[] getClients() { return clients; } public void setClients(OAuth2ClientProperties[] clients) { this.clients = clients; } public String getJwtSigningKey() { return jwtSigningKey; } public void setJwtSigningKey(String jwtSigningKey) { this.jwtSigningKey = jwtSigningKey; } }
從上面能夠知道咱們的tokenStore配置了兩個,那麼咱們如何告訴系統,咱們想用JwtTokenStore呢?因此咱們就須要外加一些配置。數組
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "redis") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration /** * 前綴(prefix):表明的是以"."隔開的配置文件中,最後一個點前面的全部字符串。 * name:表明的是以"."隔開的配置文件中,最後一個點後面的字符串。 */ @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "jwt",matchIfMissing = true) public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只負責token存儲,無論token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 負責token生成過程當中的處理的 * JwtAccessTokenConverter功能能夠進行密籤,就是進行簽名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.設置簽名祕鑰 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } } }
由於咱們在TokenStoreConfig多配置了一個bean,因此咱們須要將其加入到認證服務器配置上去。安全
@Configuration @EnableAuthorizationServer public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; @Autowired private TokenStore redisTokenStore; @Autowired(required = false) //由於這個類在JwtTokenConfig有效狀況下才有效 private JwtAccessTokenConverter jwtAccessTokenConverter; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 系統端點配置:endpoints * 1.使用咱們本身的受權管理器(AuthenticationManager)和自定義的用戶詳情服務(UserDetailsService) */ endpoints.tokenStore(redisTokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); if(jwtAccessTokenConverter != null){ endpoints.accessTokenConverter(jwtAccessTokenConverter); } } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { /** * 系統第三方客戶端配置: * 所謂的客戶端就是有哪些應用會訪問咱們的系統, * 咱們的認證服務器會決定給哪些第三方應用client去發送令牌。 * 若是這個配置後咱們以前配置文件中配置的clientId和clientSecret將不會起做用了 */ //目前咱們的應用場景是在咱們的app和咱們的前端;咱們不容許第三方來註冊,因此用內存 /* clients.inMemory().withClient("yxm") .secret("yxmsecret") .accessTokenValiditySeconds(7200)//發出去的令牌,有效期是多少? 這裏設置爲2小時 .authorizedGrantTypes("refresh_token","password")//針對當前應用客戶端:yxm,所能支持的受權模式是哪些?以前設置有4種類加上刷新總共5種:這裏只支持配置的:"refresh_token","password"。 .scopes("all","read","write")//發出去的權限有哪些?以前前端請求攜帶了scoope,此配置的scope用來指定前端發送scope的值必須在配置的裏面或者不攜帶scope;默認爲此處配置的scope .and() .withClient("startshineye") .secret("startshineyesecret") .accessTokenValiditySeconds(3600) .authorizedGrantTypes("password") .scopes("read");*/ InMemoryClientDetailsServiceBuilder builder = clients.inMemory(); if(ArrayUtils.isNotEmpty(securityProperties.getOauth2().getClients())) {//判斷咱們的配置是否爲空 for (OAuth2ClientProperties client:securityProperties.getOauth2().getClients()){ builder.withClient(client.getClientId()) .secret(client.getClientSecret()) .accessTokenValiditySeconds(client.getAccessTokenValiditySeconds()) .authorizedGrantTypes("refresh_token", "authorization_code", "password") .scopes("all"); } } } }
咱們使用password測試:
服務器
{ "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJhdXRob3JpdGllcyI6WyJhZG1pbiIsIlJPTEVfVVNFUiJdLCJqdGkiOiI3MzczNjY1NC05Y2M1LTRlOTctOWRhYS04MDY5MzU1MWRjNWIiLCJjbGllbnRfaWQiOiJzdGFydHNoaW5leWUiLCJzY29wZSI6WyJhbGwiXX0.WlBgeBkU5giUQWeDZqAAMF8i-8DrvqvdT7TglwsBzsE", "token_type": "bearer", "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiNzM3MzY2NTQtOWNjNS00ZTk3LTlkYWEtODA2OTM1NTFkYzViIiwiZXhwIjoxNTg2MjMwNTQyLCJhdXRob3JpdGllcyI6WyJhZG1pbiIsIlJPTEVfVVNFUiJdLCJqdGkiOiJjNGM4MDlmYi0zNjFkLTRkNzYtOGU5Zi1hOTY5ZGI2MmVlOGIiLCJjbGllbnRfaWQiOiJzdGFydHNoaW5leWUifQ.VrrLpj24xLefgYd6j1ALfCRAghBoVHEfkKBMQb2veO8", "scope": "all", "jti": "73736654-9cc5-4e97-9daa-80693551dc5b" }
以前咱們說過,jwtToken是包含信息的:
咱們進入網站:https://jwt.io/輸入咱們的jwtAccessToken app
咱們用戶的me接口以下:
@GetMapping("/me") public Object me(@AuthenticationPrincipal UserDetails user){ return user; }
curl -i -X GET \ -H "Authorization:bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM2NDcwNTEsInVzZXJfbmFtZSI6IuW8oOS4iSIsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6IjI5OGJhZDdkLThjZmUtNDIyZC1hZTg2LTgzN2U3Nzk2MjNmNiIsImNsaWVudF9pZCI6Inl4bSIsInNjb3BlIjpbImFsbCJdfQ.wPKwjJzT-EJ6BAZOSaAQwBBFBZKwrkmT2ymbkp_mdLA" \ 'http://127.0.0.1:8088/user/me'
請求示例:
咱們發現返回的是NO CONTENT
分析:
@GetMapping("/me") public Object me(@AuthenticationPrincipal UserDetails user){ return user; }
如今的Authentication對象裏面的Principal並非一個UserDetails,而是一個字符串 因此獲取不到值
咱們修改:
@GetMapping("/me") public Object me(Authentication user){ return user; }
咱們再次請求:獲取的結果爲:
咱們發現返回的Authentication並非按照咱們以前在jwt.io裏面解碼成的payload:
針對於JwtToken-可擴展,咱們在TokenStoreConfig下的JwtTokenConfig配置TokenEnhancer
他是一個接口,咱們須要本身實現。
public class MyJwtTokenEnhancer implements TokenEnhancer { @Override public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String,Object> info = new HashMap<>(); info.put("company","alibaba"); /** * 往accessToken裏面添加額外信息 */ ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info); return accessToken; } }
寫完上面類以後,咱們把它配置到Spring容器裏面。註解: @ConditionalOnMissingBean(name="jwtTokenEnhancer")//業務系統本身能夠添加一個:jwtTokenEnhancer來覆蓋此定義的加強器。
@Configuration public class TokenStoreConfig { @Autowired private RedisConnectionFactory redisConnectionFactory; @Bean @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "redis") public TokenStore redisTokenStore(){ return new RedisTokenStore(redisConnectionFactory); } @Configuration /** * 前綴(prefix):表明的是以"."隔開的配置文件中,最後一個點前面的全部字符串。 * name:表明的是以"."隔開的配置文件中,最後一個點後面的字符串。 */ @ConditionalOnProperty(prefix = "yxm.security.oauth2",name = "storeType",havingValue = "jwt",matchIfMissing = true) public static class JwtTokenConfig{ @Autowired private SecurityProperties securityProperties; /** * 只負責token存儲,無論token生成 * @return */ @Bean public TokenStore jwtTokenStore(){ return new JwtTokenStore(jwtAccessTokenConverter()); } /** * 負責token生成過程當中的處理的 * JwtAccessTokenConverter功能能夠進行密籤,就是進行簽名。 * @return */ @Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.設置簽名祕鑰 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; } @Bean @ConditionalOnMissingBean(name="jwtTokenEnhancer")//業務系統本身能夠添加一個:jwtTokenEnhancer來覆蓋 public TokenEnhancer jwtTokenEnhancer(){ return new MyJwtTokenEnhancer(); } } }
@Configuration @EnableAuthorizationServer public class MyAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserDetailsService userDetailsService; @Autowired private SecurityProperties securityProperties; @Autowired private TokenStore redisTokenStore; @Autowired(required = false) //由於這個類在JwtTokenConfig有效狀況下才有效 private JwtAccessTokenConverter jwtAccessTokenConverter; @Autowired(required = false) private TokenEnhancer jwtTokenEnhancer; @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { /** * 系統端點配置:endpoints * 1.使用咱們本身的受權管理器(AuthenticationManager)和自定義的用戶詳情服務(UserDetailsService) */ endpoints.tokenStore(redisTokenStore) .authenticationManager(authenticationManager) .userDetailsService(userDetailsService); if(jwtAccessTokenConverter != null && jwtTokenEnhancer != null){ /** * 1.因爲咱們以前經過DefaultTokenService建立的AccessToken默認是經過UUID來建立的,而且主框架沒有給咱們提供共有的建立accessToken方法 * 2.建立的默認方法: private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken), * 因此咱們智能在endpoints裏面配置加強器。 * 3.爲了知足1,2;咱們須要建立加強器鏈,來將jwtAccessTokenConverter和jwtTokenEnhancer鏈接起來。 */ TokenEnhancerChain enhancerChain = new TokenEnhancerChain(); List<TokenEnhancer> enhancers = new ArrayList<>(); enhancers.add(jwtTokenEnhancer); enhancers.add(jwtAccessTokenConverter); endpoints .tokenEnhancer(enhancerChain) .accessTokenConverter(jwtAccessTokenConverter); } }
咱們再次啓動下服務:
而後訪問獲取accessToken:
而後咱們拿着access_token去jwt.io網站去查找:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiY29tcGFueSI6ImFsaWJhYmEiLCJleHAiOjE1ODM2NzU1NTIsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6ImRhZTg0MWZhLTlkNTktNGRhMC05NTZhLTgyODI3ZTJjYjRmNSIsImNsaWVudF9pZCI6Inl4bSJ9.gSNL-MJU1whYNqmL4IVGSJsoSpGo7VkrND8O9_c0VBQ
而後咱們攜帶咱們的access_token去獲取Authentication信息。
咱們能夠發現:返回的Authentication數據字段裏面沒有company信息,緣由是由於:咱們的加強是在Authentication生成以後進行的加強、Spring只會管理JwtToken標準的形式,不會改變你自定義的數據格式。若是須要自定義咱們的數據格式,咱們須要:在Authentication裏面單獨添加信息。
作這個事情的時候,咱們須要添加一個依賴;就是以前https://jwt.io/對應的網站包依賴
<!--添加JWT依賴--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.7.0</version> </dependency>
//2.解密獲取用戶信息 Claims claims = Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes("UTF-8")).parseClaimsJws(token).getBody();
緣由是由於Jwts設置SigningKey默認不是utf-8的格式
而咱們在JwtAccessTokenConverter的時候是使用utf-8格式的。
@Bean public JwtAccessTokenConverter jwtAccessTokenConverter(){ JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter(); //1.設置簽名祕鑰-----默認是按照utf-8格式的 accessTokenConverter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey()); return accessTokenConverter; }
咱們重啓服務嘗試一下:
先獲取令牌:
根據令牌咱們獲取用戶信息:
從expires_in": 7199咱們知道令牌的失效時間是:接近2小時,一旦在2小時以後訪問時候,用戶訪問將會失效。咱們不能讓用戶老是登陸,從新獲取令牌。
咱們獲取令牌時候,會返回一個refresh_token
curl -i -X POST \ -H "Content-Type:application/x-www-form-urlencoded" \ -H "Authorization:Basic eXhtOnl4bXNlY3JldA==" \ -d "grant_type=refresh_token" \ -d "refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiZDU5Yzk1ZWYtZTVlNC00ZDhhLWI1MDUtNWM2ZjMwYmRiOGEwIiwiY29tcGFueSI6ImFsaWJhYmEiLCJleHAiOjE1ODYyNjg5MDcsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6IjE5YjlmODZjLWJkZDItNGRjNi04NzkzLWVlYTRhYWZlMzFhOSIsImNsaWVudF9pZCI6Inl4bSJ9.PZyZ-2S3JfJwJdAb2dL4orIuFc0Mjc9v-9QqZx7jPN8" \ -d "scop=all" \ 'http://127.0.0.1:8088/oauth/token'
此時咱們就會再次獲取到對應有效時間爲2小時的token