oauth 整合 jwt
# 什么是jwt?
以下是官网解释
JSON Web Token (JWT) 是一个开放标准 ( RFC 7519 (opens new window) ),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。该信息可以被验证和信任,因为它是经过数字签名的。JWT 可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥/私钥对进行签名。
虽然 JWT 可以加密以在各方之间提供保密,但我们将重点关注签名令牌。签名令牌可以验证其中包含的声明的完整性,而加密令牌则对其他方隐藏这些声明。当使用公钥/私钥对对令牌进行签名时,签名还证明只有持有私钥的一方才是对其进行签名的一方。
官网: https://jwt.io/
标准: https://tools.ietf.org/html/rfc7519
JWT令牌的优点:
jwt基于json,非常方便解析。
可以在令牌中自定义丰富的内容,易扩展。
通过非对称加密算法及数字签名技术,JWT防止篡改,安全性高。
资源服务使用JWT可不依赖授权服务即可完成授权。
缺点:
JWT令牌较长,占存储空间比较大。
# jwt应用场景
- 授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小,并且能够轻松跨不同域使用。
- 信息交换:JSON Web Tokens 是一种在各方之间安全传输信息的好方法。因为 JWT 可以被签名——例如,使用公钥/私钥对——你可以确定发件人就是他们所说的那样。此外,由于使用标头和有效负载计算签名,因此您还可以验证内容是否未被篡改。
# JWT组成
一个JWT实际上就是一个字符串,它由用(.)分割的三部分组成,头部(header)、载荷(payload)与签名 (signature)。
因此,JWT 通常如下所示。
xxxxx.yyyyy.zzzzz
接下来分别介绍这三个部分
# 头部(header)
头部用于描述关于该JWT的最基本的信息:类型(即JWT)以及签名所用的算法(如 HMACSHA256或RSA)等。
这也可以被表示成一个JSON对象:
{
"alg": "HS256",
"typ": "JWT"
}
2
3
4
然后,这个 JSON 被Base64Url编码以形成 JWT 的第一部分。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
载荷(payload)
存放有效信息的地方,包含三个部分:
- 标准中注册的声明(建议但不强制使用),例如,iss: jwt签发者,sub: jwt所面向的用户 ,aud: 接收jwt的一方 ,exp: jwt的过期时间,这个过期时间必须要大于签发时间 ,nbf: 定义在什么时间之前,该jwt都是不可用的,iat: jwt的签发时间 ,jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不 建议添加敏感信息,因为该部分在客户端可解密。
- 私有的声明 :私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,该部分信息可以归类为明文信息
定义一个有效载荷
{
"sub": "1234567890",
"name": "Jack",
"iat": 1516239022
}
2
3
4
5
然后将其进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphY2siLCJpYXQiOjE1MTYyMzkwMjJ9
# 签名(signature)
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header (base64加密后的)
payload (base64加密后的)
secret(密钥,在服务端)
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分:
hrInLPw3Fzh2QZMSTXNYBRQbB2_WdyW0Yuemp3_Ht_A
把这三个部分连在一起就得到一个完整的jwt
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkphY2siLCJpYXQiOjE1MTYyMzkwMjJ9.hrInLPw3Fzh2QZMSTXNYBRQbB2_WdyW0Yuemp3_Ht_A
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的 签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。
# jwt使用
是在请求头里加入Authorization,并加上Bearer标注:Authorization: Bearer <token>
下图显示了如何获取 JWT 并将其用于访问 API 或资源:
应用程序或客户端向授权服务器请求授权。
当授权被授予时,授权服务器向应用程序返回一个访问令牌。
应用程序使用访问令牌来访问受保护的资源(如 API)。
# 快速使用
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
引入依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.6.0</version>
</dependency>
2
3
4
5
生成token
private static final String SECRETKEY="1111111";
@Test
public void test() {
Map map = new HashMap();
map.put("phone","13211111");
map.put("email","11@qq.com");
//创建一个JwtBuilder对象
JwtBuilder jwtBuilder = Jwts.builder()
//声明的标识{"jti":"666"}
.setId("666")
//主体,用户{"sub":"jack"}
.setSubject("jack")
//创建日期{"ita":"xxxxxx"}
.setIssuedAt(new Date())
//设置过期时间 10分钟
.setExpiration(new Date(System.currentTimeMillis()+600*1000))
//claims 有效载荷
.addClaims(map)
.claim("roles","admin")
//签名手段,参数1:算法,参数2:密钥
.signWith(SignatureAlgorithm.HS256, SECRETKEY);
//获取token jwt
String token = jwtBuilder.compact();
System.out.println(token);
//三部分的base64解密
String[] split = token.split("\\.");
//{"alg":"HS256"}
System.out.println(Base64Codec.BASE64.decodeToString(split[0]));
//{"jti":"666","sub":"jack","iat":1636896993,"exp":1636897053,"phone":"13211111",
"email":"11@qq.com","roles":"admin","logo":"xxx.jpg"}
System.out.println(Base64Codec.BASE64.decodeToString(split[1]));
//base64无法解密
System.out.println(Base64Codec.BASE64.decodeToString(split[2]));
}
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
验证解析token
public void testParseToken(){
//token
String token ="eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiJqYWNrIiwiaWF0IjoxNjM2ODk3NDk2LCJleHAiOjE2MzY4OTgwOTYsInBob25lIjoiMTMyMTExMTEiLCJlbWFpbCI6IjExQHFxLmNvbSIsInJvbGVzIjoiYWRtaW4iLCJsb2dvIjoieHh4LmpwZyJ9.VHZDARQFehcZZsp9Uurd4yWY_TwBwi8UVN01s3r7cfU";
//解析token获取载荷中的声明对象
Claims claims = Jwts.parser()
.setSigningKey(SECRETKEY)
.parseClaimsJws(token)
.getBody();
System.out.println("id:"+claims.getId());
System.out.println("subject:"+claims.getSubject());
System.out.println("issuedAt:"+claims.getIssuedAt());
DateFormat sf =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("签发时间:"+sf.format(claims.getIssuedAt()));
System.out.println("过期时间:"+sf.format(claims.getExpiration()));
System.out.println("当前时间:"+sf.format(new Date()));
System.out.println("roles:"+claims.get("roles"));
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
打印结果:
id:666
subject:jack
issuedAt:Sun Nov 14 21:44:56 CST 2021
签发时间:2021-11-14 21:44:56
过期时间:2021-11-14 21:54:56
当前时间:2021-11-14 21:45:30
roles:admin
2
3
4
5
6
7
# Spring Security 整合Oauth2、JWT
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring‐security‐jwt</artifactId>
<version>1.0.9.RELEASE</version>
</dependency>
2
3
4
5
6
7
8
9
省略了spring security 配置
配置JwtTokenStore
@Configuration
public class JwtTokenStoreConfig {
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter accessTokenConverter = new
JwtAccessTokenConverter();
//配置JWT使用的秘钥
accessTokenConverter.setSigningKey("111111");
return accessTokenConverter;
}
@Bean
public JwtTokenEnhancer jwtTokenEnhancer() {
return new JwtTokenEnhancer();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
配置扩展jwt存储内容
public class JwtTokenEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("phone", "1321111111");
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
return accessToken;
}
}
2
3
4
5
6
7
8
9
10
11
指定存储策略是jwt
@Autowired
@Qualifier("jwtTokenStore")
private TokenStore tokenStore;
@Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private JwtTokenEnhancer jwtTokenEnhancer;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//配置JWT的内容增强器
TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
List<TokenEnhancer> delegates = new ArrayList<>();
delegates.add(jwtTokenEnhancer);
delegates.add(jwtAccessTokenConverter);
enhancerChain.setTokenEnhancers(delegates);
endpoints.authenticationManager(authenticationManagerBean) //使用密码模式需要配置
.tokenStore(tokenStore) //配置存储令牌策略
.accessTokenConverter(jwtAccessTokenConverter)
.tokenEnhancer(enhancerChain) //配置tokenEnhancer
.reuseRefreshTokens(false) //refresh_token是否重复使用
.userDetailsService(userService) //刷新令牌授权包含对用户信息的检查
.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); //支持GET,POST请求
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//允许表单认证
security.allowFormAuthenticationForClients();
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
//配置client_id
.withClient("client")
//配置client-secret
.secret(passwordEncoder.encode("111111"))
//配置访问token的有效期
.accessTokenValiditySeconds(3600)
//配置刷新token的有效期
.refreshTokenValiditySeconds(864000)
//配置redirect_uri,用于授权成功后跳转
.redirectUris("http://www.baidu.com")
//配置申请的权限范围
.scopes("all")
/**
* 配置grant_type,表示授权类型
* authorization_code: 授权码
* password: 密码
* client_credentials: 客户端
* refresh_token: 更新令牌
*/
.authorizedGrantTypes("authorization_code","password","refresh_token");
}
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
通过密码方式获取token
http://localhost:8080/oauth/token?grant_type=password&username=jack&password=123456&client_id=client&client_secret=123123&scope=all
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwaG9uZSI6IjEzMjExMTExMTEiLCJ1c2VyX25hbWUiOiJqYWNrIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYzNjkwNjU4MiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiYzA1ZWJlNzItMmU1Zi00OGJjLThhNzYtODAzMDIxNGM3MzliIiwiY2xpZW50X2lkIjoiY2xpZW50In0.6DdtIkeR9inSuHAxSTo4lQvanDqrKJ0v8ke6ZzWQZdA","token_type":"bearer","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwaG9uZSI6IjEzMjExMTExMTEiLCJ1c2VyX25hbWUiOiJqYWNrIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImMwNWViZTcyLTJlNWYtNDhiYy04YTc2LTgwMzAyMTRjNzM5YiIsImV4cCI6MTYzNzc2Njk4MiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiOWJjMDkzM2ItMGZkNy00NTY2LWFhMTItYzlmMzYzNDBlZmY4IiwiY2xpZW50X2lkIjoiY2xpZW50In0.pcKNqiMZv4G9uKrv7P9qNYys6MQL0KJ-6MKz6HY9sbA","expires_in":3599,"scope":"all","phone":"1321111111","jti":"c05ebe72-2e5f-48bc-8a76-8030214c739b"}
使用token
http://localhost:8080/user/getUser?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwaG9uZSI6IjEzMjExMTExMTEiLCJ1c2VyX25hbWUiOiJqYWNrIiwic2NvcGUiOlsiYWxsIl0sImV4cCI6MTYzNjkwNjU4MiwiYXV0aG9yaXRpZXMiOlsiYWRtaW4iXSwianRpIjoiYzA1ZWJlNzItMmU1Zi00OGJjLThhNzYtODAzMDIxNGM3MzliIiwiY2xpZW50X2lkIjoiY2xpZW50In0.6DdtIkeR9inSuHAxSTo4lQvanDqrKJ0v8ke6ZzWQZdA