抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

JWT授权

什么是JWT

Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

起源

说起JWT,我们应该来谈一谈基于token的认证和传统的session认证的区别。

传统的session认证

​ 我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

​ 但是这种基于session的认证使应用本身很难得到扩展,随着不同客户端用户的增加,独立的服务器已无法承载更多的用户,而这时候基于session认证应用的问题就会暴露出来.

基于session认证所显露的问题

Session: 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大。

扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。

CSRF: 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于token的鉴权机制

基于token的鉴权机制类似于http协议也是无状态的,它不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录了,这就为应用的扩展提供了便利。

流程上是这样的:

  • 用户使用用户名密码来请求服务器
  • 服务器进行验证用户的信息
  • 服务器通过验证发送给用户一个token
  • 客户端存储token,并在每次请求时附送上这个token值
  • 服务端验证token值,并返回数据

JWT官网有一张图描述了JWT的认证过程:

这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *

使用场景

JWT 的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。但是,如果系统中需要使用黑名单实现长期有效的 Token 刷新机制,这种无状态的优势就不明显了。

一次性验证

​ 比如用户注册后需要发一封邮件让其激活账户,通常邮件中需要有一个链接,这个链接需要具备以下的特性:能够标识用户,该链接具有时效性(通常只允许几小时之内激活),不能被篡改以激活其他可能的账户…这种场景就和 jwt 的特性非常贴近,jwt 的 payload 中固定的参数:iss 签发者和 exp 过期时间正是为其做准备的。

无状态认证

​ 使用 jwt 来做 restful api 的身份认证也是值得推崇的一种使用方案。客户端和服务端共享 secret;过期时间由服务端校验,客户端定时刷新;签名信息不可被修改。spring security oauth jwt 提供了一套完整的 jwt 认证体系。

优势

  • 快速开发
  • 不需要 Cookie
  • JSON 在移动端的广泛应用
  • 不依赖于社交登录
  • 相对简单的概念理解
  • 因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。
  • 因为有了payload部分,所以JWT可以在自身存储一些其他业务逻辑所必要的非敏感信息。
  • 便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的。
  • 它不需要在服务端保存会话信息, 所以它易于应用的扩展

限制

  • Token有长度限制
  • Token不能撤销
  • 需要 Token 有失效时间限制(exp)

安全性

  • 不应该在jwt的payload部分存放敏感信息,因为该部分是客户端可解密的部分。
  • 保护好secret私钥,该私钥非常重要。
  • 如果可以,请使用https协议

JWT 的几个特点

  1. JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
  2. JWT 不加密的情况下,不能将秘密数据写入 JWT。
  3. JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  4. JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
  5. JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  6. 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

JWT的构成

JSON Web Token(JWT)是一个非常轻巧的规范。这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息。

一个JWT实际上就是一个字符串,它由三部分组成,头部、载荷与签名。

第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature).

典型的,一个JWT看起来如下图。

改对象为一个很长的字符串,字符之间通过”.”分隔符分为三个子串。注意JWT对象为一个长字串,各字串之间也没有换行符,此处为了演示需要,我们特意分行并用不同颜色表示了。每一个子串表示了一个功能块,总共有以下三个部分:

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。

jwt的头部承载两部分信息:

  • 声明类型,这里是jwt
  • 声明加密的算法 通常直接使用 HMAC SHA256
1
2
3
4
{
'typ': 'JWT',
'alg': 'HS256'
}

然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分.

1
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

载荷(playload)

载荷就是存放有效信息的地方。这个名字像是特指飞机上承载的货品,这些有效信息包含三个部分

标准中注册的声明(建议但不强制使用)
  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明

​ 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

私有的声明

​ 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

​ 这个指的就是自定义的claim。比如前面那个结构举例中的admin和name都属于自定的claim。这些claim跟JWT标准规定的claim区别在于:JWT规定的claim,JWT的接收方在拿到JWT之后,都知道怎么对这些标准的claim进行验证(还不知道是否能够验证);而private claims不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行。

定义一个payload:

1
{"sub":"1234567890","name":"John Doe","admin":true}

然后将其进行base64加密,得到Jwt的第二部分。

1
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
注意

​ 请注意,默认情况下JWT是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。

​ JSON对象也使用Base64 URL算法转换为字符串保存。

签证(signature)

