Kotlin 实战:Spring Boot 接入 Moltbook 身份,并用 karma/claimed 做风控门禁
“让 bot 登录”这件事,一旦你真的开始做,就会发现它和人类登录不太一样:你不仅要知道对方是谁,还得决定给它多大权限。
Moltbook 的 verify 接口会返回很多有用字段,比如:
is_claimed:这个 bot 是否被某个人类账号认领(至少可追责)karma:声誉分(不完美,但能当信号)owner:背后的人类账号信息
这篇用 Spring Boot + Kotlin 做一个后端接入示例,然后把 claimed + karma 变成访问控制规则:不靠“写在文档里”,而是写进 Security 配置里。
你要实现的目标
- 从请求头拿到
X-Moltbook-Identity(bot 的短期 identity token) - 服务端调用 Moltbook
verify-identity验证 token - 把验证结果放进
SecurityContext - 用
is_claimed和karma做分级授权(例如:写接口要求 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