JWT #3

Dearยท2025๋…„ 8์›” 4์ผ

TIL

๋ชฉ๋ก ๋ณด๊ธฐ
66/74

๐Ÿ’™ UsernamePasswordAuthenticationToken

Spring Security์—์„œ ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๊ฐ์ฒด

Authentication ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์ด๋ฉฐ "ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€", "์–ด๋–ค ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€" ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.

๐Ÿ’š ์ƒ์„ฑ์ž ์ •๋ฆฌ

์ธ์ฆ ์ „ (๋กœ๊ทธ์ธ ์‹œ ์‚ฌ์šฉ)

new UsernamePasswordAuthenticationToken(username, password);
  • ์•„์ง ์ธ์ฆ๋˜์ง€ ์•Š์€ ์ƒํƒœ
  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๋‚ด๋ถ€์—์„œ ๋กœ๊ทธ์ธ ์‹œ๋„ํ•  ๋•Œ ์‚ฌ์šฉ
  • isAuthenticated() โ†’ false

์ธ์ฆ ํ›„ (JWT ๊ฒ€์ฆ ์™„๋ฃŒ ํ›„ ์‚ฌ์šฉ)

new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
  • ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋กœ ์ƒ์„ฑ
  • ์˜ˆ : principal = username, credentials = null, authorities = ROLE_USER
  • isAuthenticated() -> true

isAuthenticated()

Authentication ์ธํ„ฐํŽ˜์ด์Šค์˜ ๋ฉ”์†Œ๋“œ ์ค‘ ํ•˜๋‚˜๋กœ, ํ˜„์žฌ Authentication ๊ฐ์ฒด๊ฐ€ ์ •์ƒ์ ์œผ๋กœ ์ธ์ฆ๋˜์—ˆ๋Š”์ง€(true) ๋˜๋Š” ๊ทธ๋ ‡์ง€ ์•Š์€์ง€(false)๋ฅผ ๋ฐ˜ํ™˜

์ƒํ™ฉ์˜ˆ์‹œisAuthenticated() ๊ฐ’
์ธ์ฆ ์ „ (๋กœ๊ทธ์ธ ์‹œ๋„ ๋‹จ๊ณ„)new UsernamePasswordAuthenticationToken(username, password)false
์ธ์ฆ ํ›„ (JWT ๊ฒ€์ฆ ์™„๋ฃŒ ๋“ฑ)new UsernamePasswordAuthenticationToken(username, null, ๊ถŒํ•œ๋ชฉ๋ก)true
// ๋กœ๊ทธ์ธ ์‹œ๋„ (์ธ์ฆ ์ „)
UsernamePasswordAuthenticationToken unauthenticated =
    new UsernamePasswordAuthenticationToken("user", "1234");

System.out.println(unauthenticated.isAuthenticated());  // false

JWT ์ธ์ฆ ํ›„ (์ธ์ฆ ์™„๋ฃŒ)
List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER"));
UsernamePasswordAuthenticationToken authenticated =
    new UsernamePasswordAuthenticationToken("user", null, authorities);

System.out.println(authenticated.isAuthenticated());  // true

๐Ÿ’™ SimpleGrantedAuthority

GrantedAuthority ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค ์ค‘ ํ•˜๋‚˜๋กœ "์ด ์‚ฌ์šฉ์ž๊ฐ€ ์–ด๋–ค ๊ถŒํ•œ(์—ญํ• )์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๊ฐ€?" ๋ฅผ ๋‚˜ํƒ€๋‚ธ๋‹ค.

์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ(Role)์„ ํ‘œํ˜„ํ• ๋•Œ ์‚ฌ์šฉํ•˜๋Š” ํด๋ž˜์Šค (์˜ˆ : ROLE_USER, ROLE_ADMIN )

์‚ฌ์šฉ ์˜ˆ์‹œ

String role = "USER";

UsernamePasswordAuthenticationToken authentication =
    new UsernamePasswordAuthenticationToken(
        username,
        null,
        Collections.singleton(new SimpleGrantedAuthority("ROLE_" + role))
    );

