30.SpringSecurity-使用JWT替換默認令牌

前言

上一節咱們已經講解了token的基本配置,包括過時時間,密匙等。這一節主要講解使用JWT替換默認的Token配置。前端

內容

1.JWT(Json Web Token)特色

JWT的全稱是Json Web Token;Json開放的Token標準。 JWT的特色:web

  1. 自包含:咱們以前讀取Spring-Security-oauth代碼時候,在DefaultTokenServices裏面建立令牌的時候:使用的是默認用UUID生成的Token

image.png
最後返回給接口的串是無心義的字符串,自己並不包含任何有意義信息,信息是單獨存起來的,就像:
image.pngredis

當你用令牌去訪問的時候,咱們還須要根據這個令牌從存儲redis裏面讀取出來信息,而後知道用戶信息。這種令牌機制存在的特色會依賴一個存儲。一旦存儲出現問題,redis或者數據庫掛掉了的話,你這個存儲就毫無用處了。由於這個令牌自己不包含任何信息的。而JWT他的特色是自包含,這個令牌自己裏面是有信息的。你拿到這個令牌以後,直接解析令牌就能夠知道用戶信息。而不須要從存儲裏面去讀取任何信息。spring

  1. 密籤:有人認爲我這邊jwt Token裏面發送出去的信息被別人破解了,是否是就沒有什麼安全性了,首先,你放到token裏面的東西應該不是業務關鍵的東西。你不能把用戶密碼放到token裏面去,其次:你發出去的令牌能夠用你指定的祕鑰進行簽名的,這裏要強調他是簽名,而不是加密。所謂的簽名是防止別人去串改。你發出去的信息別人改動裏面的信息時候你是能夠知道的。jwt是一個標準,你發出去的信息是按照這個標準生成的。全部人都是能夠看到裏面的信息的。這也說明了咱們不能把業務敏感信息放到token裏面的緣由。
  2. 可擴展:由於信息自包含,因此裏面的信息你是能夠自定義,可擴展的。

2.JWT(Json Web Token)替換到默認的token

咱們在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;
    }
}

3.TokenStore選取

從上面能夠知道咱們的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;
        }
    }
}
  1. 前綴(prefix):表明的是以"."隔開的配置文件中,最後一個點前面的全部字符串。
  2. name:表明的是以"."隔開的配置文件中,最後一個點後面的字符串。
  3. matchIfMissing=true:說明假如咱們沒有配置:yxm.security.oauth2.storeType 那麼默認會啓用此註解修飾的配置。
  4. 總體含義:當屬性配置文件中:yxm.security.oauth2.storeType的值是:jwt時候;jwt配置就會生效。

4.MyAuthorizationServerConfig中endpoints配置

由於咱們在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");
            }
        }
    }
}

5.測試

5.1 獲取JwtAccessToken

咱們使用password測試:
image.png服務器

{
"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

image.png

5.2 使用獲取到的JwtAccessToken去請求接口

咱們用戶的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'

請求示例:
image.png

咱們發現返回的是NO CONTENT

分析:

@GetMapping("/me")
public Object me(@AuthenticationPrincipal UserDetails user){
    return user;
}

如今的Authentication對象裏面的Principal並非一個UserDetails,而是一個字符串 因此獲取不到值

咱們修改:

@GetMapping("/me")
public Object me(Authentication user){
    return user;
}

咱們再次請求:獲取的結果爲:

image.png

咱們發現返回的Authentication並非按照咱們以前在jwt.io裏面解碼成的payload:

image.png

6.JwtToken-可擴展

針對於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();
        }
    }
}

7.將加強器配置到認證服務器中

  1. 因爲咱們以前經過DefaultTokenService建立的AccessToken默認是經過UUID來建立的,而且主框架沒有給咱們提供共有的(public)建立accessToken方法。
  2. 建立的默認方法: private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken),因此咱們只能在endpoints裏面配置加強器。
  3. 咱們只須要配置endpoints
@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:
image.png

而後咱們拿着access_token去jwt.io網站去查找:

image.png

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiLlvKDkuIkiLCJzY29wZSI6WyJhbGwiXSwiY29tcGFueSI6ImFsaWJhYmEiLCJleHAiOjE1ODM2NzU1NTIsImF1dGhvcml0aWVzIjpbImFkbWluIiwiUk9MRV9VU0VSIl0sImp0aSI6ImRhZTg0MWZhLTlkNTktNGRhMC05NTZhLTgyODI3ZTJjYjRmNSIsImNsaWVudF9pZCI6Inl4bSJ9.gSNL-MJU1whYNqmL4IVGSJsoSpGo7VkrND8O9_c0VBQ

而後咱們攜帶咱們的access_token去獲取Authentication信息。
image.png

咱們能夠發現:返回的Authentication數據字段裏面沒有company信息,緣由是由於:咱們的加強是在Authentication生成以後進行的加強、Spring只會管理JwtToken標準的形式,不會改變你自定義的數據格式。若是須要自定義咱們的數據格式,咱們須要:在Authentication裏面單獨添加信息。

8.解析JwtToken,將數據封裝到Authentication返回

作這個事情的時候,咱們須要添加一個依賴;就是以前https://jwt.io/對應的網站包依賴

  1. 添加依賴(spring-security-demo)
<!--添加JWT依賴-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>
  1. 解密驗籤:咱們在解密驗籤時候使用下面utf-8形式:
//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;
}

咱們重啓服務嘗試一下:
先獲取令牌:

image.png

根據令牌咱們獲取用戶信息:
image.png

9.令牌刷新

從expires_in": 7199咱們知道令牌的失效時間是:接近2小時,一旦在2小時以後訪問時候,用戶訪問將會失效。咱們不能讓用戶老是登陸,從新獲取令牌。

咱們獲取令牌時候,會返回一個refresh_token

  1. 咱們先獲取token
  2. 根據咱們獲取的token爲參數獲取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
image.png

相關文章
相關標籤/搜索