微服务OAuth 2.1认证授权可行性方案(Spring Security 6)

微服务OAuth 2.1认证授权可行性方案(Spring Security 6)

一、背景

Oauth2停止维护,基于OAuth 2.1OpenID Connect 1.0Spring Authorization Server模块独立于SpringCloud

本文开发环境如下:

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

https://spring.io/projects/spring-security#learn
https://spring.io/projects/spring-authorization-server#learn

二、微服务架构介绍

一个认证服务器(也是一个微服务),专门用于颁发JWT。
一个网关(也是一个微服务),用于白名单判断和JWT校验。
若干微服务。

本文的关键在于以下几点:

  • 搭建认证服务器
  • 网关白名单判断
  • 网关验证JWT
  • 认证服务器如何共享公钥,让其余微服务有JWT自校验的能力。

三、认证服务器

这里是官方文档https://spring.io/projects/spring-authorization-server#learn
基本上跟着Getting Started写完就可以。

1. 数据库创建

新建一个数据库xc_users
然后执行jar里自带的三个sql

这一步官方并没有给出,大概因为可以使用内存存储,在简单demo省去了持久化。不建立数据库可能也是可行的,我没试过。

2. 新建模块

新建一个auth模块,作为认证服务器。

3. 导入依赖和配置

language-xml
1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
language-yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
servlet:
context-path: /auth
port: 63070
spring:
application:
name: auth-service
profiles:
active: dev
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.65:3306/xc_users?serverTimezone=UTC&userUnicode=true&useSSL=false&
username: root
password: 1009

4. 安全认证配置类

language-java
1
2
3
4
@Configuration
@EnableWebSecurity
public class AuthServerSecurityConfig {
}

里面包含诸多内容,有来自Spring Security的,也有来自的Spring Authorization Server的。

  1. UserDetailsService 的实例,用于检索用户进行身份验证。
language-java
1
2
3
4
5
6
7
8
9
10
@Bean
public UserDetailsService userDetailsService() {

UserDetails userDetails = User
.withUsername("lisi")
.password("456")
.roles("read")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
  1. 密码编码器(可选,本文不用)
language-java
1
2
3
4
5
6
7
8
    @Bean
public PasswordEncoder passwordEncoder() {

// 密码为明文方式
return NoOpPasswordEncoder.getInstance();
// 或使用 BCryptPasswordEncoder
// return new BCryptPasswordEncoder();
}
  1. 协议端点的 Spring Security 过滤器链
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
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {

OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http

// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
)
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));

return http.build();
}
  1. 用于身份验证的 Spring Security 过滤器链。
    至于哪些要校验身份,哪些不用,根据自己需求写。
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
@Bean
@Order(2)
public SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {

http
.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers(new AntPathRequestMatcher("/actuator/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/login")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/oauth2/**")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/**/*.html")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/**/*.json")).permitAll()
.requestMatchers(new AntPathRequestMatcher("/auth/**")).permitAll()
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);

return http.build();
}
  1. 自定义验证转化器(可选)
language-java
1
2
3
4
5
6
7
private JwtAuthenticationConverter jwtAuthenticationConverter() {

JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
// 此处可以添加自定义逻辑来提取JWT中的权限等信息
// jwtConverter.setJwtGrantedAuthoritiesConverter(...);
return jwtConverter;
}
  1. 用于管理客户端的 RegisteredClientRepository 实例
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
28
29
30
	@Bean
public RegisteredClientRepository registeredClientRepository() {

RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("XcWebApp")
// .clientSecret("{noop}XcWebApp")
.clientSecret("XcWebApp")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://www.51xuecheng.cn")
// .postLogoutRedirectUri("http://localhost:63070/login?logout")
.scope("all")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(2)) // 设置访问令牌的有效期
.refreshTokenTimeToLive(Duration.ofDays(3)) // 设置刷新令牌的有效期
.reuseRefreshTokens(true) // 是否重用刷新令牌
.build())
.build();