์—ฌ๊ธฐ์„œ "ROLE_" + role โ†’ "ROLE_USER"
์ด ๊ถŒํ•œ ์ •๋ณด๊ฐ€ Authentication ๊ฐ์ฒด์— ํฌํ•จ๋˜์–ด ์ปจํŠธ๋กค๋Ÿฌ๋‚˜ ๋ณด์•ˆ ์„ค์ •์—์„œ ์‚ฌ์šฉ๋œ๋‹ค.

@PreAuthorize("hasRole('USER')") // ๋‚ด๋ถ€์ ์œผ๋กœ ROLE_USER๋ฅผ ๊ธฐ๋Œ€
public void viewUserData() {
    ...
}

// ๋‘˜ ๋‹ค ์‚ฌ์šฉ ๊ฐ€๋Šฅ

http.authorizeHttpRequests()
    .requestMatchers("/admin/**").hasRole("ADMIN")

๐Ÿ’™ CustomUserDetails

์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ์‚ฌ์šฉ์ž ์ •์˜ ํด๋ž˜์Šค

Spring Security์—์„œ UserDetails๋ฅผ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•ํ•œ ํด๋ž˜์Šค๋กœ Spring Security์—์„œ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•  ๋•Œ UserDetailsService์˜ loadUserByUsername() ๋ฉ”์†Œ๋“œ๋ฅผ ํ†ตํ•ด ๋ฐ˜ํ™˜๋˜๋Š” ๊ฐ์ฒด๋Š” UserDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์—ฌ์•ผ ํ•œ๋‹ค.

UserDetails ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค -> CustomUserDetails
public class CustomUserDetails implements UserDetails

์ฃผ์š” ์—ญํ• 

  • ์‚ฌ์šฉ์ž์˜ ์•„์ด๋””, ๋น„๋ฐ€๋ฒˆํ˜ธ ๊ถŒํ•œ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๋‹ด์Œ
  • Spring Security๊ฐ€ ์ธ์ฆ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ๋•Œ ์ฐธ์กฐ
public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("ROLE_" + user.getRole()));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    // ๊ณ„์ • ๋งŒ๋ฃŒ, ์ž ๊ธˆ ๋“ฑ์˜ ๊ธฐ๋ณธ๊ฐ’ ์ฒ˜๋ฆฌ
    @Override public boolean isAccountNonExpired() { return true; }
    @Override public boolean isAccountNonLocked() { return true; }
    @Override public boolean isCredentialsNonExpired() { return true; }
    @Override public boolean isEnabled() { return true; }

    public User getUser() {
        return user;
    }
}

์‚ฌ์šฉ ํ๋ฆ„

  1. ์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ ์‹œ๋„
  2. UserDetailSErvice๊ฐ€ DB์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด ์กฐํšŒ
  3. CustomerUserDetails๋กœ ๊ฐ์‹ธ์„œ ๋ฐ˜ํ™˜
  4. Spring Security๊ฐ€ ๋‚ด๋ถ€์—์„œ ์ด ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ์ธ์ฆ ์ง„ํ–‰

๐Ÿ’™ SecurityContextHolder

Spring Security์—์„œ ํ˜„์žฌ ์‹คํ–‰ ์ค‘์ธ ์Šค๋ ˆ๋“œ์˜ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ๋ฅผ ์ €์žฅ/์กฐํšŒํ•˜๋Š” ํด๋ž˜์Šค๋กœ ์ธ์ฆ ์ •๋ณด๋ฅผ thread-safeํ•˜๊ฒŒ ์ €์žฅํ•˜๋Š” ์ „์—ญ ์ €์žฅ์†Œ ์—ญํ• ์„ ํ•œ๋‹ค.

public class SecurityContextHolder {
    // ํ˜„์žฌ ์Šค๋ ˆ๋“œ์— ์ธ์ฆ ์ •๋ณด๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ThreadLocal
    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

