Kotlin 实战:Spring Boot 接入 Moltbook 身份,并用 karma/claimed 做风控门禁

Kotlin 实战:Spring Boot 接入 Moltbook 身份,并用 karma/claimed 做风控门禁

“让 bot 登录”这件事,一旦你真的开始做,就会发现它和人类登录不太一样:你不仅要知道对方是谁,还得决定给它多大权限。

Moltbook 的 verify 接口会返回很多有用字段,比如:

  • is_claimed:这个 bot 是否被某个人类账号认领(至少可追责)
  • karma:声誉分(不完美,但能当信号)
  • owner:背后的人类账号信息

这篇用 Spring Boot + Kotlin 做一个后端接入示例,然后把 claimed + karma 变成访问控制规则:不靠“写在文档里”,而是写进 Security 配置里。

你要实现的目标

  1. 从请求头拿到 X-Moltbook-Identity(bot 的短期 identity token)
  2. 服务端调用 Moltbook verify-identity 验证 token
  3. 把验证结果放进 SecurityContext
  4. is_claimedkarma 做分级授权(例如:写接口要求 claimed 且 karma ≥ 100)

数据结构(Kotlin data class)

data class MoltbookOwner(
    val xHandle: String?,
    val xVerified: Boolean?
)

data class MoltbookAgent(
    val id: String,
    val name: String,
    val karma: Int,
    val isClaimed: Boolean,
    val owner: MoltbookOwner?
)

实际返回字段更多,你可以按需扩展。建议解析 JSON 时忽略未知字段,避免接口加字段就把你打挂。

1) 写一个 verifier:只干“验 token”这件事

WebClient 调 verify 接口。要点就两个:固定 host 为 www.moltbook.com,以及别把 token 打进日志。

import org.springframework.http.MediaType
import org.springframework.web.reactive.function.client.WebClient

class MoltbookVerifier(
    private val webClient: WebClient,
    private val appKey: String,
    private val audience: String?
) {
    data class VerifyRequest(val token: String, val audience: String?)
    data class VerifyResponse(val valid: Boolean?, val agent: Map<String, Any>?)

    fun verify(identityToken: String): MoltbookAgent? {
        val resp = webClient
            .post()
            .uri("https://www.moltbook.com/api/v1/agents/verify-identity")
            .header("X-Moltbook-App-Key", appKey)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(VerifyRequest(token = identityToken, audience = audience))
            .retrieve()
            .bodyToMono(VerifyResponse::class.java)
            .block() ?: return null

        if (resp.valid != true) return null

        val agent = resp.agent ?: return null
        // 这里为了简洁用 Map 转换;生产建议用强类型 JSON 映射
        val owner = agent["owner"] as? Map<*, *>
        return MoltbookAgent(
            id = agent["id"].toString(),
            name = agent["name"].toString(),
            karma = (agent["karma"] as? Number)?.toInt() ?: 0,
            isClaimed = agent["is_claimed"] as? Boolean ?: false,
            owner = MoltbookOwner(
                xHandle = owner?.get("x_handle") as? String,
                xVerified = owner?.get("x_verified") as? Boolean
            )
        )
    }
}

这段代码的“丑”是有意的:它只展示流程。你完全可以换成 kotlinx.serialization 或 Jackson 的强类型映射,让解析更干净。

2) 写一个 Filter:把 agent 塞进 SecurityContext

import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
import org.springframework.security.authentication.AbstractAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.OncePerRequestFilter

class MoltbookAuthenticationToken(
    val agent: MoltbookAgent
) : AbstractAuthenticationToken(emptyList()) {
    override fun getCredentials(): Any = ""
    override fun getPrincipal(): Any = agent
}

class MoltbookAuthFilter(
    private val verifier: MoltbookVerifier,
    private val headerName: String = "X-Moltbook-Identity"
) : OncePerRequestFilter() {

    override fun doFilterInternal(
        request: HttpServletRequest,
        response: HttpServletResponse,
        filterChain: FilterChain
    ) {
        val token = request.getHeader(headerName)?.trim().orEmpty()
        if (token.isNotBlank()) {
            val agent = verifier.verify(token)
            if (agent != null) {
                val auth = MoltbookAuthenticationToken(agent).apply { isAuthenticated = true }
                SecurityContextHolder.getContext().authentication = auth
            }
        }
        filterChain.doFilter(request, response)
    }
}

这里的策略是“有 token 就尝试认证,没有就当匿名”。你也可以在 filter 里直接拦截未认证请求,但那会让公共接口更难写。更推荐把“哪些接口必须登录”交给 Spring Security 的授权规则。

3) 用 karma + claimed 做访问控制

下面给一个简单的 AuthorizationManager:要求 is_claimed=true 且 karma ≥ 某个阈值。

import org.springframework.security.authorization.AuthorizationDecision
import org.springframework.security.authorization.AuthorizationManager
import org.springframework.security.web.access.intercept.RequestAuthorizationContext
import java.util.function.Supplier
import org.springframework.security.core.Authentication

class ClaimedKarmaAccess(private val minKarma: Int) : AuthorizationManager<RequestAuthorizationContext> {
    override fun check(
        authentication: Supplier<Authentication>,
        context: RequestAuthorizationContext
    ): AuthorizationDecision {
        val auth = authentication.get()
        val agent = (auth.principal as? MoltbookAgent) ?: return AuthorizationDecision(false)
        val ok = agent.isClaimed && agent.karma >= minKarma
        return AuthorizationDecision(ok)
    }
}

SecurityFilterChain 里挂上去:

import org.springframework.context.annotation.Bean
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.SecurityFilterChain

@Bean
fun securityFilterChain(http: HttpSecurity, filter: MoltbookAuthFilter): SecurityFilterChain {
    http
        .csrf { it.disable() }
        .addFilterBefore(filter, org.springframework.security.web.authentication.AnonymousAuthenticationFilter::class.java)
        .authorizeHttpRequests {
            it.requestMatchers("/api/write/**").access(ClaimedKarmaAccess(minKarma = 100))
            it.requestMatchers("/api/admin/**").access(ClaimedKarmaAccess(minKarma = 1000))
            it.anyRequest().permitAll()
        }
    return http.build()
}

这套规则的好处是:你不用在每个 controller 里手写 if/else。权限策略集中在一处,改动可审计。

写在最后

karma 不完美,claimed 也不证明“真自治”。但它们能让你在早期就把风险分层,而不是一股脑把“写接口、交易接口、外呼接口”全开放给任何带 token 的请求。

我更愿意把这套思路理解成“默认谨慎”:先让 bot 进门,再按信号决定它能去哪几间房。

参考链接

  • Moltbook 开发者页面(verify-identity 接口说明):https://www.moltbook.com/developers
  • Moltbook 集成指南(audience 字段与错误码解释):https://moltbook.com/developers.md
← 返回博客列表