return new InMemoryRegisteredClientRepository(registeredClient);
}
  1. 用于对访问令牌进行签名的实例
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
28
@Bean
public JWKSource<SecurityContext> jwkSource() {

KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {

KeyPair keyPair;
try {

KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {

throw new IllegalStateException(ex);
}
return keyPair;
}
  1. 用于解码签名访问令牌的JwtDecoder 实例
language-java
1
2
3
4
5
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {

return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
  1. 用于配置Spring Authorization ServerAuthorizationServerSettings 实例
language-java
1
2
3
4
5
@Bean
public AuthorizationServerSettings authorizationServerSettings() {

return AuthorizationServerSettings.builder().build();
}

这里可以设置各种端点的路径,默认路径点开builder()即可看到,如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static Builder builder() {

return new Builder()
.authorizationEndpoint("/oauth2/authorize")
.deviceAuthorizationEndpoint("/oauth2/device_authorization")
.deviceVerificationEndpoint("/oauth2/device_verification")
.tokenEndpoint("/oauth2/token")
.jwkSetEndpoint("/oauth2/jwks")
.tokenRevocationEndpoint("/oauth2/revoke")
.tokenIntrospectionEndpoint("/oauth2/introspect")
.oidcClientRegistrationEndpoint("/connect/register")
.oidcUserInfoEndpoint("/userinfo")
.oidcLogoutEndpoint("/connect/logout");
}

这里我必须吐槽一下,qnmd /.well-known/jwks.json,浪费我一下午。获取公钥信息的端点现在已经替换成了/oauth2/jwks。

四、认证服务器测试

基本上跟着Getting Started走就行。只不过端点的变动相较于Oauth2很大,还有使用方法上不同。

在配置RegisteredClient的时候,我们设置了三种GrantType,这里只演示两种AUTHORIZATION_CODECLIENT_CREDENTIALS

1. AUTHORIZATION_CODE(授权码模式)

1. 获取授权码

用浏览器打开以下网址,

language-txt
1
http://localhost:63070/auth/oauth2/authorize?client_id=XcWebApp&response_type=code&scope=all&redirect_uri=http://www.51xuecheng.cn

对应oauth2/authorize端点,后面的参数和当时设置RegisteredClient 保持对应就行。response_type一定是code
进入到登陆表单,输入lisi - 456登陆。

选择all,同意请求。

url被重定向到http://www.51xuecheng.cn,并携带一个code,这就是授权码。

language-txt
1
http://www.51xuecheng.cn/?code=9AexK_KFH1m3GiNBKsc0FU2KkedM2h_6yR-aKF-wPnpQT5USKLTqoZiSkHC3GUvt-56_ky-E3Mv5LbMeH9uyd-S1UV6kfJO6znqAcCAF43Yo4ifxTAQ8opoPJTjLIRUC

2. 获取JWT

使用apifox演示,postmanidea-http都可以。
localhost:63070/auth服务的/oauth2/token端点发送Post请求,同时需要携带认证信息。
认证信息可以如图所填的方法,也可以放到Header中,具体做法是将客户端ID和客户端密码用冒号(:)连接成一个字符串,进行Base64编码放入HTTP请求的Authorization头部中,前缀为Basic 。比如
Authorization: Basic bXlDbGllbnRJZDpteUNsaWVudFNlY3JldA==

得到JWT

2. CLIENT_CREDENTIALS(客户端凭证模式)

不需要授权码,直接向localhost:63070/auth服务的/oauth2/token端点发送Post请求,同时需要携带认证信息。

五、Gateway

至于gateway基础搭建步骤和gateway管理的若干微服务本文不做指导。

相较于auth模块(也就是Authorization Server),gateway的角色是Resource Server

1. 引入依赖

language-xml
1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

2. 添加白名单文件

resource下添加security-whitelist.properties文件。
写入以下内容

language-properties
1
2
3
/auth/**=????
/content/open/**=??????????
/media/open/**=??????????

3. 全局过滤器

在全局过滤器中,加载白名单,然后对请求进行判断。

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
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
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {


//白名单
private static List<String> whitelist = null;

static {

//加载白名单
try (
InputStream resourceAsStream = GatewayAuthFilter.class.getResourceAsStream("/security-whitelist.properties");
) {

Properties properties = new Properties();
properties.load(resourceAsStream);
Set<String> strings = properties.stringPropertyNames();
whitelist= new ArrayList<>(strings);

} catch (Exception e) {

log.error("加载/security-whitelist.properties出错:{}",e.getMessage());
e.printStackTrace();
}
}


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

String requestUrl = exchange.getRequest().getPath().value();
log.info("请求={}",requestUrl);
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {

if (pathMatcher.match(url, requestUrl)) {

return chain.filter(exchange);
}
}
}

private Mono<Void> buildReturnMono(String error, ServerWebExchange exchange) {

ServerHttpResponse response = exchange.getResponse();
String jsonString = JSON.toJSONString(new RestErrorResponse(error));
byte[] bits = jsonString.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}

@Override
public int getOrder() {

return 0;
}
}

4. 获取远程JWKS

yml配置中添加jwk-set-uri属性。

language-yml
1
2
3
4
5
6
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:63070/auth/oauth2/jwks

新建配置类,自动注入JwtDecoder

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class JwtDecoderConfig {

@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
String jwkSetUri;
@Bean
public JwtDecoder jwtDecoderLocal() {

return NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
}

5. 校验JWT

在全局过滤器中补全逻辑。

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
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
@Component
@Slf4j
public class GatewayAuthFilter implements GlobalFilter, Ordered {


@Lazy
@Autowired
private JwtDecoder jwtDecoderLocal;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

String requestUrl = exchange.getRequest().getPath().value();
log.info("请求={}",requestUrl);
AntPathMatcher pathMatcher = new AntPathMatcher();
//白名单放行
for (String url : whitelist) {

if (pathMatcher.match(url, requestUrl)) {

return chain.filter(exchange);
}
}

//检查token是否存在
String token = getToken(exchange);
log.info("token={}",token);
if (StringUtils.isBlank(token)) {

return buildReturnMono("没有携带Token,没有认证",exchange);
}
// return chain.filter(exchange);
try {

Jwt jwt = jwtDecoderLocal.decode(token);
// 如果没有抛出异常,则表示JWT有效

// 此时,您可以根据需要进一步检查JWT的声明
log.info("token有效期至:{}", formatInstantTime(jwt.getExpiresAt()));
return chain.filter(exchange);
} catch (JwtValidationException e) {

log.info("token验证失败:{}",e.getMessage());
return buildReturnMono("认证token无效",exchange);
}
}

/**
* 从请求头Authorization中获取token
*/
private String getToken(ServerWebExchange exchange) {

String tokenStr = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isBlank(tokenStr)) {

return null;
}
String token = tokenStr.split(" ")[1];
if (StringUtils.isBlank(token)) {

return null;
}
return token;
}

/**
* 格式化Instant时间
*
* @param expiresAt 在到期
* @return {@link String}
*/
public String formatInstantTime(Instant expiresAt) {

// 将Instant转换为系统默认时区的LocalDateTime
LocalDateTime dateTime = LocalDateTime.ofInstant(expiresAt, ZoneId.systemDefault());

// 定义日期时间的格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

// 格式化日期时间并打印
return dateTime.format(formatter);
}

}

6. 测试(如何携带JWT)

携带一个正确的JWTgateway发送请求。
JWT写到HeaderAuthorization字段中,添加前缀Bearer(用空格隔开),向gateway微服务所在地址发送请求。

gateway日志输出。

六、后记

颁发JWT都归一个认证服务器管理,校验JWT都归Gateway管理,至于授权,则由各个微服务自己定义。耦合性低、性能较好。

关于授权,可以接着这篇文章。
微服务OAuth 2.1认证授权Demo方案(Spring Security 6)

微服务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"
}
}
从RSA角度出发解析JWT原理

