dromara/Sa-Token · error · SaOAuth2ClientModelScopeException
30112
30112
Error message
该 client 暂未签约 scope:
What it means
Thrown by SaOAuth2Template.checkContractScope: the client is valid but one of the requested scopes is not in its contractScopes list. An empty/absent scope list passes silently; any non-empty scope must be contracted. Thrown as SaOAuth2ClientModelScopeException with clientId and the offending scope. Error code 30112.
Source
Thrown at sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java:134
* @return /
*/
public SaClientModel checkContractScope(String clientId, List<String> scopes) {
return checkContractScope(checkClientModel(clientId), scopes);
}
/**
* 校验:该 Client 是否签约了指定的 Scope,如果没有则抛出异常
* @param cm 应用
* @param scopes 权限列表
* @return /
*/
public SaClientModel checkContractScope(SaClientModel cm, List<String> scopes) {
if(SaFoxUtil.isEmptyList(scopes)) {
return cm;
}
for (String scope : scopes) {
if(! cm.contractScopes.contains(scope)) {
throw new SaOAuth2ClientModelScopeException("该 client 暂未签约 scope: " + scope)
.setClientId(cm.clientId)
.setScope(scope)
.setCode(SaOAuth2ErrorCode.CODE_30112);
}
}
return cm;
}
// --------- redirect_uri 相关
/**
* 校验:该 Client 使用指定 url 作为回调地址,是否合法
* @param clientId 应用id
* @param url 指定url
*/
public void checkRedirectUri(String clientId, String url) {
// 1、是否是一个有效的url
if( ! SaFoxUtil.isUrl(url)) {View on GitHub (pinned to ac2c7f6e94)
Solutions
- Add the missing scope to the client's contractScopes registration (setContractScopes)
- Or trim the requested scope down to what the client has contracted
- Keep scope names in a single shared constant list so client code and registration cannot drift
Example fix
// before
new SaClientModel().setClientId("1001")
.setContractScopes(Collections.singletonList("getuserinfo"));
// request: scope=getuserinfo,orders -> throws 30112
// after
new SaClientModel().setClientId("1001")
.setContractScopes(Arrays.asList("getuserinfo", "orders")); Defensive patterns
Strategy: validation
Validate before calling
SaClientModel cm = oauth2Template.checkClientModel(clientId);
for(String s : requestedScopes) {
if(!cm.getContractScopes().contains(s)) {
throw new IllegalArgumentException("scope not contracted: " + s);
}
} Try / catch
catch(SaOAuth2ClientModelScopeException e) {
if("30112".equals(e.getCode())) return badRequest("scope not allowed: " + e.getScope());
} Prevention
- Define scopes as shared constants used by both the client app and the SaClientModel registration
- Add a startup check that each client's requested scopes ⊆ contractScopes
When it happens
Trigger: Authorize or token request with scope=getuserinfo,orders while the SaClientModel only contracts [getuserinfo]; scope string with a stray value (typo, extra separator producing an empty/odd token); frontend requests an ever-growing scope list after new features ship.
Common situations: New API scope added to the client app but the server-side client registration was not updated; different environments register different contractScopes; scope string parsing splits on unexpected separators.
Related errors
AI-assisted analysis of dromara/Sa-Token@ac2c7f6e94 (2026-08-14).
Data as JSON: /api/errors/d25c3a2f072efacd.
Report an issue: GitHub.