jwt的第三部分是一个签证信息,这个签证信息由三部分组成:

  • header (base64后的)
  • payload (base64后的)
  • secret

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

1
2
3
4
// javascript
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);

var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

将这三部分用.连接成一个完整的字符串,构成了最终的jwt:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

Base64URL算法

如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别。

​ 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符是”+”,”/“和”=”,由于在URL中有特殊含义,因此Base64URL中对他们做了替换:”=”去掉,”+”用”-“替换,”/“用”_”替换,这就是Base64URL算法,很简单把。

项目改造

基于原来的aouth2 改造

生成公私钥

生成私钥

生成私钥并将私钥放在服务器上

1
keytool -genkeypair -alias order-jwt -validity 3650 -keyalg RSA -dname "CN=jwt,OU=jtw,O=jtw,L=zurich,S=zurich,C=CH" -keypass 123456 -keystore order-jwt.jks -storepass 123456
生成公钥

将公钥内容放在一个新建文件中(比如我的叫 public.cert),公钥放在资源服务器中

1
2
keytool -list -rfc --keystore order-jwt.jks | openssl x509 -inform pem -pubkey

1
2
3
4
5
6
7
8
9
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Zrm/dn7zUKFHU7z3Fui
eFa2smFMX9aGF8/J9jHWQxxLqAGdSzr9EHq4ZxRF1gdjWizUEBN8/yHABEoQ0JYJ
nKVhNzcKxtT2Uy8iy3iSC7qgixSJ3wRlVvtfYO9j0hk1T5x9Ni0XkbfaoE+rfKVr
h2qlijF3YD3FutOMYCcFCbUR92eo6I5WHG7H8O1/qN/WuJ4iRuP/ORwntLxIGwq8
DSt22G1Khq22OZiltyhBzLNJi3rMSquCGTyYHNyBmB5VcaCRHVskuMtTYBBdaQme
EhhPVRwj0+az9FF7RCp/NkQcmGYLKSrzNCPBhNksEMKn/JyQhh4kkA3tOwZRop4X
MQIDAQAB
-----END PUBLIC KEY-----

服务端

导入POM文件

导入spring-security-jwt jar对jwt进行支持

1
2
3
4
5
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>

增加插件防止密钥没有打包

1
2
3
4
5
6
7
8
9
10
11
<!--防止jks文件被mavne编译导致不可用-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
防止私钥

将私钥放在授权服务端

配置类修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/**
* 授权服务器配置
**/
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {


@Autowired
private AuthenticationManager authenticationManager;

@Autowired
UserDetailsService userDetailsService;

// 使用最基本的InMemoryTokenStore生成token
@Bean
public TokenStore memoryTokenStore() {
return new InMemoryTokenStore();
}


/**
* 配置客户端详情服务
* 客户端详细信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
* 1.授权码模式(authorization code)
* 2.简化模式(implicit)
* 3.密码模式(resource owner password credentials)
* 4.客户端模式(client credentials)
* ClientDetailsServiceConfigurer
*
* @param clients
* @throws Exception
*/
@Override

public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("order-server")//用于标识用户ID
//支持 授权码、密码两种授权模式,支持刷新token功能
.authorizedGrantTypes("client_credentials", "refresh_token")//授权方式
.resourceIds("order-server")
.authorities("client_credentials")
//跳转客户端地址
.scopes("test")//授权范围
//客户端安全码,secret密码配置从 Spring Security 5.0开始必须以 {bcrypt}+加密后的密码 这种格式填写;
.secret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"))
.accessTokenValiditySeconds(1200)
.refreshTokenValiditySeconds(50000);


}

/**
* 用来配置令牌端点(Token Endpoint)的安全约束.
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
/* 配置token获取合验证时的策略 允许表单认证 */

security.allowFormAuthenticationForClients().
//客户端token调用许可
tokenKeyAccess("permitAll()").
//客户端校验token访问许可
checkTokenAccess("isAuthenticated()");
}

