写在前面
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 之后一切正常,但是发现无法指定授权范围了,我这里请求了 read 和 info 两个部分内容,前端相应的界面如下,没有了可选的授权类型选项,只有授权和拒绝。

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

如果要使用 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方法)会调用 updateAfterApproval 和 isApproved。
查看这三个方法的实现逻辑,可以看到 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 类,将 ApprovalStoreUserApprovalHandler 的 getUserApprovalRequest、updateAfterApproval 、isApproved 三个方法拷贝出来简单改动,然后进行配置。
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.userApprovalHandler(myTokenStoreUserApprovalHandler);
}
注意:userApprovalHandler 需要手动配置里面的 tokenStore 和 clientDetailsService。
没有详细代码实现,目前不再准备进行使用 JWT 做 token,如果要实现提前失效 JWT ,最终还是需要把部分验证信息存储在服务器上,把整个 token 放在 Redis 也许会是更合适的选择。