dromara/Sa-Token · error · SaSsoException

CODE_30011

CODE_30011

Error message

该 ticket 不属于 client={client}, ticket 值: {ticket}

What it means

Thrown during ticket validation when the client identifier presented at check time differs from the client that the ticket was created for (unless the wildcard is used). This binds each one-time ticket to the client app that triggered it, preventing a ticket issued for app A from being redeemed by app B.

Source

Thrown at sa-token-plugin/sa-token-sso/src/main/java/cn/dev33/satoken/sso/template/SaSsoServerTemplate.java:213

     * 校验 Ticket,无效 ticket 会抛出异常,如果此ticket是有效的,则立即删除
     *
     * @param ticket Ticket码
     * @param client client 标识
     * @return /
     */
    public TicketModel checkTicketParamAndDelete(String ticket, String client) {
        TicketModel ticketModel = checkTicket(ticket);

        // 校验 client 参数是否正确,即:创建 ticket 的 client 和当前校验 ticket 的 client 是否一致
        String ticketClient = ticketModel.getClient();
        if(SaSsoConsts.CLIENT_WILDCARD.equals(client)) {
            // 如果提供的是通配符,直接越过 client 校验
        } else if (SaFoxUtil.isEmpty(client) && SaFoxUtil.isEmpty(ticketClient)) {
            // 如果提供的和期望的两者均为空,则通过校验
        } else {
            // 开始详细比对
            if(SaFoxUtil.notEquals(client, ticketClient)) {
                throw new SaSsoException("该 ticket 不属于 client=" + client + ", ticket 值: " + ticket).setCode(SaSsoErrorCode.CODE_30011);
            }
        }

        // 删除 ticket 信息,使其只有一次性有效
        deleteTicket(ticket);
        deleteTicketIndex(client, ticketModel.getLoginId());

        //
        return ticketModel;
    }

    // ticket 索引

    /**
     * 保存 Ticket 索引 (id 反查 ticket)
     *
     * @param client 应用端
     * @param ticket ticket码

View on GitHub (pinned to ac2c7f6e94)

Solutions

  1. Make the same client identifier flow through both steps: the redirect into /sso/auth and the callback that validates the ticket
  2. Check the client param name mapping (paramName.client) is not being rewritten by a proxy between the two requests
  3. If you genuinely need multi-client ticket redemption, pass SaSsoConsts.CLIENT_WILDCARD to bypass the check (understand the security implications first)

Example fix

// before
ssoServerTemplate.checkTicketParamAndDelete(ticket, requestClient); // CODE_30011 when mismatch

// after — ensure the auth redirect carried this client's id
String authUrl = ssoServerTemplate.buildAuthUrl("http://client.com/sso/login", "client-1");
// ... on callback, validate with the same id
ssoServerTemplate.checkTicketParamAndDelete(ticket, "client-1");
Defensive patterns

Strategy: validation

Validate before calling

String expected = myClientId; // same id sent to /sso/auth
if(!expected.equals(req.getParam(paramName.client))) {
    // fix the parameter before calling checkTicketParamAndDelete
}

Try / catch

try { ssoServerTemplate.checkTicketParamAndDelete(ticket, client); } catch (SaSsoException e) { if(SaSsoErrorCode.CODE_30011 == e.getCode()) { /* reject callback, restart SSO flow */ } }

Prevention

When it happens

Trigger: checkTicketParamAndDelete(ticket, client) is called with a client value that does not string-equal ticketModel.getClient() — e.g. the client app sends a different/missing client parameter at /sso/login than it sent at /sso/auth.

Common situations: Two client apps share one callback domain and the ticket gets intercepted by the other app; the client's configured allow-client / client identifier changed between auth and callback; one client deliberately reuses a ticket meant for another (blocked by design).

Related errors


AI-assisted analysis of dromara/Sa-Token@ac2c7f6e94 (2026-08-14). Data as JSON: /api/errors/ef2b2048e4704796. Report an issue: GitHub.