写在前面

Spring Oauth 提供了对 jwt 的支持,要实现 jwt 功能很简单,但是在指定授权范围时将会遇到无法指定授权范围的问题,本文主体描述的是如何解决这个问题。

本文依赖环境:

<!-- spring-security-oauth2 2.3.4.RELEASE -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-oauth2</artifactId>
    <version>2.2.4.RELEASE</version>
</dependency>

1. 配置实现

AuthServerConfiguration 对应配置中添加相应的 jwt 配置,具体如下:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
        .tokenStore(tokenStore())
        .accessTokenConverter(jwtAccessTokenConverter() );
}

/**
 * 令牌存储(jwt存储令牌)
 */
@Bean
public TokenStore tokenStore() {
    return new JwtTokenStore(jwtAccessTokenConverter());
}

/**
 * 在JWT编码的令牌值和OAuth身份验证信息(双向)之间转换的助手。 授予令牌时,还充当TokenEnhancer
 * @return
 */
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("123");
    return converter;
}

完成!

2. 如何指定授权范围

使用 jwt 之后一切正常,但是发现无法指定授权范围了,我这里请求了 readinfo 两个部分内容,前端相应的界面如下,没有了可选的授权类型选项,只有授权和拒绝。

Oauth2使用JWT生成Token无法指定授权范围的解决方法

走完授权流程之后发现,授权范围是正常的,就是失去了指定某个范围是否授权的功能。

Oauth2使用JWT生成Token无法指定授权范围的解决方法_2

如果要使用 jwt 同时还要指定授权范围 scope,那么这时候该如何实现?

3. 调试

调试配置部分代码,可以看到 JwtTokenStore 影响到了如下内容:

// 如果 tokenStore 为jwt的返回true,也就代表禁用 approval
private boolean isApprovalStoreDisabled() {
	return approvalStoreDisabled || (tokenStore() instanceof JwtTokenStore);
}

.....

// 在这里调用了 isApprovalStoreDisabled 导致其不会执行 if 内部的逻辑,返回了null
private ApprovalStore approvalStore() {
	if (approvalStore == null && tokenStore() != null && !isApprovalStoreDisabled()) {
		TokenApprovalStore tokenApprovalStore = new TokenApprovalStore();
		tokenApprovalStore.setTokenStore(tokenStore());
		this.approvalStore = tokenApprovalStore;
	}
	return this.approvalStore;
}

.....

// 这里导致 approvalStore 返回 null,从而不会 new ApprovalStoreUserApprovalHandler,new了TokenStoreUserApprovalHandler,所以原因应该在这里,因为在 AuthorizationEndpoint中用到了UserApprovalHandler
private UserApprovalHandler userApprovalHandler() {
	if (userApprovalHandler == null) {
		if (approvalStore() != null) {
			ApprovalStoreUserApprovalHandler handler = new ApprovalStoreUserApprovalHandler();
			handler.setApprovalStore(approvalStore());
			handler.setRequestFactory(requestFactory());
			handler.setClientDetailsService(clientDetailsService);
			this.userApprovalHandler = handler;
		}
		else if (tokenStore() != null) {
			TokenStoreUserApprovalHandler userApprovalHandler = new TokenStoreUserApprovalHandler();
			userApprovalHandler.setTokenStore(tokenStore());
			userApprovalHandler.setClientDetailsService(clientDetailsService());
			userApprovalHandler.setRequestFactory(requestFactory());
			this.userApprovalHandler = userApprovalHandler;
		}
		else {
			throw new IllegalStateException("Either a TokenStore or an ApprovalStore must be provided");
		}
	}
	return this.userApprovalHandler;
}

Tips: Get 无用小知识,通过在配置类添加 approvalStoreDisabled() 配置可以关闭对单个 scope 项的授权修改。

切换到 AuthorizationEndpoint 类,搜索 userApprovalPage 使用,可以发现在调整授权界面前将会执行 userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal),在确定授权进行验证时( approveOrDeny方法)会调用 updateAfterApprovalisApproved

查看这三个方法的实现逻辑,可以看到 TokenStoreUserApprovalHandler 相比于 ApprovalStoreUserApprovalHandler 少掉了很多关于 scope 校验的逻辑。

getUserApprovalRequest 方法

// ApprovalStoreUserApprovalHandler
public Map<String, Object> getUserApprovalRequest(AuthorizationRequest authorizationRequest,
		Authentication userAuthentication) {
	Map<String, Object> model = new HashMap<String, Object>();
	model.putAll(authorizationRequest.getRequestParameters());
	Map<String, String> scopes = new LinkedHashMap<String, String>();
	for (String scope : authorizationRequest.getScope()) {
		scopes.put(scopePrefix + scope, "false");
	}
	for (Approval approval : approvalStore.getApprovals(userAuthentication.getName(),
			authorizationRequest.getClientId())) {
		if (authorizationRequest.getScope().contains(approval.getScope())) {
			scopes.put(scopePrefix + approval.getScope(),
					approval.getStatus() == ApprovalStatus.APPROVED ? "true" : "false");
		}
	}
	model.put("scopes", scopes);
	return model;
}

// TokenStoreUserApprovalHandler
public Map<String, Object> getUserApprovalRequest(AuthorizationRequest authorizationRequest,
		Authentication userAuthentication) {
	Map<String, Object> model = new HashMap<String, Object>();
	// In case of a redirect we might want the request parameters to be included
	model.putAll(authorizationRequest.getRequestParameters());
	return model;
}

后面其他方法就不再列举。

TokenStoreUserApprovalHandler 注释 A user approval handler that remembers approval decisions by consulting existing tokens.(用户审批处理程序,通过咨询现有令牌记住审批决策。),目的应该是在生成 token 给客户端后就没办法修改授权内容了,但是这里却在生成 token 时使用了这个类,此时 token 还未生成,还在生成 code 阶段,所以是可以修改授权内容的,其实这部分逻辑可以做改动并不会影响最终结果的。

4. 解决

​ 简单改动,自定义 UserApprovalHandler 继承 TokenStoreUserApprovalHandler 类,将 ApprovalStoreUserApprovalHandlergetUserApprovalRequestupdateAfterApprovalisApproved 三个方法拷贝出来简单改动,然后进行配置。

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    endpoints
        .userApprovalHandler(myTokenStoreUserApprovalHandler);
}

注意:userApprovalHandler 需要手动配置里面的 tokenStoreclientDetailsService

没有详细代码实现,目前不再准备进行使用 JWTtoken,如果要实现提前失效 JWT ,最终还是需要把部分验证信息存储在服务器上,把整个 token 放在 Redis 也许会是更合适的选择。