微服务OAuth 2.1扩展额外信息到JWT并解析(Spring Security 6)

微服务OAuth 2.1扩展额外信息到JWT并解析(Spring Security 6)

一、简介

Version
Java 17
SpringCloud 2023.0.0
SpringBoot 3.2.1
Spring Authorization Server 1.2.1
Spring Security 6.2.1
mysql 8.2.0

Spring Authorization Server 使用JWT时,前两部分默认格式如下

language-json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{

"kid": "33bd1cad-62a6-4415-89a6-c2c816f3d3b1",
"alg": "RS256"
}
{

"sub": "XcWebApp",
"aud": "XcWebApp",
"nbf": 1707373072,
"iss": "http://localhost:63070/auth",
"exp": 1707380272,
"iat": 1707373072,
"jti": "62e885c5-6b3f-49a2-aa10-b2e872a52b33"
}

现在我们要把用户信息也扩展到JWT,最简便的方法就是将用户信息写成JSON字符串替换sub字段。其中用户信息由xc_user数据库表存储。

二、重写UserDetailsService

注释掉原来的UserDetailsService实例。新建一个实现类,如下

language-java
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
@Component
@Slf4j
public class UserServiceImpl implements UserDetailsService {

@Autowired
XcUserMapper xcUserMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//根据username查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>()
.eq(XcUser::getUsername, username));
//用户不存在,返回null
if (xcUser == null){

return null;
}
//用户存在,拿到密码,封装成UserDetails,密码对比由框架进行
String password = xcUser.getPassword();
//扩展用户信息
xcUser.setPassword(null);
String userInfo = JSON.toJSONString(xcUser);
UserDetails userDetails = User.withUsername(userInfo).password(password).authorities("read").build();
return userDetails;
}
}
  • 如果XcUsernull,返回null,这里处理了用户不存在的情况。
  • 如果用户存在,则获取密码,放到UserDetails 中,密码比对过程我们不关心,由框架完成。如果使用了加密算法,这里的password应该是密文。
  • 我们可以把用户信息转成JSON字符串放入withUsername。这样框架生成JWT时就会把用户信息也放进去。

是的,你没有猜错,UserDetails 只要返回密码框架就能比对成功,不需要再返回username

三、Controller解析JWT获取用户信息

写一个工具类,通过Security ContextHolder.getContext()上下文获取Authentication然后解析JWT

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Slf4j
public class SecurityUtil {

public static XcUser getUser(){

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken) {

JwtAuthenticationToken jwtAuth = (JwtAuthenticationToken) authentication;
Map<String, Object> tokenAttributes = jwtAuth.getTokenAttributes();
Object sub = tokenAttributes.get("sub");
return JSON.parseObject(sub.toString(), XcUser.class);
}
return null;
}
@Data
public static class XcUser implements Serializable {

//...
}
}

JwtAuthenticationTokenSpring Authorization Server的一个类,可以帮助我们解析JWT

四、后记

SecurityContextHolder.getContext().getAuthentication()解析我们放进去的XcUser的方法不止一种,将其打印出来就可以看出,有多个地方包含了XcUser,例如。

