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

相关文章
相关标签/搜索