从RSA角度出发解析JWT原理

在今天的数字化世界中,安全地传递信息变得越来越重要。JSON Web TokenJWT)作为一种流行的开放标准,为简化服务器与客户端之间的安全交流提供了一种高效的方式。本文旨在深入解析JWT的工作原理,并通过示例演示如何使用JWT进行安全通信。我们将从JWT的基本组成部分讲起,探讨公密钥加密的原理,解释为什么JWT是安全的,以及如何验证JWT的有效性。

一、JWT介绍

1. JWT组成部分

JWT由三个部分组成,它们分别是头部(Header)、载荷(Payload)、和签名(Signature)。通过将这三部分用点(.)连接起来,形成了一个完整的JWT字符串。例如:xxxxx.yyyyy.zzzzz

2. 头部(Header)

头部通常由两部分组成:令牌类型(typ)和签名算法(alg)。例如:

language-json
1
2
3
4
5
{

"kid": "33bd1cad-62a6-4415-89a6-c2c816f3d3b1",
"alg": "RS256"
}
  • kid (Key ID):密钥标识符,用于指明验证JWT签名时使用的密钥。在含有多个密钥的系统中,这可以帮助接收者选择正确的密钥进行验证。这里的值33bd1cad-62a6-4415-89a6-c2c816f3d3b1是一个UUID,唯一标识了用于签名的密钥。
  • alg (Algorithm):指明用于签名的算法,这里是RS256,表示使用RSA签名算法和SHA-256散列算法。这种算法属于公钥/私钥算法,意味着使用私钥进行签名,而用公钥进行验证。

