[๋ณด์•ˆ / ์ธ์ฆ] ๐Ÿ”‘ํ•ด์‹ฑ - Hashing, ๐Ÿช™ํ† ํฐ - Token

TATAยท2023๋…„ 3์›” 8์ผ
0

๋ณด์•ˆ / ์ธ์ฆ

๋ชฉ๋ก ๋ณด๊ธฐ
2/3

โ–ท ํ•ด์‹ฑ

Hashing

๋ณตํ˜ธํ™”๊ฐ€ ๊ฐ€๋Šฅํ•œ ๋‹ค๋ฅธ ์•”ํ˜ธํ™” ๋ฐฉ์‹๋“ค๊ณผ ๋‹ฌ๋ฆฌ, ํ•ด์‹ฑ์€ ์•”ํ˜ธํ™”๋งŒ ๊ฐ€๋Šฅํ•˜๋‹ค.
๋‹จ๋ฐฉํ–ฅ ์•”ํ˜ธํ™” ๋ฐฉ์‹์ด๋ฉฐ, ํ•ด์‹ฑ์€ ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•”ํ˜ธํ™”๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.

โ“๋ณตํ˜ธํ™”๊ฐ€ ๋ถˆ๊ฐ€๋Šฅํ•œ ํ•ด์‹ฑ์„ ์‚ฌ์šฉํ•˜๋Š” ์ด์œ ?
ํ•ด์‹ฑ์˜ ๋ชฉ์ ์€ ๋ฐ์ดํ„ฐ ์ž์ฒด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ,
๋™์ผํ•œ ๊ฐ’์˜ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋Š”์ง€ ์—ฌ๋ถ€๋งŒ์„ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด๊ธฐ ๋•Œ๋ฌธ.

๋กœ๊ทธ์ธ ์š”์ฒญ ์ฒ˜๋ฆฌ ์˜ˆ์‹œ
์„œ๋ฒ„์ธก์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ชจ๋ฅด๋Š” ์ƒํƒœ์—์„œ ๋กœ๊ทธ์ธ ์š”์ฒญ์„ ๋ฐ›์•˜์„ ๋•
ํ•ด์‹ฑํ•œ ๊ฐ’๋ผ๋ฆฌ ๋น„๊ตํ•ด์„œ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์—ฌ ์ฒ˜๋ฆฌํ•œ๋‹ค.
(์ •ํ™•ํ•œ ๊ฐ’์„ ๋ชฐ๋ผ๋„ ์ผ์น˜ํ•˜๋Š”์ง€๋งŒ ํ™•์ธํ•˜๋ฉด ๋˜๊ธฐ ๋•Œ๋ฌธ, ๋ฐ์ดํ„ฐ ์œ ์ถœ ์œ„ํ—˜โ†“)

โ—๏ธํ•ด์‹œ ํ•จ์ˆ˜์˜ ํŠน์ง•
๏ผ ํ•ญ์ƒ ๊ฐ™์€ ๊ธธ์ด์˜ ๋ฌธ์ž์—ด์„ ๋ฆฌํ„ด
๏ผ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฌธ์ž์—ด, ๋™์ผํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ โ†’ ๋ฐ˜๋“œ์‹œ ๋‹ค๋ฅธ ๊ฒฐ๊ณผ๊ฐ’
๏ผ ๋™์ผํ•œ ๋ฌธ์ž์—ด, ๋™์ผํ•œ ํ•ด์‹œ ํ•จ์ˆ˜ โ†’ ํ•ญ์ƒ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ’

๋น„๋ฐ€๋ฒˆํ˜ธํ•ด์‹œ ํ•จ์ˆ˜(SHA1) ๋ฆฌํ„ด ๊ฐ’
โ€˜passwordโ€™โ€˜5BAA61E4C9B93F3F0682250B6CF8331B7EE68FD8โ€™
โ€˜Passwordโ€™โ€˜8BE3C943B1609FFFBFC51AAD666D0A04ADF83C9Dโ€™

๐Ÿ”‘ ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”

