JWT令牌生成与校验

Posted 赵广陆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JWT令牌生成与校验相关的知识,希望对你有一定的参考价值。

目录


1 JWT介绍

通过上边的测试我们发现,当资源服务和授权服务不在一起时资源服务使用RemoteTokenServices 远程请求授权服务验证token,如果访问量较大将会影响系统的性能 。

解决上边问题:

令牌采用JWT格式即可解决上边的问题,用户认证通过会得到一个JWT令牌,JWT令牌中已经包括了用户相关的信息,客户端只需要携带JWT访问资源服务,资源服务根据事先约定的算法自行完成令牌校验,无需每次都请求认证服务完成授权。

1.1 什么是JWT?

JSON Web Token(JWT)是一个开放的行业标准(RFC 7519),它定义了一种简介的、自包含的协议格式,用于在通信双方传递json对象,传递的信息经过数字签名可以被验证和信任。JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改。

官网:JWT.IO

标准: JSON Web Token (JWT)

JWT令牌的优点:

1)jwt基于json,非常方便解析。

2)可以在令牌中自定义丰富的内容,易扩展。

3)通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。

4)资源服务使用JWT可不依赖认证服务即可完成授权。

缺点:

1)JWT令牌较长,占存储空间比较大。

1.2 JWT令牌结构

通过学习JWT令牌结构为自定义jwt令牌打好基础。

JWT令牌由三部分组成,每部分中间使用点(.)分隔,比如:xxxxx.yyyyy.zzzzz

  • Header

头部包括令牌的类型(即JWT)及使用的哈希算法(如HMAC SHA256或RSA)

一个例子如下:

下边是Header部分的内容

 
  "alg": "HS256",
  "typ": "JWT"

将上边的内容使用Base64Url编码,得到一个字符串就是JWT令牌的第一部分。

  • Payload

第二部分是负载,内容也是一个json对象,它是存放有效信息的地方,它可以存放jwt提供的现成字段,比如:iss(签发者),exp(过期时间戳), sub(面向的用户)等,也可自定义字段。

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容。

最后将第二部分负载使用Base64Url编码,得到一个字符串就是JWT令牌的第二部分。

一个例子:

 
  "sub": "1234567890",
  "name": "456",
  "admin": true

  • Signature

第三部分是签名,此部分用于防止jwt内容被篡改。

这个部分使用base64url将前两部分进行编码,编码后使用点(.)连接组成字符串,最后使用header中声明签名算法进行签名。

一个例子:

HMACSHA256( 
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

base64UrlEncode(header):jwt令牌的第一部分。

base64UrlEncode(payload):jwt令牌的第二部分。

secret:签名所使用的密钥。

2 配置JWT令牌服务

在uaa中配置jwt令牌服务,即可实现生成jwt格式的令牌。

1、TokenConfig

@Configuration 
public class TokenConfig 
    private String SIGNING_KEY = "uaa123";
    @Bean
    public TokenStore tokenStore() 
        return new JwtTokenStore(accessTokenConverter());
    
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() 
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    

2、定义JWT令牌服务

@Autowired
private JwtAccessTokenConverter accessTokenConverter;
 @Bean
 public AuthorizationServerTokenServices tokenService() 
      DefaultTokenServices service=new DefaultTokenServices();
      service.setClientDetailsService(clientDetailsService);
      service.setSupportRefreshToken(true);
      service.setTokenStore(tokenStore);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
service.setTokenEnhancer(tokenEnhancerChain);
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时
      service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
      return service;
  

3 生成JWT令牌


响应:

4 校验JWT令牌

资源服务需要和授权服务拥有一致的签字、令牌服务等:

1、将授权服务中的TokenConfig类拷贝到资源 服务中

2、屏蔽资源 服务原来的令牌服务类

@Configuration 
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ResouceServerConfig extends
        ResourceServerConfigurerAdapter 
    public static final String RESOURCE_ID = "res1";
    @Autowired
    TokenStore tokenStore;
    //资源服务令牌解析服务
//    @Bean
//    public ResourceServerTokenServices tokenService() 
//        //使用远程服务请求授权服务器校验token,必须指定校验token 的url、client_id,client_secret
//        RemoteTokenServices service=new RemoteTokenServices(); 
//        service.setCheckTokenEndpointUrl("http://localhost:53020/uaa/oauth/check_token");
//        service.setClientId("c1");
//        service.setClientSecret("secret");
//        return service;
//    
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) 
        resources.resourceId(RESOURCE_ID)
                .tokenStore(tokenStore)
                .stateless(true);
    

3、测试

1)申请jwt令牌

2)使用令牌请求资源

小技巧:

令牌申请成功可以使用/uaa/oauth/check_token校验令牌的有效性,并查询令牌的内容,例子如下:

5 JWT整合Spring Security

截止目前客户端信息和授权码仍然存储在内存中,生产环境中通过会存储在数据库中,下边完善环境的配置:

5.1 创建表

在user_db中创建如下表:

DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `client_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端标
识',
  `resource_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '接入资源列表',
  `client_secret` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '客户端秘钥',
  `scope` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authorized_grant_types` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT
NULL,
  `web_server_redirect_uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT
NULL,
  `authorities` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `access_token_validity` int(11) NULL DEFAULT NULL,
  `refresh_token_validity` int(11) NULL DEFAULT NULL,
  `additional_information` longtext CHARACTER SET utf8 COLLATE utf8_general_ci NULL,
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE
CURRENT_TIMESTAMP(0),
  `archived` tinyint(4) NULL DEFAULT NULL,
  `trusted` tinyint(4) NULL DEFAULT NULL,
  `autoapprove` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`client_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '接入客户端信息'
ROW_FORMAT = Dynamic;
INSERT INTO `oauth_client_details` VALUES ('c1', 'res1',
'$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_ADMIN,ROLE_USER,ROLE_API',
'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com',
NULL, 7200, 259200, NULL, '2019‐09‐09 16:04:28', 0, 0, 'false');
INSERT INTO `oauth_client_details` VALUES ('c2', 'res2',
'$2a$10$NlBC84MVb7F95EXYTXwLneXgCca6/GipyWR5NHm8K0203bSQMLpvm', 'ROLE_API',
'client_credentials,password,authorization_code,implicit,refresh_token', 'http://www.baidu.com',
NULL, 31536000, 2592000, NULL, '2019‐09‐09 21:48:51', 0, 0, 'false');

oauth_code表,Spring Security OAuth2使用,用来存储授权码:

DROP TABLE IF EXISTS `oauth_code`;
CREATE TABLE `oauth_code`  (
  `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `code` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `authentication` blob NULL,
  INDEX `code_index`(`code`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

6 配置授权服务

(1)修改AuthorizationServer:

ClientDetailsService和AuthorizationCodeServices从数据库读取数据。

@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends
AuthorizationServerConfigurerAdapter         
@Autowired    
private TokenStore tokenStore;    
@Autowired    
private JwtAccessTokenConverter accessTokenConverter;    
@Autowired    
private ClientDetailsService clientDetailsService;    
@Autowired    
private AuthorizationCodeServices authorizationCodeServices;    
@Autowired    
private AuthenticationManager authenticationManager;    
/**    
 * 1.客户端详情相关配置    
 */    
@Bean    
    public PasswordEncoder passwordEncoder() 
        return new BCryptPasswordEncoder();
    
 
@Bean    
public ClientDetailsService clientDetailsService(DataSource dataSource)     
ClientDetailsService clientDetailsService = new JdbcClientDetailsService(dataSource);        
((JdbcClientDetailsService)
clientDetailsService).setPasswordEncoder(passwordEncoder());
       
return clientDetailsService;        
    
@Override 
public void configure(ClientDetailsServiceConfigurer clients)    
throws Exception             
clients.withClientDetails(clientDetailsService);        
    
/**    
 * 2.配置令牌服务(token services)    
 */    
@Bean    
public AuthorizationServerTokenServices tokenService()     
DefaultTokenServices service=new DefaultTokenServices();        
service.setClientDetailsService(clientDetailsService);        
service.setSupportRefreshToken(true);//支持刷新令牌        
service.setTokenStore(tokenStore); //绑定tokenStore        
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();        
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));        
service.setTokenEnhancer(tokenEnhancerChain);        
service.setAccessTokenValiditySeconds(7200); // 令牌默认有效期2小时        
service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3return service;        
    
/**    
 * 3.配置令牌(token)的访问端点    
 */    
@Bean    
public AuthorizationCodeServices authorizationCodeServices(DataSource dataSource)      
return new JdbcAuthorizationCodeServices(dataSource);//设置授权码模式的授权码如何存取        
    
@Override    
public void configure(AuthorizationServerEndpointsConfigurer endpoints)     
endpoints.authenticationManager(authenticationManager)        
.authorizationCodeServices(authorizationCodeServices)                
.tokenServices(tokenService())                
.allowedTokenEndpointRequestMethods(HttpMethod.POST);                
    
/**    
 * 4.配置令牌端点(Token Endpoint)的安全约束    
 */    
@Override    
public void configure(AuthorizationServerSecurityConfigurer security)    
security        
.tokenKeyAccess("permitAll()")                
.checkTokenAccess("permitAll()")                
.allowFormAuthenticationForClients()//允许表单认证                
;
    

6.1 测试

1、测试申请令牌

使用密码模式申请令牌,客户端信息需要和数据库中的信息一致。

POST http://localhost:53020/uaa/oauth/token

2、测试授权码模式

生成的授权存储到数据库中。

http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com

注意scope=?,查看数据库

POST http://localhost:53020/uaa/oauth/token

以上是关于JWT令牌生成与校验的主要内容,如果未能解决你的问题,请参考以下文章

Lua识别Jwt令牌业务

Lua识别Jwt令牌业务

刷新 JWT 过期而不刷新或生成新 Token

PHP JWT 令牌无效签名

OAuth2.0-JWT令牌

Laravel/Vue - JWT 令牌记住我