写在前面
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
也许会是更合适的选择。