ํ•ญ์ƒ ๊ฐ™์€ ๊ฒฐ๊ณผ๊ฐ’์ด ๋‚˜์˜ค๋Š” ํ•ด์‹œ ํ•จ์ˆ˜์˜ ํŠน์„ฑ์„ ์ด์šฉํ•˜์—ฌ
ํ•ด์‹œ ํ•จ์ˆ˜๋ฅผ ๊ฑฐ์น˜๊ธฐ ์ด์ „์— ๊ฐ’์„ ๋ฏธ๋ฆฌ ์•Œ์•„๋‚ผ ์ˆ˜ ์žˆ๋„๋ก
๊ธฐ๋กํ•ด๋†“์€ ํ‘œ์ธ ๋ ˆ์ธ๋ณด์šฐ ํ…Œ์ด๋ธ”์ด ์กด์žฌํ•œ๋‹ค.

์ด๋Š” ๋ณด์•ˆ์— ์ทจ์•ฝํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์†”ํŠธ(salt)๋ฅผ ํ™œ์šฉํ•ด์•ผ ํ•œ๋‹ค.


๐Ÿ”‘ ย ์†”ํŠธย 

Salt

ํ•ด์‹ฑ ์ด์ „ ๊ฐ’์— ์ž„์˜์˜ ๊ฐ’์„ ๋”ํ•ด ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์ถœ ๋˜๋”๋ผ๋„
ํ•ด์‹ฑ ์ด์ „์˜ ๊ฐ’์„ ์•Œ์•„๋‚ด๊ธฐ ๋”์šฑ ์–ด๋ ต๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์ด๋‹ค.

์†”ํŠธ๊ฐ€ ํ•จ๊ป˜ ์œ ์ถœ ๋œ ๊ฒƒ์ด ์•„๋‹ˆ๋ผ๋ฉด ์•”ํ˜ธํ™” ์ด์ „์˜ ๊ฐ’์„ ์•Œ์•„๋‚ด๋Š” ๊ฒƒ์€ ๋ถˆ๊ฐ€๋Šฅ์— ๊ฐ€๊น๋‹ค.

๋น„๋ฐ€๋ฒˆํ˜ธ + ์†”ํŠธํ•ด์‹œ ํ•จ์ˆ˜(SHA1) ๋ฆฌํ„ด ๊ฐ’
โ€˜passwordโ€™ + โ€˜saltโ€™โ€˜C88E9C67041A74E0357BEFDFF93F87DDE0904214โ€™
โ€˜Passwordโ€™ + โ€˜saltโ€™โ€˜38A8FDE622C0CF723934BA7138A72BEACCFC69D4โ€™

โ–ท Token

ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹

ํ† ํฐ์„ ์‚ฌ์šฉํ•˜๋ฉด ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ
์„œ๋ฒ„๊ฐ€ ์•„๋‹Œ ํด๋ผ์ด์–ธํŠธ ์ธก์— ์ €์žฅํ•  ์ˆ˜ ์žˆ๋‹ค.

โ“์™œ ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹์ด ์ƒ๊ฒผ๋‚˜์š”?
์„ธ์…˜ ์ธ์ฆ ๋ฐฉ์‹์—์„œ๋Š” ์„œ๋ฒ„์—์„œ ์œ ์ €์˜ ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•˜๋Š”๋ฐ
์„œ๋ฒ„์˜ ๋ถ€๋‹ด์ด ์ปค์ ธ์„œ, ์„œ๋ฒ„์˜ ๋ถ€๋‹ด์„ ์ค„์ด๊ธฐ ์œ„ํ•ด
ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๋ฐฉ์‹์ด ๋“ฑ์žฅํ•œ ๊ฒƒ์ด๋‹ค.

ํ† ํฐ์€ ๋ฌด์–ธ๊ฐ€๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒํ•œ์ด๋‚˜ ์ž๊ฒฉ์„ ์˜๋ฏธํ•˜๋ฉฐ,
์›น ๋ณด์•ˆ์—์„œ๋Š” ์ธ์ฆ๊ณผ ๊ถŒํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ์•”ํ˜ธํ™”๋œ ๋ฌธ์ž์—ด์„ ๋งํ•œ๋‹ค.

