JsonView กับการเข้ารหัสแค่บางตัวใน object (Kotlin, Java, Spring Boot)
เรื่องมีอยู่ว่าเราอยากได้ 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 รอติดตามได้เลยครับผม
อ้างอิง
- https://github.com/9tae2013/json-view2 โดยพี่เต้ https://fb.me/taemonz ผมยกย่องให้เป็น Living {Kotlin|Java|Spring Boot} documents. ถามอะไรตอบได้ 🤣🤣🤣
ขอบคุณ
- น้องยุทธ์ https://fb.me/iBabieTM
- พี่เก๋ https://medium.com/@phatpan