{"record":{"id":"d25c3a2f072efacd","repo":"dromara/Sa-Token","slug":"30112","errorCode":"30112","errorMessage":"该 client 暂未签约 scope: ","messagePattern":"该 client 暂未签约 scope: ","errorType":"exception","errorClass":"SaOAuth2ClientModelScopeException","httpStatus":null,"severity":"error","filePath":"sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java","lineNumber":134,"sourceCode":"\t * @return /\n\t */\n\tpublic SaClientModel checkContractScope(String clientId, List<String> scopes) {\n\t\treturn checkContractScope(checkClientModel(clientId), scopes);\n\t}\n\n\t/**\n\t * 校验：该 Client 是否签约了指定的 Scope，如果没有则抛出异常\n\t * @param cm 应用\n\t * @param scopes 权限列表\n\t * @return /\n\t */\n\tpublic SaClientModel checkContractScope(SaClientModel cm, List<String> scopes) {\n\t\tif(SaFoxUtil.isEmptyList(scopes)) {\n\t\t\treturn cm;\n\t\t}\n\t\tfor (String scope : scopes) {\n\t\t\tif(! cm.contractScopes.contains(scope)) {\n\t\t\t\tthrow new SaOAuth2ClientModelScopeException(\"该 client 暂未签约 scope: \" + scope)\n\t\t\t\t\t\t.setClientId(cm.clientId)\n\t\t\t\t\t\t.setScope(scope)\n\t\t\t\t\t\t.setCode(SaOAuth2ErrorCode.CODE_30112);\n\t\t\t}\n\t\t}\n\t\treturn cm;\n\t}\n\n\t// --------- redirect_uri 相关\n\n\t/**\n\t * 校验：该 Client 使用指定 url 作为回调地址，是否合法\n\t * @param clientId 应用id\n\t * @param url 指定url\n\t */\n\tpublic void checkRedirectUri(String clientId, String url) {\n\t\t// 1、是否是一个有效的url\n\t\tif( ! SaFoxUtil.isUrl(url)) {","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/dromara/Sa-Token/blob/ac2c7f6e94a78573cf0bcb932dd8b04e68fad189/sa-token-plugin/sa-token-oauth2/src/main/java/cn/dev33/satoken/oauth2/template/SaOAuth2Template.java#L116-L152","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nnew SaClientModel().setClientId(\"1001\")\n    .setContractScopes(Collections.singletonList(\"getuserinfo\"));\n// request: scope=getuserinfo,orders  -> throws 30112\n\n// after\nnew SaClientModel().setClientId(\"1001\")\n    .setContractScopes(Arrays.asList(\"getuserinfo\", \"orders\"));","handlingStrategy":"validation","validationCode":"SaClientModel cm = oauth2Template.checkClientModel(clientId);\nfor(String s : requestedScopes) {\n    if(!cm.getContractScopes().contains(s)) {\n        throw new IllegalArgumentException(\"scope not contracted: \" + s);\n    }\n}","typeGuard":null,"tryCatchPattern":"catch(SaOAuth2ClientModelScopeException e) {\n    if(\"30112\".equals(e.getCode())) return badRequest(\"scope not allowed: \" + e.getScope());\n}","preventionTips":["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"],"tags":["oauth2","scope","client-registration","sa-token"],"backgroundTag":null,"analyzedSha":"ac2c7f6e94a78573cf0bcb932dd8b04e68fad189","analyzedAt":"2026-08-14T14:36:10.271Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}