์ด๋ฅผ ์ด์šฉํ•ด ํŠน์ • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋Œ€ํ•œ ์‚ฌ์šฉ์ž์˜ ์ ‘๊ทผ ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿช™ ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹์˜ ํ๋ฆ„


๐Ÿช™ ํ† ํฐ ์ธ์ฆ ๋ฐฉ์‹์˜ ์žฅ์ ๊ณผ ํ•œ๊ณ„

ย ๋ฌด์ƒํƒœ์„ฑย 
์„œ๋ฒ„๊ฐ€ ์œ ์ €์˜ ์ธ์ฆ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜์ง€ ์•Š์Œ.
์„œ๋ฒ„๋Š” ๋น„๋ฐ€ ํ‚ค๋ฅผ ํ†ตํ•ด ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ณด๋‚ธ ํ† ํฐ์˜ ์œ ํšจ์„ฑ๋งŒ ๊ฒ€์ฆํ•  ๋ฟ.

But, ํ† ํฐ ์ž์ฒด๊ฐ€ ํƒˆ์ทจ๋˜์—ˆ์„ ๋•Œ์—๋„ ์„œ๋ฒ„๊ฐ€ ํ† ํฐ์„ ๊ฐ•์ œ๋กœ ๋งŒ๋ฃŒ์‹œํ‚ฌ ์ˆ˜ ์—†๋‹ค.

ย ํ™•์žฅ์„ฑย 
๋‹ค์ˆ˜์˜ ์„œ๋ฒ„๊ฐ€ ๊ณตํ†ต๋œ ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์งˆ ํ•„์š”๊ฐ€ ์—†์Œ.
๋•๋ถ„์— ์„œ๋ฒ„ ํ™•์žฅ์ด ์šฉ์ดํ•จ.

But, ์œ ํšจ๊ธฐ๊ฐ„์„ ๊ธธ๊ฒŒ ์„ค์ •ํ•˜๊ฒŒ ๋˜๋ฉด ํƒˆ์ทจ๋  ๊ฒฝ์šฐ ๋” ์น˜๋ช…์ ์ผ ์ˆ˜ ์žˆ๋‹ค.

ย ์–ด๋””์„œ๋‚˜ ํ† ํฐ ์ƒ์„ฑ ๊ฐ€๋Šฅย 
ํ† ํฐ์˜ ์ƒ์„ฑ๊ณผ ๊ฒ€์ฆ์ด ํ•˜๋‚˜์˜ ์„œ๋ฒ„์—์„œ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์•„๋„ ๋จ.
๊ทธ๋ž˜์„œ ํ† ํฐ ์ƒ์„ฑ๋งŒ์„ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Œ.
(์—ฌ๋Ÿฌ ์„œ๋น„์Šค ๊ฐ„์˜ ๊ณตํ†ต๋œ ์ธ์ฆ ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ ๊ฐ€๋Šฅ)

ย ๊ถŒํ•œ ๋ถ€์—ฌ์— ์šฉ์ดย 
ํ† ํฐ์€ ์ธ์ฆ ์ƒํƒœ, ์ ‘๊ทผ ๊ถŒํ•œ ๋“ฑ ๋‹ค์–‘ํ•œ ์ •๋ณด๋ฅผ ๋‹ด์„ ์ˆ˜ ์žˆ๊ธฐ์—
์‚ฌ์šฉ์ž ๊ถŒํ•œ ๋ถ€์—ฌ์— ์šฉ์ดํ•˜๋‹ค. ๊ทธ๋ž˜์„œ ์–ด๋“œ๋ฏผ ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฐ ์ •๋ณด์—
์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๋ฒ”์œ„๋„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

But, ํ† ํฐ์— ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด์œผ๋ฉด ๊ทธ๋งŒํผ ์•”ํ˜ธํ™” ๊ณผ์ •์ด ๊ธธ์–ด์ง€๊ณ 
ํ† ํฐ์˜ ํฌ๊ธฐ๋„ ์ปค์ง€๊ธฐ ๋•Œ๋ฌธ์— ๋„คํŠธ์›Œํฌ ๋น„์šฉ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿช™ JWT