    public static SecurityContext getContext();
    public static void setContext(SecurityContext context);
    public static void clearContext();
}
  • ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ ๋ฐ”๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ •์ (static) ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค
  • ThreadLocal์„ ์ด์šฉํ•ด, ๊ฐ ์š”์ฒญ(์Šค๋ ˆ๋“œ)๋งˆ๋‹ค ๋…๋ฆฝ์ ์ธ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ €์žฅ

์‚ฌ์šฉ ํ๋ฆ„ (์ธ์ฆ ๊ณผ์ •)

  1. ๋กœ๊ทธ์ธํ•˜๊ฑฐ๋‚˜ JWT ๊ฒ€์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด
  2. Authentication ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ (UsernamePasswordAuthenticationToken)
  3. SecurityContextHolder.getContext().setAuthentication(auth) ํ˜ธ์ถœ
  4. ํ˜„์žฌ ์š”์ฒญ ์Šค๋ ˆ๋“œ์˜ SecurityContext์— ์ธ์ฆ ์ •๋ณด๋ฅผ ์ €์žฅ
  5. ์ดํ›„ ์ปจํŠธ๋กค๋Ÿฌ๋‚˜ ์„œ๋น„์Šค์—์„œ SecurityContextHolder.getContext().getAuthentication()์œผ๋กœ ์ •๋ณด ๊บผ๋ƒ„

์ฃผ์š” ๋ฉ”์„œ๋“œ

  • getContext() : ํ˜„์žฌ ์š”์ฒญ์˜ ์ธ์ฆ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
  • setContext(SecurityContext) : ์ธ์ฆ ์ •๋ณด ์ €์žฅ
  • clearContext() : ์ธ์ฆ ์ •๋ณด ์ œ๊ฑฐ (๋กœ๊ทธ์•„์›ƒ ๋“ฑ)

๐Ÿ’š SecurityContext

  • Spring Security์—์„œ ํ˜„์žฌ ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค

๋ˆ„๊ฐ€ ๋กœ๊ทธ์ธํ–ˆ๋Š”์ง€(Authentication) ์–ด๋–ค ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ฐ์ฒด

// ๋‚ด๋ถ€์—๋Š” ๋‹จ ํ•˜๋‚˜์˜ ํ•„๋“œ๋งŒ ์กด์žฌ 
public interface SecurityContext {
    Authentication getAuthentication();
    void setAuthentication(Authentication authentication);
}

// ๊ตฌํ˜„์ฒด
public class SecurityContextImpl implements SecurityContext, Serializable {
    private Authentication authentication;

    public SecurityContextImpl() {}

    public SecurityContextImpl(Authentication authentication) {
        this.authentication = authentication;
    }

    @Override
    public Authentication getAuthentication() {
        return this.authentication;
    }

    @Override
    public void setAuthentication(Authentication authentication) {
        this.authentication = authentication;
    }
}

// ์ธ์ฆ์ด ์™„๋ฃŒ๋˜๋ฉด
Authentication auth = new UsernamePasswordAuthenticationToken("user", null, ๊ถŒํ•œ);
SecurityContext context = SecurityContextHolder.createEmptyContext(); // ๋นˆ ์ปจํ…์ŠคํŠธ ์ƒ์„ฑ
context.setAuthentication(auth); // ์ธ์ฆ ์ •๋ณด ์ฃผ์ž…
SecurityContextHolder.setContext(context); // SecurityContextHolder์— ์ €์žฅ

์ธ์ฆ์ด ์™„๋ฃŒ๋˜๋ฉด ์–ธ์ œ๋“  SecurityContextHolder.getContext().getAuthentication() ๋กœ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊บผ๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ’š Authentication

ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€, ์ธ์ฆ๋˜์—ˆ๋Š”์ง€, ์–ด๋–ค ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค

Spring Security์—์„œ๋Š” ๋ชจ๋“  ์ธ์ฆ ์ •๋ณด๋ฅผ Authentication ๊ฐ์ฒด ํ•˜๋‚˜์— ๋‹ด๋Š”๋‹ค.
๋กœ๊ทธ์ธํ–ˆ๋Š”์ง€ ์—ฌ๋ถ€, ์‚ฌ์šฉ์ž๋ช…, ๋น„๋ฐ€๋ฒˆํ˜ธ, ๊ถŒํ•œ, ์ธ์ฆ ์ƒํƒœ ๋“ฑ

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // ๊ถŒํ•œ ๋ชฉ๋ก
    Object getCredentials();          // ๋น„๋ฐ€๋ฒˆํ˜ธ ๋“ฑ ์ธ์ฆ ์ˆ˜๋‹จ
    Object getDetails();              // ์š”์ฒญ ๊ด€๋ จ ์ถ”๊ฐ€ ์ •๋ณด (IP, ์„ธ์…˜ ๋“ฑ)
    Object getPrincipal();            // ์‚ฌ์šฉ์ž ์ •๋ณด
    boolean isAuthenticated();        // ์ธ์ฆ ์—ฌ๋ถ€
    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
๋ฉ”์„œ๋“œ์„ค๋ช…์˜ˆ์‹œ
getPrincipal()์‚ฌ์šฉ์ž ์ฃผ์ฒด ์ •๋ณด๋ณดํ†ต username ๋˜๋Š” UserDetails ๊ฐ์ฒด
getCredentials()์ธ์ฆ ์ˆ˜๋‹จ๋น„๋ฐ€๋ฒˆํ˜ธ ๋˜๋Š” null (JWT ์‚ฌ์šฉ ์‹œ)
getAuthorities()๊ถŒํ•œ ๋ชฉ๋กROLE_USER, ROLE_ADMIN ๋“ฑ
isAuthenticated()์ธ์ฆ ์—ฌ๋ถ€๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ true
getDetails()์„ธ๋ถ€ ์ •๋ณดIP, ์„ธ์…˜ ๋“ฑ (์˜ต์…˜)

๋Œ€๋ถ€๋ถ„ ์‚ฌ์šฉํ•˜๋Š” ๊ตฌํ˜„์ฒด

๊ตฌํ˜„์ฒด์šฉ๋„
UsernamePasswordAuthenticationToken๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์ธ์ฆ ๊ฐ์ฒด (ํผ ๋กœ๊ทธ์ธ, JWT ๋“ฑ)
AnonymousAuthenticationToken์ธ์ฆ๋˜์ง€ ์•Š์€ ์‚ฌ์šฉ์ž (์ต๋ช… ์‚ฌ์šฉ์ž ์ฒ˜๋ฆฌ์šฉ)
UsernamePasswordAuthenticationToken authentication =
    new UsernamePasswordAuthenticationToken(
        "user123",                          // principal
        null,                               // credentials (JWT๋ผ null)
        List.of(new SimpleGrantedAuthority("ROLE_USER")) // ๊ถŒํ•œ
    );

authentication.setAuthenticated(true);

// ํ›„์— ์ €์žฅ๋˜๋Š” Authentication ๊ฐ์ฒด
SecurityContextHolder.getContext().setAuthentication(authentication);

์ปจํŠธ๋กค๋Ÿฌ์™€ ์„œ๋น„์Šค์—์„œ ์‚ฌ์šฉ ์˜ˆ์‹œ

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();  // ๋˜๋Š” getPrincipal()

// ์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐ”๋กœ ์ฃผ์ž… ๊ฐ€๋Šฅ
@GetMapping("/me")
public ResponseEntity<?> getMyInfo(Authentication auth){
	return ResponseEntity.ok(auth.getName());
}

๐Ÿ’š ์ธ์ฆ ์ •๋ณด ๊ณ„์ธต๋ณ„ ์ฑ…์ž„

๊ณ„์ธตํด๋ž˜์Šค์ฑ…์ž„
1๏ธโƒฃ ์‚ฌ์šฉ์ž ์ •๋ณดAuthentication์‚ฌ์šฉ์ž ID, ์ธ์ฆ ์—ฌ๋ถ€, ๊ถŒํ•œ ๋“ฑ ์‹ค์ œ ์ธ์ฆ ์ •๋ณด
2๏ธโƒฃ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธSecurityContextํ˜„์žฌ ์š”์ฒญ์˜ ์ธ์ฆ ์ƒํƒœ๋ฅผ ๋‹ด๊ณ  ์žˆ์Œ (Authentication ๊ฐ์ฒด 1๊ฐœ๋งŒ ๋ณด๊ด€)
3๏ธโƒฃ ์š”์ฒญ ์Šค๋ ˆ๋“œ ์ €์žฅ์†ŒSecurityContextHolderํ˜„์žฌ ์š”์ฒญ(์Šค๋ ˆ๋“œ)์˜ SecurityContext๋ฅผ ์ €์žฅ/๊ด€๋ฆฌ (์ •์  ThreadLocal ์‚ฌ์šฉ)