3. 载荷(Payload)

载荷包含了所要传递的信息,这些信息以声明(claims)的形式存在。声明可以是用户的身份标识,也可以是其他任何必要的信息。载荷示例:

language-json
1
2
3
4
5
6
7
8
9
10
{

"sub": "XcWebApp",
"aud": "XcWebApp",
"nbf": 1707373072,
"iss": "http://localhost:63070/auth",
"exp": 1707380272,
"iat": 1707373072,
"jti": "62e885c5-6b3f-49a2-aa10-b2e872a52b33"
}
  • sub (Subject):主题,标识了这个JWT的主体,通常是指用户的唯一标识。这里XcWebApp可能是一个应用或用户标识。
  • aud (Audience):受众,标识了这个JWT的预期接收者。这里同样是XcWebApp,意味着这个JWT是为XcWebApp这个应用或服务生成的。
  • nbf (Not Before):生效时间,这个时间之前,JWT不应被接受处理。这里的时间是Unix时间戳格式,表示JWT生效的具体时间。
  • iss (Issuer):发行者,标识了这个JWT的发行方。这里是http://localhost:63070/auth,表明JWT由本地的某个认证服务器发行。
  • exp (Expiration Time):过期时间,这个时间之后,JWT不再有效。同样是Unix时间戳格式,表示JWT过期的具体时间。
  • iat (Issued At):发行时间,JWT创建的时间。这提供了JWT的时间信息,也是Unix时间戳格式。
  • jti (JWT ID):JWT的唯一标识符,用于防止JWT被重放(即两次使用同一个JWT)。这里的值是一个UUID,确保了JWT的唯一性。

4. 签名(Signature)

签名是对头部和载荷的加密保护,确保它们在传输过程中未被篡改。根据头部中指定的算法(例如HS256),使用私钥对头部和载荷进行签名。

二、深入理解JWT签名验证

1. 签名生成

  1. 哈希处理 :首先对JWT的头部和载荷部分(经过Base64编码并用.连接的字符串)进行哈希处理,生成一个哈希值。这个步骤是为了确保数据的完整性,即使是微小的改动也会导致哈希值有很大的不同。
  2. 私钥加密哈希值 :然后使用发行者的私钥对这个哈希值进行加密,生成的结果就是JWT的签名部分。这个加密的哈希值(签名)附加在JWT的后面。

2. 签名验证

  1. 哈希处理 :接收方收到JWT后,会独立地对其头部和载荷部分进行同样的哈希处理,生成一个哈希值。
  2. 公钥解密签名 :接收方使用发行者的公钥对签名(加密的哈希值)进行解密,得到另一个哈希值。
  3. 哈希值比较比较这两个哈希值。如果它们相同,就证明了JWT在传输过程中未被篡改,因为只有相应的私钥能够生成一个对应的、能够通过公钥解密并得到原始哈希值的签名。

3. 为什么JWT是安全的

由于破解RSA算法的难度极高,没有私钥就无法生成有效的签名,因此无法伪造签名。这就是为什么使用公钥进行签名验证是安全的原因。

三、如何验证JWT是否有效

对于一个JWT,我们可以使用Base64解码,获取前两部分信息,可以进行token是否过期等验证,具体取决于前两部分具体内容,这是可以人为设置的。
对于签名部分,可以用公钥去验证其有效性(即是否被纂改)。

四、 Why JWT?

为什么是JWT?
首先是不可伪造的安全性,其次,因为你会发现,只要有公钥就可以验证JWT这种token,这也就意味着对于微服务来说,任意微服务都有能力自行验证JWT,而不需要额外的验证模块。这种自校验没有用到网络通信,性能十分好。同时,JWT有时间限制,一定程度上也提高了最坏情况的安全性。