JSON Web Token

JSON ๊ฐ์ฒด์— ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์ด๋ฅผ ํ† ํฐ์œผ๋กœ ์•”ํ˜ธํ™”ํ•˜์—ฌ ์ „์†กํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ์ˆ ์ด๋‹ค.

ย 1. Headerย 

ํ† ํฐ์˜ ์ข…๋ฅ˜, ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋งŒ๋“ค ๋•Œ ์‚ฌ์šฉํ• 
์•Œ๊ณ ๋ฆฌ์ฆ˜์„ JSON ํ˜•ํƒœ๋กœ ์ž‘์„ฑํ•œ๋‹ค.

{
  "alg": "HS256",
  "typ": "JWT"
}
// ์ด JSON ๊ฐ์ฒด๋ฅผ base64 ๋ฐฉ์‹์œผ๋กœ ์ธ์ฝ”๋”ฉํ•˜๋ฉด JWT์˜ ์ฒซ ๋ฒˆ์งธ ๋ถ€๋ถ„์ธ Header๊ฐ€ ์™„์„ฑ๋œ๋‹ค.
// โ—๏ธ์ฐธ๊ณ ) base64 ๋ฐฉ์‹ - ์–ผ๋งˆ๋“ ์ง€ ๋””์ฝ”๋”ฉํ•  ์ˆ˜ ์žˆ๋Š” ์ธ์ฝ”๋”ฉ ๋ฐฉ์‹์ž„.
//                     ๋…ธ์ถœ๋˜์–ด์„œ๋Š” ์•ˆ๋˜๋Š” ์ •๋ณด๋Š” ๋‹ด์ง€ ๋ง๊ธฐ

ย 2. Payloadย 

์ „๋‹ฌํ•˜๋ ค๋Š” ๋‚ด์šฉ๋ฌผ์„ ๋‹ด๊ณ  ์žˆ๋Š” ๋ถ€๋ถ„์ด๋‹ค.

// ์–ด๋–ค ์ •๋ณด์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ์ง€์— ๋Œ€ํ•œ ๊ถŒํ•œ,
// ์œ ์ €์˜ ์ด๋ฆ„๊ณผ ๊ฐ™์€ ๊ฐœ์ธ์ •๋ณด, ํ† ํฐ์˜ ๋ฐœ๊ธ‰ ์‹œ๊ฐ„ ๋ฐ
// ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๋“ฑ์˜ ์ •๋ณด๋“ค์„ JSON ํ˜•ํƒœ๋กœ ๋‹ด๋Š”๋‹ค.
{
  "sub": "someInformation",
  "name": "phillip",
  "iat": 151623391
}
// ์ด JSON ๊ฐ์ฒด๋ฅผ base64๋กœ ์ธ์ฝ”๋”ฉํ•˜๋ฉด JWT์˜ ๋‘ ๋ฒˆ์งธ ๋ถ€๋ถ„์ธ Payload๊ฐ€ ์™„์„ฑ๋œ๋‹ค.

ย 3. Signatureย 

ํ† ํฐ์˜ ๋ฌด๊ฒฐ์„ฑ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๋ถ€๋ถ„์ด๋‹ค.

Signature๋Š” ์„œ๋ฒ„์˜ ๋น„๋ฐ€ ํ‚ค(์•”ํ˜ธํ™”์— ์ถ”๊ฐ€ํ•  salt)์™€
Header์—์„œ ์ง€์ •ํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ
Header์™€ Payload๋ฅผ ํ•ด์‹ฑํ•œ๋‹ค.

// ๋งŒ์•ฝ HMAC SHA256 ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด Signature๋Š” ์•„๋ž˜์™€ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑ๋œ๋‹ค.
HMACSHA256(base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret);

๐Ÿช™ ์•ก์„ธ์Šค ํ† ํฐ / ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ

๋‹ค์–‘ํ•œ ๋ฐฉ๋ฒ•๋“ค์ด ์žˆ์ง€๋งŒ, ๋Œ€ํ‘œ์ ์œผ๋กœ๋Š”
์•ก์„ธ์Šค ํ† ํฐ๊ณผ ๋ฆฌํ”„๋ ˆ์‹œ ํ† ํฐ์„ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

