Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Server-side Kotlin in LINE Messaging API

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
Tweet

More Decks by LINE Developers

Other Decks in Technology

Transcript

  1. Server-side Kotlin in LINE Messaging API LINE Platform Development Center

    1 / Bot Platform Development 室 / B Part チーム Hirotaka Kawata 2022.05.25
  2. ࣗݾ঺հ ઒ా ༟و  )JSPUBLB ,BXBUB !ILUFDIOP • ೥ ։ൃΠϯλʔϯγοϓ

    • -*/&)#BTFετϨʔδͷ؅ཧπʔϧ • ೥ ৽ଔೖࣾ  -*/&4IPQνʔϜ • -*/&ελϯϓɾֆจࣈɾண͔ͤ͑ͷαʔόʔ։ൃ • ೥ -*/&5IJOHTͷ։ൃʹࢀՃ • #MVFUPPUI-&Λ࢖ͬͨ *P5αʔϏε޲͚ "1* • αʔόʔɾϋʔυ΢ΣΞɾγεςϜઃܭ౳৭ʑ • ೥ dݱࡏ -*/&.FTTBHJOH"1*αʔόʔ։ൃ • .FTTBHJOH"1*ͷαʔόʔαΠυ։ൃ • 8FCIPPLૹ৴ίϯϙʔωϯτͷஔ׵͑ • झຯं͍͡Γɾ௿ϨΠϠʔɾ$16ΞʔΩςΫνϟ
  3. .FTTBHJOH"1* #PUˠ 6TFSҰ੪഑৴ ϒϩʔυΩϟετɾφϩʔΩϟετ #PUˠ 6TFSݸผ഑৴ ϓογϡɾϦϓϥΠ 6TFSˠ #PU 8FCIPPLૹ৴

    ͦͷଞ ϦονϝχϡʔɾϢʔβʔ৘ใ "1* ϝοηʔδૹ৴਺ूܭ ౳ʑʜ -*/&.FTTBHJOH"1*͕ఏڙ͢Δ "1*
  4. .FTTBHJOH"1*ͷόοΫΤϯυ talk-server Messaging API Webhook Messaging API Push, Reply …

    https://api.line.me/v2/bot MJOFCPUCBDLFOE Chatbot server -*/&ͷ UBMLTFSWFSͱ .FTTBHJOH"1* CPU ͷڮ౉͠໾ ϝοηʔδड৴ ༑ͩͪ௥Ճ ϝοηʔδૹ৴
  5. 8FCIPPL&OEQPJOU #PUαʔόʔ 8FCIPPL ૹ৴αʔόʔ #PUΠϕϯτ ϑΟϧλʔ ඵؒສΠϕϯτҎ্ #PUʹؔ͢ΔΠϕϯτΛநग़ #PU8FCIPPLૹ৴ଆ 

    αʔόʔߏ੒ Πϕϯτॲཧ αʔόʔ $3. νϟοτ ετϨʔδ UBMLTFSWFS Ϣʔβʔ #PUνϟϯωϧ ؅ཧ "1* άϧʔϓ؅ཧ ඵؒສΠϕϯτҎ্ Πϕϯτͷॱংɾ੔߹ੑΛอূ 8FCIPPLૹ৴ઌ͕ͲΜͳʹ஗ͯ͘΋ ӨڹΛड͚ͣʹ҆ఆՔಇɾ࠶ૹػೳ ೥ʹେ෯࡮৽ ,PUMJOΛੵۃ࠾༻ 8FCIPPL $PSPVUJOFT
  6. UBMLTFSWFS "1*(BUFXBZ ϦΫΤετ ඵؒສҎ্ #PUϝοηʔδ഑৴ଆ  αʔόʔߏ੒ ഑৴ॲཧαʔόʔ #PUνϟϯωϧ ؅ཧ

    "1* ೔ԯҎ্ͷϝοηʔδॲཧ ਺ઍສ୯ҐͷϢʔβʔ΁Ұ੪഑৴ ༏ઌ౓෇͖ͷ഑৴Ωϡʔ $3. νϟοτ ετϨʔδ ૹ৴਺؅ཧ ϚΠΫϩαʔϏεΞʔΩςΫνϟ ͦΕͦΕ͕ଟ਺ͷϦΫΤετΛॲཧ Ϣʔβʔ ഑৴ 2VFVF ϝοηʔδૹ৴ αʔόʔ
  7. UBMLTFSWFS "1*(BUFXBZ #PUϝοηʔδ഑৴ଆ  αʔόʔߏ੒ ഑৴ॲཧαʔόʔ #PUνϟϯωϧ ؅ཧ "1* $3.

    νϟοτ ετϨʔδ ૹ৴਺؅ཧ Ϣʔβʔ ഑৴ 2VFVF ,PUMJO ΁ͷҠߦΛܭըத ,BGLB %FDBUPO  3FBDUPS ϝοηʔδૹ৴ αʔόʔ
  8. 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
  9. +BWBͱ ,PUMJOͱ 4DBMB +BWBͱͷ ਌࿨ੑ ݴޠͷػೳ ֶशίετ  +BWBҎ߱ਐԽ ·ͩ·ͩ͜Ε͔Β

    ࣾ಺ڞ௨ݴޠ ৑௕͕ͩ೉͕͠͞গͳ͍ +BWB͔Βؾܰʹݺ΂Δ ΄ͱΜͲࠔΒͳ͍ ҉໧తͳܕม׵ /VMMBCMF %BUBDMBTT  XIFOCMPDL $PSPVUJOFT +BWBΛ࢖ͬͨ͜ͱ͕ ͋Ε͹௚ײత Kotlin Java +BWB͔Β DBMM͕ݫ͍͠ ಛ༗ͷίϨΫγϣϯܕ BT4DBMB BT+BWB ಛ༗ͳ 0QUJPO ڧྗͳ 'VODUJPOBM 1SPHSBNNJOHαϙʔτ ͱ͖ͬͭʹ͍͘ߏจ ҉໧తɺॳݟͰಡΊͳ͍ *NQMJDJU Scala ஸ౓͍͍
  10. ඇಉظॲཧͷ ॻ͖΍͢͞ ಋೖͷखܰ͞ ετϦʔϜॲཧ 3FBDUJWF4USFBNT ͷֶशίετ͕ߴ͍ ରԠϥΠϒϥϦ͕ଟ͍ 4QSJOHͷαϙʔτ ετϦʔϜॲཧʹಛԽ ॆ࣮ͨ͠

    0QFSBUPS ௚ײత ݴޠػೳͷҰ෦ TVTQFOEGVO BXBJU +BWB'VUVSF΍ 3Yͱͷ ίωΫλʔ͕ॆ࣮ 'MPXͷ 0QFSBUPS͕ශऑ ୯ൃ࣮ߦͷඇಉظԽ޲͚ ඇಉظॲཧ  $PSPVUJOFT3FBDUPS Coroutines Reactor
  11. ྑ͍ͱ͜Ζ • +7.Ͱ͸ࠔΔ͜ͱ͕ຆͲͳ͍ • +BWBͱͷ਌࿨ੑ͕࠷ߴ • 4DBMBͱൺ΂ͯ΋֨ஈʹྑ͍ • ͔Ώ͍ͱ͜Ζʹख͕ಧ͘ •

    /VMMBCMF TNBSUDBTU • ֦ுؔ਺͕Α͍ • ಛʹ *%&ͱͷ਌࿨ੑ • ͪΐͬͱॏ͍͚Ͳʜ ؾʹͳΔͱ͜Ζ • ύλʔϯϚονϯά͕ऑ͍ • $BTFDMBTT XIFOʜ • GMBU.BQ ͕ݴޠͱͯ͠΄͍͠ • 4DBMBͷ GPS • &YQFSJNFOUBM͕ຊ౰ʹ࣮ݧత • ίϯύΠϧʹ͕͔͔࣌ؒΔ • LBQU ͕ಛʹॏ͍ • *%&ิ׬΋ॏ͍ جຊతʹ +BWB͔Β৐Γ׵͑ͯ΋ࠔΔ͜ͱ͸ຆͲͳ͍ ,PUMJOͷॻ͖৺஍
  12. talk-server ༑ͩͪ௥Ճ ϝοηʔδૹ৴ MJOFCPUCBDLFOE Chatbot server ϝοηʔδड৴ αʔϏε୯ମͷςετͰ͸ ෳ߹తͳཁҼͰյΕΔ৔߹΋ &OEUP&OEͰ

    .FTTBHJOH"1*ͷςετΛߦ͍͍ͨ .FTTBHJOH"1*ͷςετ؀ڥ େن໛ͳมߋΛ࠷খݶͷϦεΫͰϦϦʔε͢ΔͨΊʹ͸ɾϚΠΫϩαʔϏεͳΒͰ͸ͷ೰Έ
  13. .FTTBHJOH"1*ͷςετ؀ڥ #PU։ൃऀ޲͚ ςετ "1*ͷ 3FRVFTU3FTQPOTFʹมߋ͕ͳ͍͔ .FTTBHJOH"1*ࣗମͷςετɺ8FCIPPLͷड৴ςετ -*/&Ϣʔβʔ ޲͚ςετ ਖ਼͍͠Ϣʔβʔʹϝοηʔδ͕ૹ৴͞ΕΔ͔ɾ಺༰͸ਖ਼͍͔͠ -*/&ϢʔβʔΛٖࣅతʹ࠶ݱͯ͠ςετ

    UBMLTFSWFS಺෦ "1*ར༻ ಺෦తͳςετ ਖ਼͘͠ૹ৴਺͕ܭ্͞ΕΔ͔ɾ#PUͷઃఆ͕ਖ਼͘͠൓ө͞ΕΔ͔ .FTTBHJOH"1*಺෦ͷ *OUFHSBUJPO5FTU "1*ϓϥοτϑΥʔϜͷςετʹٻΊΒΕΔ΋ͷ
  14. 1VTI3FQMZ ༑ͩͪ௥Ճ ˠϝοηʔδૹ৴ ˠ 8FCIPPLड৴ ˠ#PUฦ৴ #SPBEDBTU Ϣʔβʔ࡞੒ ˠ ༑ͩͪ௥Ճ

    ˠϒϩʔυΩϟετ /BSSPXDBTU ಛఆͷଐੑΛ࣋ͭϢʔβʔ࡞੒ ˠφϩʔΩϟετ 8FCIPPL ༷ʑͳλΠϓͷ 8FCIPPLΛݕূ ಈըɾը૾ɾԻ੠ 3JDI.FOV Ϧονϝχϡʔͷηοτ ˠ൓ө֬ೝ ˠVTFS*E ࢦఆͰηοτ (SPVQ3PPN άϧʔϓͷೖୀࣨɾ#PUͷάϧʔϓڐՄͷಈ࡞ɾෳ਺ #PU ରԠ &UD ૯ςετ߲໨͸ Ҏ্ ೥݄ݱࡏ ֎෦޲͚ .FTTBHJOH"1* ͷಈ࡞͔Βɺૹ৴਺؅ཧ౳ɾ಺෦ঢ়ଶɾ৚݅෼ذΛ໢ཏతʹ .FTTBHJOH"1*ςετέʔεͷྫ
  15. Ϣʔβʔϝοηʔδड৴ ಺༰ݕূ #PUUP6TFS ϝοηʔδ഑৴ .FTTBHJOH"1* Ϣʔβʔ࡞੒ UBMLTFSWFS .FTTBHJOH"1*ςετͷྲྀΕ -*/&ެࣜΞΧ΢ϯτ ༑ୡొ࿥

    -*/&಺෦ "1* 6TFSUP#PU ϝοηʔδૹ৴ -*/&಺෦ "1* 8FCIPPLड৴ ಺༰ݕূ "QQUZQFWFSTJPO ϓϥΠόγʔಉҙͷ༗ແ ϦϓϥΠϝοηʔδ 3FQMZ5PLFO ༗ޮੑݕূ #PUબ୒ɾ࡞੒ ಺෦ "1* (SPVQڐՄ "VUIPSJUZͷ༗ແ ૹ৴਺ݕূ Ϣʔβʔ࡟আ ඇಉظతͳ݁ՌΛݕূ͢Δςετέʔε͕ଟ͍
  16. 4DBMB5FTU ͷॻ͖৺஍Λ ,PUMJO Ͱ΋࠶ݱͨ͠ςετϑϨʔϜϫʔΫ LPUFTU  LPUFTUJP ॊೈͳ 4QFD 4USJOH4QFD

    'VO4QFD 4IPVME4QFD ͳͲଟ਺ͷ 4QFD͕༻ҙ͞Ε͍ͯΔ ্खʹ࢖͏ͱ5FTUDBTF͕ߏ଄Խ͠΍͍͢ɾ໊લʹΑͬͯςετ͕؅ཧ͠΍͍͢ ڧྗͳ "TTFSUJPO ,PUMJOOBUJWFͳ "TTFSTUJPO %4-తͳه๏ʹ΋ରԠ /VMMBCMFରԠ΍ &YUFOTJPOGVODUJPO͕ଟ਺༻ҙ͞Ε͍ͯͯॊೈʹॻ͚Δ $PSPVUJOFT "TZOD ରԠ 5FTUNFUIPE͕͢΂ͯ TVTQFOEGVODUJPOʹͳ͍ͬͯΔ "XBJUJMJUZ ͷΑ͏ͳҰఆ࣌ؒ಺ʹ৚݅Λຬͨ͢͜ͱΛ֬ೝ͢Δඇಉظςετ͕ॻ͖΍͍͢
  17. ςετͷ؅ཧ ඇಉظରԠ "TTFSUJPOT ॊೈͳ 4QFD ߏ଄Խ͞Εͨςετ $PSPVUJOFTରԠ FWFOUVBMMZ DPOUJOVBMMZ $PODVSSFOUUFTUJOH

    ,PUMJOOBUJWFͳ GMVFOUBTTFSUJPO 5FTU໊͕Θ͔Γʹ͍͘ ςετΛߏ଄Խ͠ʹ͍͘ SVO#MPDLJOH5FTU ඇಉظςετ೉͍͠ +BWBͷ BTTFSUMJCSBSZ ,PUMJOαϙʔτ͕ෆ׬શ /VMMBCMF౳ +6OJUͱͷൺֱ JUnit 5 kotest
  18. ࣮ࡍʹ࢖ΘΕ͍ͯΔςετίʔυΛҰ෦ൈਮ 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 ͰςετΛߏ଄Խ ςετ݁Ռͷ໊લ͕Θ͔Γ΍͍͢
  19. $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Πϕϯτ͕ඵಧ͔ͳ͍
  20. $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ʹઃఆͨ͠಺༰Λग़ྗ
  21. ͦͷଞศརͳػೳ 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
  22. &OEUP&OE5FTUJOH • ࣦഊ͕ଟ͍ ಛʹ#&5" • ৚݅Λ΋ͬͱϥϯμϜʹ͍ͨ͠ • 1SPQFSUZCBTFEUFTUJOH • ࣮ߦ͕࣌ؒ௕Ί

     ෼ఔ౓ • LPUFTU ͰฒྻԽ • 4FOUSZͷ 5SBDJOHͰ஗͍ ςετΛ؂ࢹ LPUFTU • ࠷৽൛ͷ ,PUMJO͕ඞਢ • &YQFSJNFOUBMػೳΛͨ͘͞Μ࢖͏ • ͨ·ʹόάͬͯΔ • ฒྻςετͱ͔ • .PDLJUP͕ͪΐͬͱ࢖͍ʹ͍͘ • .PDLL Λ࢖͑͹ྑ͍͕ .FTTBHJOH"1*ςετ  ࠓޙͷ՝୊ͱؾʹͳΔ఺
  23. 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&ൃߦ  ୈҰൃݟऀ੢໺͞Μʹײँ
  24. 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 !!