language-json
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
{

"authenticated": true,
"authorities": [{

"authority": "SCOPE_all"
}],
"credentials": {

"audience": ["XcWebApp"],
"claims": {

"sub": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"aud": ["XcWebApp"],
"nbf": "2024-02-08T14:01:16Z",
"scope": ["all"],
"iss": "http://localhost:63070/auth",
"exp": "2024-02-08T16:01:16Z",
"iat": "2024-02-08T14:01:16Z",
"jti": "91df8f15-2096-4e03-a927-877b51bf5997"
},
"expiresAt": "2024-02-08T16:01:16Z",
"headers": {

"kid": "e14df18d-1c95-441d-80d6-8457f3ceba9e",
"alg": "RS256"
},
"id": "91df8f15-2096-4e03-a927-877b51bf5997",
"issuedAt": "2024-02-08T14:01:16Z",
"issuer": "http://localhost:63070/auth",
"notBefore": "2024-02-08T14:01:16Z",
"subject": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"tokenValue": "eyJraWQiOiJlMTRkZjE4ZC0xYzk1LTQ0MWQtODBkNi04NDU3ZjNjZWJhOWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4IDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJhZG1pblwiLFwidXR5cGVcIjpcIjEwMTAwM1wifSIsImF1ZCI6IlhjV2ViQXBwIiwibmJmIjoxNzA3NDAwODc2LCJzY29wZSI6WyJhbGwiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MzA3MC9hdXRoIiwiZXhwIjoxNzA3NDA4MDc2LCJpYXQiOjE3MDc0MDA4NzYsImp0aSI6IjkxZGY4ZjE1LTIwOTYtNGUwMy1hOTI3LTg3N2I1MWJmNTk5NyJ9.NZ3f_Pkh871L1c8XkV2PxHfn17pWjaRvBd9HQQTRJhfFvNBN7zoh2riumpfVUj_xVmnCadVX3YE4ARxc0CuiV1QyVDpFnmKiuvWdsRVV9NF5Kkb67CGtF2zw1l2gFdSDFWAwOq1SemtogHX5a4XFF2kG6kx9ZOSL4EoiQMMhUOwfY6mL9Zdcgq_E28kfnrAk__q84rgo9JPvj3jH6cm_oS63-tNXZYdClDG61DHS4Bw7cswUfVf_bcI_a8kXfiM8SzCnROvxe1hU2dM88qxUgkI1GPlrtZhe9z7113XP7ilaPo2UknCFh_OSbfUeUDmP1GpTaspfGmnHhBXLQyG06Q"
},
"details": {

"remoteAddress": "192.168.222.1"
},
"name": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"principal": {

"audience": ["XcWebApp"],
"claims": {

"sub": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"aud": ["XcWebApp"],
"nbf": "2024-02-08T14:01:16Z",
"scope": ["all"],
"iss": "http://localhost:63070/auth",
"exp": "2024-02-08T16:01:16Z",
"iat": "2024-02-08T14:01:16Z",
"jti": "91df8f15-2096-4e03-a927-877b51bf5997"
},
"expiresAt": "2024-02-08T16:01:16Z",
"headers": {

"kid": "e14df18d-1c95-441d-80d6-8457f3ceba9e",
"alg": "RS256"
},
"id": "91df8f15-2096-4e03-a927-877b51bf5997",
"issuedAt": "2024-02-08T14:01:16Z",
"issuer": "http://localhost:63070/auth",
"notBefore": "2024-02-08T14:01:16Z",
"subject": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"tokenValue": "eyJraWQiOiJlMTRkZjE4ZC0xYzk1LTQ0MWQtODBkNi04NDU3ZjNjZWJhOWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4IDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJhZG1pblwiLFwidXR5cGVcIjpcIjEwMTAwM1wifSIsImF1ZCI6IlhjV2ViQXBwIiwibmJmIjoxNzA3NDAwODc2LCJzY29wZSI6WyJhbGwiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MzA3MC9hdXRoIiwiZXhwIjoxNzA3NDA4MDc2LCJpYXQiOjE3MDc0MDA4NzYsImp0aSI6IjkxZGY4ZjE1LTIwOTYtNGUwMy1hOTI3LTg3N2I1MWJmNTk5NyJ9.NZ3f_Pkh871L1c8XkV2PxHfn17pWjaRvBd9HQQTRJhfFvNBN7zoh2riumpfVUj_xVmnCadVX3YE4ARxc0CuiV1QyVDpFnmKiuvWdsRVV9NF5Kkb67CGtF2zw1l2gFdSDFWAwOq1SemtogHX5a4XFF2kG6kx9ZOSL4EoiQMMhUOwfY6mL9Zdcgq_E28kfnrAk__q84rgo9JPvj3jH6cm_oS63-tNXZYdClDG61DHS4Bw7cswUfVf_bcI_a8kXfiM8SzCnROvxe1hU2dM88qxUgkI1GPlrtZhe9z7113XP7ilaPo2UknCFh_OSbfUeUDmP1GpTaspfGmnHhBXLQyG06Q"
},
"token": {

"audience": ["XcWebApp"],
"claims": {

"sub": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"aud": ["XcWebApp"],
"nbf": "2024-02-08T14:01:16Z",
"scope": ["all"],
"iss": "http://localhost:63070/auth",
"exp": "2024-02-08T16:01:16Z",
"iat": "2024-02-08T14:01:16Z",
"jti": "91df8f15-2096-4e03-a927-877b51bf5997"
},
"expiresAt": "2024-02-08T16:01:16Z",
"headers": {

"kid": "e14df18d-1c95-441d-80d6-8457f3ceba9e",
"alg": "RS256"
},
"id": "91df8f15-2096-4e03-a927-877b51bf5997",
"issuedAt": "2024-02-08T14:01:16Z",
"issuer": "http://localhost:63070/auth",
"notBefore": "2024-02-08T14:01:16Z",
"subject": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"tokenValue": "eyJraWQiOiJlMTRkZjE4ZC0xYzk1LTQ0MWQtODBkNi04NDU3ZjNjZWJhOWUiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ7XCJjcmVhdGVUaW1lXCI6XCIyMDIyLTA5LTI4IDA4OjMyOjAzXCIsXCJpZFwiOlwiNDhcIixcIm5hbWVcIjpcIuezu-e7n-euoeeQhuWRmFwiLFwic2V4XCI6XCIxXCIsXCJzdGF0dXNcIjpcIjFcIixcInVzZXJuYW1lXCI6XCJhZG1pblwiLFwidXR5cGVcIjpcIjEwMTAwM1wifSIsImF1ZCI6IlhjV2ViQXBwIiwibmJmIjoxNzA3NDAwODc2LCJzY29wZSI6WyJhbGwiXSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MzA3MC9hdXRoIiwiZXhwIjoxNzA3NDA4MDc2LCJpYXQiOjE3MDc0MDA4NzYsImp0aSI6IjkxZGY4ZjE1LTIwOTYtNGUwMy1hOTI3LTg3N2I1MWJmNTk5NyJ9.NZ3f_Pkh871L1c8XkV2PxHfn17pWjaRvBd9HQQTRJhfFvNBN7zoh2riumpfVUj_xVmnCadVX3YE4ARxc0CuiV1QyVDpFnmKiuvWdsRVV9NF5Kkb67CGtF2zw1l2gFdSDFWAwOq1SemtogHX5a4XFF2kG6kx9ZOSL4EoiQMMhUOwfY6mL9Zdcgq_E28kfnrAk__q84rgo9JPvj3jH6cm_oS63-tNXZYdClDG61DHS4Bw7cswUfVf_bcI_a8kXfiM8SzCnROvxe1hU2dM88qxUgkI1GPlrtZhe9z7113XP7ilaPo2UknCFh_OSbfUeUDmP1GpTaspfGmnHhBXLQyG06Q"
},
"tokenAttributes": {

"sub": "{\"createTime\":\"2022-09-28 08:32:03\",\"id\":\"48\",\"name\":\"系统管理员\",\"sex\":\"1\",\"status\":\"1\",\"username\":\"admin\",\"utype\":\"101003\"}",
"aud": ["XcWebApp"],
"nbf": "2024-02-08T14:01:16Z",
"scope": ["all"],
"iss": "http://localhost:63070/auth",
"exp": "2024-02-08T16:01:16Z",
"iat": "2024-02-08T14:01:16Z",
"jti": "91df8f15-2096-4e03-a927-877b51bf5997"
}
}
Spring Authorization Server Spring Security密码加密