/**
* 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 配置tokenStore,需要配置userDetailsService,否则refresh_token会报错
endpoints.authenticationManager(authenticationManager).
//配置用户服务
userDetailsService(userDetailsService).
allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST).
//指定token存储位置
tokenStore(tokenStore()).
// 配置用于JWT私钥加密的增强器
tokenEnhancer(jwtTokenEnhancer());
// 配置tokenServices参数
DefaultTokenServices tokenServices = new DefaultTokenServices();
tokenServices.setTokenStore(endpoints.getTokenStore());
// 是否支持刷新
tokenServices.setSupportRefreshToken(true);
tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// 20分钟
tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(20));
endpoints.tokenServices(tokenServices);
}

@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
// 配置jks文件
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("order-jwt.jks"), "123456".toCharArray());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setKeyPair(keyStoreKeyFactory.getKeyPair("order-jwt"));
return converter;
}

@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtTokenEnhancer());
}

}

到这里服务端就配置完成了

简单说下spring security oauth2的认证思路

client模式,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,客户端有自己的clientid,clientsecret对应于用户的username,password,而客户端也拥有自己的authorities,当采取client模式认证时,对应的权限也就是客户端自己的authorities password模式,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的clientid,clientsecret。此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限 你的系统已经有了一套用户体系,每个用户也有了一定的权限,可以采用password模式;如果仅仅是接口的对接,不考虑用户,则可以使用client模式

启动测试

因为我们配置的是客户端模式 ,所以通过postman客户端模式进行测试

http://127.0.0.1:9988/oauth/token?grant_type=client_credentials&client_id=order-server&client_secret=123456

返回的数据如下,access_token就是我们需要的jwt

1
2
3
4
5
6
7
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib3JkZXItc2VydmVyIl0sInNjb3BlIjpbInRlc3QiXSwiZXhwIjoxNTkwODA2NTM3LCJhdXRob3JpdGllcyI6WyJjbGllbnRfY3JlZGVudGlhbHMiXSwianRpIjoiODJiMGRmZGItOTA4MC00OGRmLTk1ZjEtZmM3NmZkZmZmNzBmIiwiY2xpZW50X2lkIjoib3JkZXItc2VydmVyIn0.wID2hyzroar_vf_Qf0aadMB61w1GUPpYDK5ovqwa2f8slAs-kEOkcdfcAh48YfIkM09IXXKCtd6pLum7g0etAAQlrmFhSguRUxqub5dVQSu0usFCmyUd0R8nr83dED6WkJahJK_griyAvcsitesz_1RwQ4n7ZHuA6CBlzdI-nP2U4MR5bATJgixOTJpIahp7BnfGBAp492a6-mjZjVsMmpCv6OLZ5xZhq0PebEToqkMMRrRB89sSXJPNpkbeLE78JYF9H3Z2pes0If_ooZGNQutfS6vyCMkIKAg05l2vkm6chSPn4eDD5S9ULRQuqdjHej0Mhzb_V9m8z3ZCYRppZw",
"token_type": "bearer",
"expires_in": 1199,
"scope": "test",
"jti": "82b0dfdb-9080-48df-95f1-fc76fdfff70f"
}
验证Token

https://jwt.io/ 进行token的验证

将我们的 access_token的值放进encoded里面可以看到解析出来了一些数据

我们发现验签是错误的

放置公钥后在测试,我们发现验证通过说明生成的token可用

资源服务器

导入POM文件

导入spring-security-jwt jar对jwt进行支持

1
2
3
4
5
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>

增加插件防止密钥没有打包

1
2
3
4
5
6
7
8
9
10
11
<!--防止jks文件被mavne编译导致不可用-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>cert</nonFilteredFileExtension>
<nonFilteredFileExtension>jks</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
防止私钥

将公钥放在资源服务器

JWT配置类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class JwtConfig {

public static final String public_cert = "public.cert";

@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;

@Bean
@Qualifier("tokenStore")
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter);
}

@Bean
public JwtAccessTokenConverter jwtTokenEnhancer() {
// 用作JWT转换器
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
Resource resource = new ClassPathResource(public_cert);
String publicKey;
try {
publicKey = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()));
} catch (IOException e) {
throw new RuntimeException(e);
}
//设置公钥
converter.setVerifierKey(publicKey);
return converter;
}
}
资源服务器配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
//开启资源服务器
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

@Autowired
private TokenStore tokenStore;

@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
//无状态
resources.stateless(true);
//设置token存储
resources.tokenStore(tokenStore).resourceId("order-server");
}
}
启动测试

访问接口 http://127.0.0.1:8083/order/createorder/1

注意postman接口测试需要配置jwt token

点击访问测试

到这里就完成了 一次jwt token的请求

评论