YUMEMI Advent Calendar 2021 19日目の記事です。
はじめに
Vironとはフロントの開発要らずで管理画面を構築することのできるツールです。
管理画面用のAPIとOAS2.0準拠のjsonファイル(swagger.json)を用意さえすれば、あとは自動的に管理画面を構築してくれます。
今回はSpringBoot+KotlinでAPIを用意し、Springfoxでjson形式のSwagger定義ファイルを自動生成してVironを試してみました!
参考文献
主にこれら2つの記事が参考になりました。
Vironを立ち上げる
Vironのソースコードはこちらにありますのでcloneしておきます。
git clone https://github.com/cam-inc/viron
こちらを参考にVironのビルドを行います。
2021/12/19現在はv1.3.2
が最新のようで、こちらも動作確認済みです。
ビルド後、dist/
以下をWebサーバ上で(WebServerForChromeを使いました)で立ち上げます。
必須APIを作る
Vironサーバを開発するにあたって、まず3つのAPIを作成する必要があります。
認証方式を取得する /viron_authtype
swaggerを取得する /swagger.json
グローバルメニューを取得する /viron
です。
管理画面が立ち上がる前に、Viron側からAPIへ上記のパスにリクエストが投げられます。
Vironの動きとしてはこうなっているようです。
- この画面でまずはswagger.jsonへのパス(http://localhost:8080/swagger.json) を入力すると、このパスにアクセスされます。
2-a. API側でそのまま返せば、次に/viron
にリクエストします。
2-b. API側で認証機能を挟んでいる場合は401を返すかと思いますが、401が返されると、今度は/viron_authtype
というパスにリクエストされます。 - ここで認証方式をViron側に伝えて、次に認証APIが叩かれます。
- 認証APIでtokenを返すとVironはそのtokenをheaderに込めて再度
/swagger.json
をリクエストします。 - jsonファイルが取れたら、
/viron
をリクエストしてグローバルメニューの定義を取得します。
認証機能を組み込む場合は準備としてtokenを返却するエンドポイントも追加する必要があります。
というわけで、作るAPIは以下
/swagger.json(認証あり)
/viron_authtype(認証なし)
/viron/sign_in(認証なし)
/viron(認証あり)
tokenの生成や検証方法などはあんまり本質的ではないので省略します。
/swagger.json
build.gradle.kts
に以下を追記。Springfoxは3.0.0
で試しました。
implementation("io.springfox:springfox-boot-starter:3.0.0")
swagger関連のConfig
@Configuration
@EnableSwagger2
class SwaggerConfig() {
@Bean
fun customSwaggerConfig(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.securitySchemes(securitySchemes())
.securityContexts(securityContext())
// ここをhttpsにした場合はVironから/vironや管理画面用APIはhttpsでリクエストされます。
// localhostで動かす場合はhttpで大丈夫です。
.protocols(setOf("https"))
.select()
// 管理画面用のAPIは全て以下のようにエンドポイントを切るなどして管理画面に不要なエンドポイントは公開しないようにするならば
.paths(PathSelectors.regex("/viron.*"))
.build()
}
@Bean
fun uiConfig(): UiConfiguration {
return UiConfigurationBuilder.builder()
.docExpansion(DocExpansion.LIST)
.build()
}
private fun securitySchemes(): List<ApiKey> {
return listOf(
ApiKey("Bearer", HttpHeaders.AUTHORIZATION, In.HEADER.name)
)
}
private fun securityContext(): List<SecurityContext> {
return listOf(SecurityContext
.builder()
.securityReferences(defaultAuth())
.build())
}
private fun defaultAuth(): List<SecurityReference> {
val authorizationScope = AuthorizationScope("global", "accessEverything")
return listOf(SecurityReference("Bearer", arrayOf(authorizationScope)))
}
}
.protocols(setOf("https"))
実際に管理画面をデプロイしてhttpsでリクエスト投げたいときはこのメソッドを挟みます。
次にapplication.propertiesに以下の記述を追加してswaggerのjsonファイルを以下のエンドポイントで返すようにします。
springfox.documentation.swagger.v2.path=/swagger.json
/viron_authtype
雑ですが例なので...
@GetMapping("/viron_authtype", produces = ["application/json;charset=UTF-8"])
fun getVironAuthType(): String {
return """
[
{
# このtypeを指定するとemailとパスワードを入力させる画面がViron上で出現します。
"type": "email",
"provider": "sample-api",
# 以下のパスにemailとパスワードをボディに込めてリクエストされますので、OKならtokenを返してあげます。
"url": "/viron/sign_in",
"method": "POST"
},
{
# このエンドポイント使われる場面が分からなくて一応ありますが要らないかも?
"type": "signout",
"provider": "sample-api",
"url": "/viron/sign_out",
"method" : "POST"
}
]
""".trimIndent()
}
上記のjsonを返すと、1.で401を返した後にemailとパスワードを入力させる画面がViron上で出現します。
入力されたemailとパスワードをボディに込めて/viron/sign_in
にリクエストされます。
正しい組み合わせであればtokenを返しますが、詳しくは次へ
/viron/sign_in
@PostMapping("/viron/sign_in", produces = ["application/json;charset=UTF-8"])
fun signIn(
@RequestBody signInRequest: signInRequest,
response: HttpServletResponse
) {
// sign in成功したらtokenを返す関数
val token = login(signInRequest.email, signInRequest.password)
// tokenはautorizationヘッダに入れて200を返す。
response.addHeader("authorization", "Bearer $token")
}
トークンは上記のようにヘッダに込めて返します。
/viron
各項目の意味は以下の公式ドキュメントに記載されておりますので、よしなに参考にしていきます。
https://cam-inc.github.io/viron-doc/docs/dev_api_menu.html
@GetMapping("/viron", produces = ["application/json;charset=UTF-8"])
fun getVironGlobalMenu(): String {
return """
{
"theme": "stadndard",
"color": "black",
"name": "sample CMS",
"tags": [
"CMS"
],
"thumbnail": "https://example.com/thumbnail",
"pages": [
{
"section": "manage",
"group": "books",
"id": "books",
"name": "Books",
"components": [
{
"api": {
"method": "get",
"path": "/viron/books"
},
"name": "Books",
"style": "table",
"query": [
{"key": "name", "value": "string"}
],
"primary" : "id",
"pagination": true,
"table_labels": [
"id",
"name"
]
}
]
}
]
}
""".trimIndent()
}
CORS
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH", "OPTION")
.allowedOrigins(
arrayOf("http://localhost:3000", "other app client origin") // ここにVironのOrigin
)
.exposedHeaders(
"authorization" // Access-Control-Expose-Headers に authorizationを追加する
)
}
CORSの設定を追加します。
認証ライブラリに何を使うか次第なところはありますが、共通するのはVironフロントのOriginを許可するのと、Viron側からトークンをauthorizationヘッダに込めてViron側に渡すので読み出し許可します。
例ではこちらを使用しています。
https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.html
管理画面用API
本の一覧、作成、更新、削除ができるようにAPIを追加しましょう。
一覧
一覧画面では、ページネーションと検索クエリの追加ができますので両方追加します。
ページネーションに必要なリクエストパラメータはlimit
, offset
で、これ以外の名前のパラメータは以下の写真のように検索クエリとして認識させることができます。
以下の部分の/viron
のレスポンスが該当する設定になります。
"query": [
{"key": "name", "value": "string"}
],
ページネーションと検索クエリを使用した場合のControllerの例です。
@GetMapping("/viron/books", produces = ["application/json;charset=UTF-8"])
fun getAllNews(
@RequestParam(name = "limit", required = false) limit: Int?,
@RequestParam(name = "offset", required = false) offset: Int?,
@RequestParam(name = "name", required = false) name: String?,
response: HttpServletResponse
): List<Book> {
bookService.getPaginatedBooksByName(limit, offset, name).apply {
response.addHeader("x-pagination-current-page", this.currentPage.toString())
response.addHeader("x-pagination-limit", this.limit.toString())
response.addHeader("x-pagination-total-pages", this.totalPage.toString())
return this.books
}
}
ページネーション
ページネーションを行うには以下のヘッダに適切な値を込めて返却する必要があります。
Viron側でこれらのヘッダを参照してよしなにページネーションしてくれます。
x-pagination-current-page
x-pagination-limit
-
x-pagination-total-pages
これらの計算方法は例えばこんな感じでしょうか
fun getPaginatedBooks(
limit: Int?,
offset: Int?,
): PaginatedBooks {
val usingLimit = limit ?: 50
val usingOffset = offset ?: 0
val currentPage = usingOffset / usingLimit + 1
// DBに存在する総レコード数を取得して、ページ数の計算
val totalPages = booksRepository.getBooksCount().let {
if (it < 1) 0 else it / usingLimit + 1
}
val books = bookRepository.findAllWithLimitAndOffset(usingLimit, usingOffset)
return PaginatedBooks(usingLimit, currentPage, totalPages, books)
}
これらをControllerに返却したら、それぞれヘッダに込めますが、Vironで読めるようにする設定を追加します。
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/**")
.allowedMethods("HEAD", "GET", "PUT", "POST", "DELETE", "PATCH", "OPTION")
.allowedOrigins(
arrayOf("http://localhost:3000", "other app client origin")
)
.exposedHeaders(
"x-pagination-current-page", // 追加
"x-pagination-limit", // 追加
"x-pagination-total-pages", // 追加
"authorization"
)
検索クエリ
この検索クエリ用パラメータは自由に使えます。
例えば、nameをもらってこれで本の名前で曖昧検索+ページネーションをしてみます。
Repositoryにこんな感じで実装してServiceから呼んだら出来上がりです。
@Query(
"""
SELECT * FROM book
WHERE :name IS NULL OR name LIKE CONCAT('%', :name, '%')
ORDER BY update_time DESC
LIMIT :limit
OFFSET :offset
""", nativeQuery = true
)
fun findAllWithLimitAndOffsetAndName(
@Value("limit") limit: Int,
@Value("offset") offset: Int,
@Value("name") name: String?
): List<Book>
作成,更新,削除
作成,更新,削除のAPIも作っていきます。
@PostMapping("/viron/book", produces = ["application/json;charset=UTF-8"])
fun createBook(
@Validated @RequestBody body: BookRequest
): Book {
return bookService.create(body.name)
}
@PutMapping("/viron/book/{id}", produces = ["application/json;charset=UTF-8"])
fun updateBook(
@Validated @RequestBody body: BookRequest,
@PathVariable("id") id: Int
): Book {
return bookService.update(id, body.name)
}
@DeleteMapping("/viron/book/{id}", produces = ["application/json;charset=UTF-8"])
fun deleteBook(
@PathVariable("id") id: Int
) {
bookService.delete(id)
}
これらエンドポイントもswagger.jsonで自動出力されたのをViron側が読み取ってよしなにこれらのパスにリクエストを投げてくれます。
最後に
これで土台はできました。
swagger.jsonもAPIで自動生成するのであとは管理画面用のAPIを追加してそのままAPIをデプロイするだけでどんどん管理画面が充実していきます。
ただSpringfoxを使用してjsonを自動生成しているためspringfoxで認識できないformatなどを指定しようとしてもできない事態に遭遇しました。
例えばこちらのWYSIWYS入力フォームを追加するためには
format: "wyswyg"
swagger.json上の追加したいプロパティに対して上記のformatを指定する必要があります。
これを自動生成するために、
class BookRequest(
@field:NotNull
var name: String?,
@field:NotNull
@field:ApiParam(format = "wyswig")
var overview: String?,
)
こういうことがしたかったのですがwyswig
はどうやらSpringfoxで認識できないようで、できませんでした。
引き続き調査は進めたいですね...
今のところ少し制約はありますが引き続き便利なVironを使っていきたいと思いますー。