Spring Authorization Server Spring Security密码加密

一、修改密码编码器

BCryptPasswordEncoder举例。
直接将其注册成PasswordEncoderBean即可。

language-java
1
2
3
4
5
6
7
8
    @Bean
public PasswordEncoder passwordEncoder() {

// 密码为明文方式
// return NoOpPasswordEncoder.getInstance();
// 或使用 BCryptPasswordEncoder
return new BCryptPasswordEncoder();
}

二、效果

使用了加密算法后,无论是RegisteredClient的密码还是UserDetailsService的密码,都会以密文的方式存储在服务器上。

但是前端输入的密码仍然是以明文的方式出现并传到服务器,之后服务器会对明文进行相同的手段(指对同样的明文,密文相同)加密,比较两个密文是否一致。

三、注意点

1. RegisteredClient

language-java
1
2
3
4
5
6
7
8
9
   @Bean
public RegisteredClientRepository registeredClientRepository() {

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("XcWebApp")
.clientSecret(passwordEncoder().encode("XcWebApp"))
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}

RegisteredClientclientSecret仍然需要提供密文。

是因为,加密这个行为,只在服务器校验前端发送的明文时使用,至于对照物,则是代码中提供好的密文,所以这个需要提供密文。

2. UserDetailsService

对于UserDetailsService也是,密码也需要提供现成的密文形式。
下面的代码中数据库保存的是password密文。

language-java
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
@Component
@Slf4j
public class UserServiceImpl implements UserDetailsService {

@Autowired
XcUserMapper xcUserMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//根据username查询数据库
XcUser xcUser = xcUserMapper.selectOne(new LambdaQueryWrapper<XcUser>()
.eq(XcUser::getUsername, username));
//用户不存在,返回null
if (xcUser == null){

return null;
}
//用户存在,拿到密码,封装成UserDetails,密码对比由框架进行
String password = xcUser.getPassword();

UserDetails userDetails = User.withUsername(username).password(password).authorities("read").build();
return userDetails;
}
}