๐Ÿ’™ @EnableMethodSecurity

๋ฉ”์†Œ๋“œ ๋‹จ์œ„์—์„œ @PreAuthorize, @PostAuthorize ๊ฐ™์€ ์–ด๋…ธํ…Œ์ด์…˜์„ ์‚ฌ์šฉํ•˜๋„๋ก ๋ฉ”์†Œ๋“œ ๋ณด์•ˆ ํ™œ์„ฑํ™”

๐Ÿ’™ Spring Security + JWT ์ธ์ฆ ํ๋ฆ„

 ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ (ex. /user/profile)
       โ†“
 1. Spring Security Filter Chain ์‹œ์ž‘
       โ†“
 2. JwtFilter ์‹คํ–‰
    - Authorization ํ—ค๋”์—์„œ ํ† ํฐ ์ถ”์ถœ
    - ํ† ํฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (JwtUtil)
    - ์‚ฌ์šฉ์ž ์ •๋ณด(username, role ๋“ฑ) ์ถ”์ถœ
    - Authentication ๊ฐ์ฒด ์ƒ์„ฑ
    - SecurityContextHolder์— ์ €์žฅ
       โ†“
 3. ๋‹ค์Œ ํ•„ํ„ฐ๋“ค (Spring ๋‚ด๋ถ€ ์ธ์ฆ/์ธ๊ฐ€ ํ•„ํ„ฐ๋“ค)
    - SecurityContextHolder์— Authentication ์žˆ์œผ๋ฉด
      ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋กœ ๊ฐ„์ฃผ
       โ†“
 4. ์ปจํŠธ๋กค๋Ÿฌ ๋„๋‹ฌ (@PreAuthorize ๋“ฑ๋„ ์ ์šฉ ๊ฐ€๋Šฅ)
       โ†“
 5. ์š”์ฒญ ์ฒ˜๋ฆฌ ๋ฐ ์‘๋‹ต ๋ฐ˜ํ™˜

์š”์ฒญ ํ๋ฆ„๋„

์‚ฌ์šฉ์ž๊ฐ€ /user/profile ์š”์ฒญ (Authorization: Bearer abc.def.ghi)
                    โ†“
[SecurityFilterChain] (filterChain()์—์„œ ๊ตฌ์„ฑ๋จ)
                    โ†“
[JwtFilter]
    - Authorization ํ—ค๋”์—์„œ ํ† ํฐ ๊บผ๋ƒ„
    - ์œ ํšจํ•œ ํ† ํฐ์ธ์ง€ ๊ฒ€์‚ฌ (jwtUtil.validateToken)
    - ํ† ํฐ์—์„œ username, role ์ถ”์ถœ
    - ์ธ์ฆ ๊ฐ์ฒด ์ƒ์„ฑํ•ด์„œ SecurityContextHolder์— ์ €์žฅ
                    โ†“
[UsernamePasswordAuthenticationFilter] ๋“ฑ ๋‹ค์Œ ํ•„ํ„ฐ๋“ค
    - SecurityContextHolder์— ์ธ์ฆ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ํ†ต๊ณผ
                    โ†“
[Controller (@GetMapping("/user/profile"))]
    - ROLE_USER or ROLE_ADMIN ํ™•์ธ ํ›„ ์š”์ฒญ ์ฒ˜๋ฆฌ
                    โ†“
[์‘๋‹ต ๋ฐ˜ํ™˜]
profile
์นœ์• ํ•˜๋Š” ๊ฐœ๋ฐœ์ž

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