Share
Embed
Server-side Kotlin in LINE Messaging API
LINEでは、外部企業向けのLINE公式アカウントを公開しています。LINEのシステムと外部のシステムを繋げて大量のリクエストを処理しており、Messaging APIのサーバー開発ではシステムの改善に日々取り組んでいます。
LINEのMessaging APIは基本的にはBotを作るAPIですが、Botからユーザーへの一斉配信や個別配信をするAPI、ユーザーからメッセージが送信されたり、友だち追加やアクションが来た際にサーバーにWebhookを送信するような機能、リッチメニューや、ユーザーの情報を取得したり、メッセージ送信を管理するAPIなどを提供しています。
川田 裕貴 / LINE株式会社 LINE Platform Developmentセンター1 B Part チーム
2014年に開発インターンシップ参加、2016年に新卒入社後LINEスタンプ・着せかえ・絵文字のサーバーサイド開発を経て、2019年から現在までLINE Messaging APIやBot Platformのバックエンド開発に関わる。Webhook配送システムやシステム更新作業を担当。趣味はクルマいじりや電子工作。 Twitter: https://twitter.com/hktechno
※以下イベントで発表した資料です
https://line.connpass.com/event/246551/
LINE Developers
May 25, 2022
Transcript
34 slides
-
Server-side Kotlin in LINE Messaging API LINE Platform Development Center
1 / Bot Platform Development 室 / B Part チーム Hirotaka Kawata 2022.05.25 -
ࣗݾհ ా ༟و )JSPUBLB ,BXBUB !ILUFDIOP • ։ൃΠϯλʔϯγοϓ
• -*/&)#BTFετϨʔδͷཧπʔϧ • ৽ଔೖࣾ -*/&4IPQνʔϜ • -*/&ελϯϓɾֆจࣈɾண͔ͤ͑ͷαʔόʔ։ൃ • -*/&5IJOHTͷ։ൃʹࢀՃ • #MVFUPPUI-&Λͬͨ *P5αʔϏε͚ "1* • αʔόʔɾϋʔυΣΞɾγεςϜઃܭ৭ʑ • dݱࡏ -*/&.FTTBHJOH"1*αʔόʔ։ൃ • .FTTBHJOH"1*ͷαʔόʔαΠυ։ൃ • 8FCIPPLૹ৴ίϯϙʔωϯτͷஔ͑ • झຯं͍͡ΓɾϨΠϠʔɾ$16ΞʔΩςΫνϟ -
.FTTBHJOH"1* #PUˠ 6TFSҰ੪৴ ϒϩʔυΩϟετɾφϩʔΩϟετ #PUˠ 6TFSݸผ৴ ϓογϡɾϦϓϥΠ 6TFSˠ #PU 8FCIPPLૹ৴
ͦͷଞ ϦονϝχϡʔɾϢʔβʔใ "1* ϝοηʔδૹ৴ूܭ ʑʜ -*/&.FTTBHJOH"1*͕ఏڙ͢Δ "1* -
.FTTBHJOH"1*ͷόοΫΤϯυ talk-server Messaging API Webhook Messaging API Push, Reply …
https://api.line.me/v2/bot MJOFCPUCBDLFOE Chatbot server -*/&ͷ UBMLTFSWFSͱ .FTTBHJOH"1* CPU ͷڮ͠ ϝοηʔδड৴ ༑ͩͪՃ ϝοηʔδૹ৴ -
8FCIPPL&OEQPJOU #PUαʔόʔ 8FCIPPL ૹ৴αʔόʔ #PUΠϕϯτ ϑΟϧλʔ ඵؒສΠϕϯτҎ্ #PUʹؔ͢ΔΠϕϯτΛநग़ #PU8FCIPPLૹ৴ଆ
αʔόʔߏ Πϕϯτॲཧ αʔόʔ $3. νϟοτ ετϨʔδ UBMLTFSWFS Ϣʔβʔ #PUνϟϯωϧ ཧ "1* άϧʔϓཧ ඵؒສΠϕϯτҎ্ Πϕϯτͷॱংɾ߹ੑΛอূ 8FCIPPLૹ৴ઌ͕ͲΜͳʹͯ͘ ӨڹΛड͚ͣʹ҆ఆՔಇɾ࠶ૹػೳ ʹେ෯৽ ,PUMJOΛੵۃ࠾༻ 8FCIPPL $PSPVUJOFT -
UBMLTFSWFS "1*(BUFXBZ ϦΫΤετ ඵؒສҎ্ #PUϝοηʔδ৴ଆ αʔόʔߏ ৴ॲཧαʔόʔ #PUνϟϯωϧ ཧ
"1* ԯҎ্ͷϝοηʔδॲཧ ઍສ୯ҐͷϢʔβʔҰ੪৴ ༏ઌ͖ͷ৴Ωϡʔ $3. νϟοτ ετϨʔδ ૹ৴ཧ ϚΠΫϩαʔϏεΞʔΩςΫνϟ ͦΕͦΕ͕ଟͷϦΫΤετΛॲཧ Ϣʔβʔ ৴ 2VFVF ϝοηʔδૹ৴ αʔόʔ -
UBMLTFSWFS "1*(BUFXBZ #PUϝοηʔδ৴ଆ αʔόʔߏ ৴ॲཧαʔόʔ #PUνϟϯωϧ ཧ "1* $3.
νϟοτ ετϨʔδ ૹ৴ཧ Ϣʔβʔ ৴ 2VFVF ,PUMJO ͷҠߦΛܭըத ,BGLB %FDBUPO 3FBDUPS ϝοηʔδૹ৴ αʔόʔ -
line-bot-backend (monorepo) 3FEJT 0CKFDU 4UPSBHF .POJUPSJOH 6UJMJUJFT ,BGLB %FDBUPO "1*DMJFOUT
$BDIF .JDSPTFSWJDFT.POPSFQP .FTTBHJOH"1*CBDLFOEͷαʔϏεߏ "1* (BUFXBZ .FTTBHF "1*4FSWFS 2VFVF 1SPDFTTPS 8FCIPPL 1SPDFTTPS 8FCIPPL %JTQBUDIFS νʔϜڞ௨ϥΠϒϥϦ DPNNPOT TUPSBHF NPEFMT CVJMEHSBEMFLUT 4QPUMFTT LUMJOU -
+BWBͱ ,PUMJOͱ 4DBMB +BWBͱͷ ੑ ݴޠͷػೳ ֶशίετ +BWBҎ߱ਐԽ ·ͩ·ͩ͜Ε͔Β
ࣾڞ௨ݴޠ ͕͕ͩ͠͞গͳ͍ +BWB͔ΒؾܰʹݺΔ ΄ͱΜͲࠔΒͳ͍ ҉తͳܕม /VMMBCMF %BUBDMBTT XIFOCMPDL $PSPVUJOFT +BWBΛͬͨ͜ͱ͕ ͋Εײత Kotlin Java +BWB͔Β DBMM͕ݫ͍͠ ಛ༗ͷίϨΫγϣϯܕ BT4DBMB BT+BWB ಛ༗ͳ 0QUJPO ڧྗͳ 'VODUJPOBM 1SPHSBNNJOHαϙʔτ ͱ͖ͬͭʹ͍͘ߏจ ҉తɺॳݟͰಡΊͳ͍ *NQMJDJU Scala ஸ͍͍ -
ඇಉظॲཧͷ ॻ͖͢͞ ಋೖͷखܰ͞ ετϦʔϜॲཧ 3FBDUJWF4USFBNT ͷֶशίετ͕ߴ͍ ରԠϥΠϒϥϦ͕ଟ͍ 4QSJOHͷαϙʔτ ετϦʔϜॲཧʹಛԽ ॆ࣮ͨ͠
0QFSBUPS ײత ݴޠػೳͷҰ෦ TVTQFOEGVO BXBJU +BWB'VUVSF 3Yͱͷ ίωΫλʔ͕ॆ࣮ 'MPXͷ 0QFSBUPS͕ශऑ ୯ൃ࣮ߦͷඇಉظԽ͚ ඇಉظॲཧ $PSPVUJOFT3FBDUPS Coroutines Reactor -
ྑ͍ͱ͜Ζ • +7.ͰࠔΔ͜ͱ͕ຆͲͳ͍ • +BWBͱͷੑ͕࠷ߴ • 4DBMBͱൺͯ֨ஈʹྑ͍ • ͔Ώ͍ͱ͜Ζʹख͕ಧ͘ •
/VMMBCMF TNBSUDBTU • ֦ு͕ؔΑ͍ • ಛʹ *%&ͱͷੑ • ͪΐͬͱॏ͍͚Ͳʜ ؾʹͳΔͱ͜Ζ • ύλʔϯϚονϯά͕ऑ͍ • $BTFDMBTT XIFOʜ • GMBU.BQ ͕ݴޠͱͯ͠΄͍͠ • 4DBMBͷ GPS • &YQFSJNFOUBM͕ຊʹ࣮ݧత • ίϯύΠϧʹ͕͔͔࣌ؒΔ • LBQU ͕ಛʹॏ͍ • *%&ิॏ͍ جຊతʹ +BWB͔ΒΓ͑ͯࠔΔ͜ͱຆͲͳ͍ ,PUMJOͷॻ͖৺ -
talk-server ༑ͩͪՃ ϝοηʔδૹ৴ MJOFCPUCBDLFOE Chatbot server ϝοηʔδड৴ αʔϏε୯ମͷςετͰ ෳ߹తͳཁҼͰյΕΔ߹ &OEUP&OEͰ
.FTTBHJOH"1*ͷςετΛߦ͍͍ͨ .FTTBHJOH"1*ͷςετڥ େنͳมߋΛ࠷খݶͷϦεΫͰϦϦʔε͢ΔͨΊʹɾϚΠΫϩαʔϏεͳΒͰͷΈ -
.FTTBHJOH"1*ͷςετڥ #PU։ൃऀ͚ ςετ "1*ͷ 3FRVFTU3FTQPOTFʹมߋ͕ͳ͍͔ .FTTBHJOH"1*ࣗମͷςετɺ8FCIPPLͷड৴ςετ -*/&Ϣʔβʔ ͚ςετ ਖ਼͍͠Ϣʔβʔʹϝοηʔδ͕ૹ৴͞ΕΔ͔ɾ༰ਖ਼͍͔͠ -*/&ϢʔβʔΛٖࣅతʹ࠶ݱͯ͠ςετ
UBMLTFSWFS෦ "1*ར༻ ෦తͳςετ ਖ਼͘͠ૹ৴͕ܭ্͞ΕΔ͔ɾ#PUͷઃఆ͕ਖ਼͘͠ө͞ΕΔ͔ .FTTBHJOH"1*෦ͷ *OUFHSBUJPO5FTU "1*ϓϥοτϑΥʔϜͷςετʹٻΊΒΕΔͷ -
1VTI3FQMZ ༑ͩͪՃ ˠϝοηʔδૹ৴ ˠ 8FCIPPLड৴ ˠ#PUฦ৴ #SPBEDBTU Ϣʔβʔ࡞ ˠ ༑ͩͪՃ
ˠϒϩʔυΩϟετ /BSSPXDBTU ಛఆͷଐੑΛ࣋ͭϢʔβʔ࡞ ˠφϩʔΩϟετ 8FCIPPL ༷ʑͳλΠϓͷ 8FCIPPLΛݕূ ಈըɾը૾ɾԻ 3JDI.FOV Ϧονϝχϡʔͷηοτ ˠө֬ೝ ˠVTFS*E ࢦఆͰηοτ (SPVQ3PPN άϧʔϓͷೖୀࣨɾ#PUͷάϧʔϓڐՄͷಈ࡞ɾෳ #PU ରԠ &UD ૯ςετ߲ Ҏ্ ݄ݱࡏ ֎෦͚ .FTTBHJOH"1* ͷಈ࡞͔Βɺૹ৴ཧɾ෦ঢ়ଶɾ݅ذΛཏతʹ .FTTBHJOH"1*ςετέʔεͷྫ -
3FMFBTF1IBTF͝ͱʹςετΛߦ͏ .FTTBHJOH"1*ςετͷର #&5" 3&-&"4& ͦΕͧΕͷ %FQMPZޙʹςετ جຊػೳͷಈ࡞Λ۱ʑ·Ͱࣗಈݕূ ։ൃڥ SVOTEBZ ຊ൪ڥ
ݱࡏະରԠ 45"(*/( εςʔδϯάڥ 3$ SVOEBZ ࣗ৴Λ࣋ͬͯຊ൪ڥ 8FCIPPL৽࣌ʹཱͬͨ -
Ϣʔβʔϝοηʔδड৴ ༰ݕূ #PUUP6TFS ϝοηʔδ৴ .FTTBHJOH"1* Ϣʔβʔ࡞ UBMLTFSWFS .FTTBHJOH"1*ςετͷྲྀΕ -*/&ެࣜΞΧϯτ ༑ୡొ
-*/&෦ "1* 6TFSUP#PU ϝοηʔδૹ৴ -*/&෦ "1* 8FCIPPLड৴ ༰ݕূ "QQUZQFWFSTJPO ϓϥΠόγʔಉҙͷ༗ແ ϦϓϥΠϝοηʔδ 3FQMZ5PLFO ༗ޮੑݕূ #PUબɾ࡞ ෦ "1* (SPVQڐՄ "VUIPSJUZͷ༗ແ ૹ৴ݕূ Ϣʔβʔআ ඇಉظతͳ݁ՌΛݕূ͢Δςετέʔε͕ଟ͍ -
4DBMB5FTU ͷॻ͖৺Λ ,PUMJO Ͱ࠶ݱͨ͠ςετϑϨʔϜϫʔΫ LPUFTU LPUFTUJP ॊೈͳ 4QFD 4USJOH4QFD
'VO4QFD 4IPVME4QFD ͳͲଟͷ 4QFD͕༻ҙ͞Ε͍ͯΔ ্खʹ͏ͱ5FTUDBTF͕ߏԽ͍͢͠ɾ໊લʹΑͬͯςετ͕ཧ͍͢͠ ڧྗͳ "TTFSUJPO ,PUMJOOBUJWFͳ "TTFSTUJPO %4-తͳه๏ʹରԠ /VMMBCMFରԠ &YUFOTJPOGVODUJPO͕ଟ༻ҙ͞Ε͍ͯͯॊೈʹॻ͚Δ $PSPVUJOFT "TZOD ରԠ 5FTUNFUIPE͕ͯ͢ TVTQFOEGVODUJPOʹͳ͍ͬͯΔ "XBJUJMJUZ ͷΑ͏ͳҰఆ࣌ؒʹ݅Λຬͨ͢͜ͱΛ֬ೝ͢Δඇಉظςετ͕ॻ͖͍͢ -
ςετͷཧ ඇಉظରԠ "TTFSUJPOT ॊೈͳ 4QFD ߏԽ͞Εͨςετ $PSPVUJOFTରԠ FWFOUVBMMZ DPOUJOVBMMZ $PODVSSFOUUFTUJOH
,PUMJOOBUJWFͳ GMVFOUBTTFSUJPO 5FTU໊͕Θ͔Γʹ͍͘ ςετΛߏԽ͠ʹ͍͘ SVO#MPDLJOH5FTU ඇಉظςετ͍͠ +BWBͷ BTTFSUMJCSBSZ ,PUMJOαϙʔτ͕ෆશ /VMMBCMF +6OJUͱͷൺֱ JUnit 5 kotest -
࣮ࡍʹΘΕ͍ͯΔςετίʔυΛҰ෦ൈਮ LPUFTU ͷαϯϓϧίʔυ class PushTest( … ) : ShouldSpec({ context("/v2/bot/message/push")
{ should("not push to unrelated user").config(enabledIf = betaOnly) { val text = randomMessageText() val msg = PushMessage(userId, TextMessage(text)) val response = channel.messagingClient.push(msg).awaitResponse() .shouldHaveCode(200) … } } }) BXBJU3FTQPOTF TVTQFOEGVO TIPVME)BWF$PEF ֦ுؔ QVTI ͷฦΓ SFUSPGJU3FTQPOTF DPOUFYU ͰςετΛߏԽ ςετ݁Ռͷ໊લ͕Θ͔Γ͍͢ -
$PSPVUJOFTΛͬͨඇಉظ BTTFSUJPO FWFOUVBMMZ ͱ DPOUJOVBMMZ LPUFTU ͷαϯϓϧ eventually(1.minutes) { user.fetchReceivedMessages(channel.primaryBotMid)
.forOne { it.message.text.shouldContain(text) // should have proper metadata it.message.contentMetadata.shouldContainExactly(mapOf(…)) } } user.follow(channel.primaryBotMid) continually(20.seconds) { channel.events(seqNo) .filterIsInstance<FollowEvent>() .forNone { it.source.userId.shouldBe(channel.encrypt(user.mid)) } } ಛఆͷϝοηʔδ͕Ҏʹ͚݅ͩड৴͞ΕΔ ༑ͩͪՃͯ͠ GPMMPXΠϕϯτ͕ඵಧ͔ͳ͍ -
$MVFT ͱ֦ுؔΛͬͨ BTTFSUJPOr SFUSPGJUͷΧελϜ BTTFSUJPOͷྫ LPUFTU ͷαϯϓϧ fun <T> retrofit2.Response<T>.shouldHaveCode(expectedCode:
Int): retrofit2.Response<T> { asClue { val body = lazy { if (isSuccessful) body() else errorBody()?.string() } withClue(body) { code().shouldBe(expectedCode) } } return this } val msg = PushMessage(userId, TextMessage(text)) val response = channel.messagingClient.push(msg).awaitResponse() .shouldHaveCode(200) org.opentest4j.AssertionFailedError: Response{protocol=h2, code=400, message=, url=https://api.line-beta.me/v2/bot/message/push} {"message":"Failed to send messages"} expected:<200> but was:<400> at com.linecorp.bot.test.common.Response.shouldHaveCode(Response.kt:30) $MVFΛઃఆ 3FTQPOTFͱ CPEZ Τϥʔ࣌ʹ $MVFʹઃఆͨ͠༰Λग़ྗ -
ͦͷଞศརͳػೳ LPUFTU ͷαϯϓϧ should("not push to blocked user") { val
user = autoClose(userAccountManager.get()) … } data class UserAccount(…) : AutoCloseable { override fun close() = runBlocking { unregister() } } val contentId = retry(5, 120.seconds, 10.seconds, 1) { client.uploadMessageContent(content) } channel.messagingClient.uploadUserIdList(content) .awaitResponse() .shouldHaveCode(400) .shouldHaveErrorBody { it.string().shouldMatchJson( """{"message":"The request body is missing"}""" ) } BVUP$MPTF 4QFDऴྃ࣌ʹ DMPTF SFUSZ +40/NBUDIFS -
&OEUP&OE5FTUJOH • ࣦഊ͕ଟ͍ ಛʹ#&5" • ݅ΛͬͱϥϯμϜʹ͍ͨ͠ • 1SPQFSUZCBTFEUFTUJOH • ࣮ߦ͕࣌ؒΊ
ఔ • LPUFTU ͰฒྻԽ • 4FOUSZͷ 5SBDJOHͰ͍ ςετΛࢹ LPUFTU • ࠷৽൛ͷ ,PUMJO͕ඞਢ • &YQFSJNFOUBMػೳΛͨ͘͞Μ͏ • ͨ·ʹόάͬͯΔ • ฒྻςετͱ͔ • .PDLJUP͕ͪΐͬͱ͍ʹ͍͘ • .PDLL Λ͑ྑ͍͕ .FTTBHJOH"1*ςετ ࠓޙͷ՝ͱؾʹͳΔ -
4QSJOH4FDVSJUZ "VUIPSJ[BUJPO#ZQBTTJO3FHFY3FRVFTU.BUDIFS $7& https://tanzu.vmware.com/security/cve-2022-22978 Description In Spring Security versions 5.5.6
and 5.5.7 and older unsupport ed versions, RegexRequestMatcher can easily be misconfigured to be bypassed on some servlet containers. Applications using RegexRequestMatcher with `.` in the regular expression are possibly vulnerable to an authorization bypass. .FTTBHJOH"1*#BDLFOE ։ൃνʔϜ෦Ͱൃݟ 7.8BSFʹใࠂ $7&ൃߦ ୈҰൃݟऀ͞Μʹײँ -
4QSJOH4FDVSJUZ "VUIPSJ[BUJPO#ZQBTTJO3FHFY3FRVFTU.BUDIFS $7& @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.regexMatcher("/auth/.+"); http.authorizeRequests().anyRequest().authenticated(); http.httpBasic().realmName("test"); http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } } ݅ • SFHFY.BUDIFS Λར༻ • ͳͲ Λ͏ • ඌʹվߦίʔυΛೖΕΔͱ ೝূճආ $ curl -v http://localhost:8080/auth/testname # => 401 $ curl -v http://localhost:8080/auth/testname%0a # => 200 !!