Access Token
๋ณด์•ˆ์„ ์œ„ํ•ด 24์‹œ๊ฐ„ ์ •๋„์˜ ์งง์€ ์œ ํšจ๊ธฐ๊ฐ„.

Refresh Token
์•ก์„ธ์Šค ํ† ํฐ์ด ๋งŒ๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ƒˆ๋กœ์šด ์•ก์„ธ์Šค ํ† ํฐ์„
๋ฐœ๊ธ‰๋ฐ›๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ํ† ํฐ์ด๋‹ค. (์•ก์„ธ์Šค ํ† ํฐ๋ณด๋‹ค ๊ธด ์œ ํšจ๊ธฐ๊ฐ„)


๐Ÿช™ JWT ์‚ฌ์šฉ๋ฒ•

jsonwebtoken

// npm i jsonwebtoken
// ๊ตฌ์กฐ๋ถ„ํ•ดํ• ๋‹นํ•ด์„œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
const { sign, verify } = require("jsonwebtoken")

// ํ† ํฐ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•
const token = sign(ํ† ํฐ์—_๋‹ด์„_๊ฐ’, process.env.ACCESS_SECRET, { ์˜ต์…˜1: ๊ฐ’, ์˜ต์…˜2: ๊ฐ’, ... });

// ํ† ํฐ ํ•ด๋…ํ•˜๋Š” ๋ฒ• verify(ํ•ด๋…, ๊ฒ€์ฆ)
const token = verify(token, process.env.ACCESS_SECRET);                                             

dotenv

// npm i dotenv
// .env ํŒŒ์ผ ์ž‘์„ฑ, ๊ทธ ์•ˆ์— ํ•„์š”ํ•œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ํ‚ค=๊ฐ’์˜ ํฌ๋ฉง์œผ๋กœ ๋‚˜์—ด.
// dotenv๋ฅผ ์‚ฌ์šฉํ•ด ํ˜„์žฌ ๋””๋ ‰ํ† ๋ฆฌ์— ์œ„์น˜ํ•œ .env ํŒŒ์ผ๋กœ๋ถ€ํ„ฐ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋ฅผ ์ฝ์–ด ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.
require("dotenv").config();
const { sign, verify } = require("jsonwebtoken");

module.exports = {
  generateToken: (user, checkedKeepLogin) => {
    const payload = {
      id: user.id,
      email: user.email,
    };
    let result = {
      // ํ† ํฐ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•
      // const token = sign(ํ† ํฐ์—_๋‹ด์„_๊ฐ’, ACCESS_SECRET, { ์˜ต์…˜1: ๊ฐ’, ์˜ต์…˜2: ๊ฐ’, ... });
      accessToken: sign(payload, process.env.ACCESS_SECRET, {
        expiresIn: "1d", // 1์ผ๊ฐ„ ์œ ํšจํ•œ ํ† ํฐ์„ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค.
      }),
    };

    if (checkedKeepLogin) {
      result.refreshToken = sign(payload, process.env.REFRESH_SECRET, {
        expiresIn: "7d", // ์ผ์ฃผ์ผ๊ฐ„ ์œ ํšจํ•œ ํ† ํฐ์„ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค.
      });
    }
    return result;
  },
  verifyToken: (type, token) => {
    let secretKey, decoded;
    switch (type) {
      case "access":
        secretKey = process.env.ACCESS_SECRET;
        break;
      case "refresh":
        secretKey = process.env.REFRESH_SECRET;
        break;
      default:
        return null;
    }

    // ํ† ํฐ ํ•ด๋…ํ•˜๋Š” ๋ฒ• verify(ํ•ด๋…, ๊ฒ€์ฆ)
    try {
      decoded = verify(token, secretKey);
    } catch (err) {
      console.log(`JWT Error: ${err.message}`);
      return null;
    }
    return decoded;
  },
};



๐Ÿ‘‰ JWT
๐Ÿ‘‰ Base64

profile
๐Ÿพ

0๊ฐœ์˜ ๋Œ“๊ธ€