JsonView กับการเข้ารหัสแค่บางตัวใน object (Kotlin, Java, Spring Boot)

Wasith T. (Bai-Phai)
กูโค้ด
Published in
2 min readMar 8, 2020

เรื่องมีอยู่ว่าเราอยากได้ body ของ log ที่เข้ารหัสข้อมูลแค่บางส่วนเท่านั้น แล้วเราจะทำยังไงไปดู

ตัวอย่างของ object ต้นฉบับ

{
customerId: 100,
customerName: "นายวสิฐ​ ธีรภัทรธำรง"
}

ตัวอย่างของ object ใน log

{
customerId: 100,
customerName: "{ThisTextWasEncryptedYouShouldNotAbleToRead}"
}

Complexity

ในตอนแรกเราก็คิดว่าจะวนหา key ที่ match ทุกตัว แต่ผลที่มันออกมาก็ n1*n1 เลยทีเดียว โดยที่ n1 คือจำนวนของ key ที่ระบุไว้ และ n2 คือจำนวนของ key ใน object

ซ้ำร้ายถ้าสมมุติว่าถ้า object อยู่ในอีกรูปแบบเช่น

customer: {
id: 100,
name: "Mr.Wasith Theerapattrathamrong"
}

หรือ

customers: [
{
id: 100,
name: "ใครก็ได้"
}
]

เราก็จำเป็นต้องเพิ่ม key เท่าไปในฐานข้อมูลให้มันยาก ๆ ขึ้นอีกเช่น

  • cusomter.name
  • customerName

หรืออะไรก็ได้ที่เป็นเหตุจะให้ match กันได้ แน่นอนว่ายากมาก

ตัวอย่างการ implement

โดย libraries ที่ใช้มีดังนี้

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider

เริ่มจากประกาศ class เปล่า ๆ ที่จะมาใช้บอกว่าตัวไหนที่จะให้เข้ารหัส

sealed class Views {
sealed class Secure
}

ตามด้วย custom JsonSerializer ที่ไว้ระบุว่า Views.Secure จะต้องเข้ารหัสนะ

class SecureJsonSerializer(var cryptor: Cryptor) : JsonSerializer<String>() {

override fun serialize(value: String, gen: JsonGenerator, provider: SerializerProvider) {
if (provider.activeView == Views.Secure::class.java) {
val encryptedValue = cryptor.encrypt(value)
gen.writeString(encryptedValue)
} else {
gen.writeString(value)
}
}
}

จากนั้นระบุ JsonView และ JsonSerilize ที่ poperties หรือ member ของ model นั้น ๆ

data class Customer {  val id: UInt  @JsonView(Views.Secure::class)
@JsonSerialize(using = SecureJsonSerializer::class)
val name: String
}

และสามารถตั้งค่าเพื่อเพิ่มความสามารถให้ ObjectMapper ได้ด้วย

@Configuration
class SecureWriterConfig {
@Bean
fun secureWriter(mapper: ObjectMapper): ObjectWriter {
return mapper
// ต้องการให้เขียนตัวที่ไม่ได้ประกาศ annotation JsonView ด้วย
// สามารรถใส่ไว้ใน application.properties ได้ว่า
// spring.jackson.mapper.default-view-inclusion=true
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION)
// ถ้าไม่ต้องการ pretty json สามารถเอาบรรทัดนี้ออกได้
.enable(SerializationFeature.INDENT_OUTPUT)
.writerWithView(Views.Secure::class.java)
}
}

ในตอนใช้งานนั้น

ต้องใช้งานผ่าน ObjectWriter

@RestController
class CustomerController(val service: CustomerService) {

@GetMapping("/customer")
fun customer(): Customer = service.create()

}

@Service
class CustomerService(val writer: ObjectWriter) {
fun create() = Customer(100, "ชื่อลูกค้า")
.also {
log.info("Get customer {}", writer.writeValueAsString(it))
}

companion object {
private val log: Logger = LoggerFactory.getLogger(CustomerController::class.java)
}
}

เท่านี้เราก็จะได้ output ที่อยู่ในรูป Json และเข้ารหัสเฉพาะส่วนที่ต้องการได้แล้ว และส่วนที่ return ออกไปหาหน้าบ้านไม่ถูกเข้ารหัสด้วย ถ้าอยากให้ส่วนที่ return ไปหาหน้าบ้านเข้ารหัสด้วยก็สามารถใช้ writer.writeValueAsString(objectที่จะเขียน)

และด้วยวิธีการนี้จะทำให้ค่า complexity เหลือแค่ n คือจำนวนของ key เท่านั้น

ถ้าต้องการออกแบบการเข้ารหัสยังไงให้ปลอดภัย

สามารถไปดูย้อนหลังได้ที่ https://medium.com/@phai/4b96babdd16

What’s next

สำหรับบทความต่อไปจะมาบอกว่าเราจะลบของออกจาก log เช่น password ได้อย่างไร — https://medium.com/@phai/bbec9d095a47

และจะมาเล่าให้ฟังอีกว่าเขียน log ขาเข้า ขาออกใน Spring Boot ยังไง โดยการ implement ครั้งเดียวและยังใช้การเข้ารหัสด้วยวิธีการข้างบน ไม่ต้องไปสั่งเขียนที่ controller ทุก ๆ api รอติดตามได้เลยครับผม

อ้างอิง

ขอบคุณ

--

--

Wasith T. (Bai-Phai)
กูโค้ด

ตบมือเป็นกำลังใจให้ผมด้วยนะครับ 😘