๐ŸŒฑ Spring Security

Dohyeon Kongยท2024๋…„ 7์›” 10์ผ
0

Spring๐ŸŒฑ

๋ชฉ๋ก ๋ณด๊ธฐ
10/11
post-thumbnail

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ(Spring Security)๋ž€?

Spring Security๋ž€ ์Šคํ”„๋ง ๊ธฐ๋ฐ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ(์ธ์ฆ, ์ธ๊ฐ€, ๊ถŒํ•œ)์„ ๋‹ด๋‹นํ•˜๋Š” ์Šคํ”„๋ง ํ•˜์œ„ ํ”„๋ ˆ์ž„์›Œํฌ์ด๋‹ค.

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์ดํ•ดํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization)์— ๋Œ€ํ•˜์—ฌ ์•Œ์•„์•ผ ํ•œ๋‹ค.

์ธ์ฆ(Authentication)๊ณผ ์ธ๊ฐ€(Authorization)

์ธ์ฆ(Authentication)

์ธ์ฆ(Authenticaion)์€ ์‚ฌ์šฉ์ž์˜ ์‹ ์›์„ ์ž…์ฆํ•˜๋Š” ๊ณผ์ •

  • ์‚ฌ์šฉ์ž๊ฐ€ ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธ์„ ํ•  ๋•Œ ๋ˆ„๊ตฌ์ธ์ง€ ํ™•์ธํ•˜๋Š” ๊ณผ์ •์ด ์ธ์ฆ์˜ ๋Œ€ํ‘œ์ ์ธ ์˜ˆ์ด๋‹ค.

์ธ๊ฐ€(Authorization)

์ธ๊ฐ€(Authorization)๋Š” ์‚ฌ์ดํŠธ์˜ ํŠน์ • ๋ถ€๋ถ„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๊ถŒํ•œ์„ ํ™•์ธํ•˜๋Š” ์ž‘์—…

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

์ด๋Ÿฌํ•œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” CSRF ๊ณต๊ฒฉ(์‚ฌ์šฉ์ž์˜ ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ํŠน์ • ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•˜๋„๋ก ์œ ๋„ํ•˜๋Š” ๊ณต๊ฒฉ), ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ(์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ํƒˆ์ทจํ•˜๊ฑฐ๋‚˜ ๋ณ€์กฐํ•˜๋Š” ๊ณต๊ฒฉ)์„ ๋ฐฉ์–ดํ•ด์ค€๋‹ค.


ํ•„ํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•˜๋Š” ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๐Ÿง‘๐Ÿปโ€โš–๏ธ

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ํ•„ํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.
  • ๋‹ค์Œ ์ฐธ๊ณ  ์‚ฌ์ง„์€ ์Šคํ”„๋ง์‹œํ๋ฆฌํ‹ฐ์˜ ํ•„ํ„ฐ ๊ตฌ์กฐ์— ๋Œ€ํ•ด ๋‚˜ํƒ€๋‚ธ ๊ตฌ์กฐ๋„์ด๋‹ค! ์œ ์‹ฌํžˆ ๊ด€์ฐฐํ•ด๋ณด์ž๐Ÿค“

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ๋‹ค์–‘ํ•œ ํ•„ํ„ฐ๋“ค๋กœ ์ด๋ฃจ์–ด์ ธ ์žˆ์œผ๋ฉฐ, ๊ฐ ํ•„ํ„ฐ์—์„œ ์ธ์ฆ๊ณผ ์ธ๊ฐ€์™€ ๊ด€๋ จ๋œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.
  • SecurityContextPersistenceFilter๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด์„œ ์•„๋ž˜๋กœ ๋‚ด๋ ค๊ฐ€๋ฉฐ FilterSecurityInterceptor๊นŒ์ง€ ์ˆœ์„œ๋Œ€๋กœ ํ•„ํ„ฐ๋ฅผ ๊ฑฐ์น˜๊ฒŒ ๋œ๋‹ค.
  • ํ•„ํ„ฐ๊ฐ€ ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ๋นจ๊ฐ„์ƒ‰ ํ™”์‚ดํ‘œ๋กœ ์—ฐ๊ฒฐ๋œ ์˜ค๋ฅธ์ชฝ ๋ฐ•์Šค์˜ ํด๋ž˜์Šค๋ฅผ ๊ฑฐ์น˜๋ฉฐ ์‹คํ–‰์ด ๋œ๋‹ค.
  • ํŠน์ • ํ•„ํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๊ฑฐ๋‚˜ ํ•„ํ„ฐ ๋’ค์— ์ปค์Šคํ…€ ํ•„ํ„ฐ๋ฅผ ๋„ฃ๋Š” ๋“ฑ์˜ ์„ค์ •๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

์šฐ๋ฆฌ๋Š” ์—ฌ๊ธฐ์„œ UsernamePasswordAuthenticationFilter์™€ FilterSecurityInterceptor๋ฅผ ์ง‘์ค‘ํ•ด์„œ ๋ณด์•„์•ผ ํ•œ๋‹ค.

UsernamePasswordAuthenticationFilter

์•„์ด๋””์™€ ํŒจ์Šค์›Œ๋“œ๊ฐ€ ๋„˜์–ด์˜ค๋ฉด ์ธ์ฆ ์š”์ฒญ์„ ์œ„์ž„ํ•˜๋Š” ์ธ์ฆ ๊ด€๋ฆฌ์ž ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  • ํผ ๊ธฐ๋ฐ˜์˜ ๋กœ๊ทธ์ธ์„ ์ˆ˜ํ–‰ํ•  ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ํ•„ํ„ฐ๋กœ ์•„์ด๋””, ํŒจ์‹œ์›Œ๋“œ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์‹ฑ(Parsing)ํ•ด ์ธ์ฆ ์š”์ฒญ์„ ์œ„์ž„ํ•œ๋‹ค.
  • ์ธ์ฆ์ด ์„ฑ๊ณตํ•˜๋ฉด AuthenticationSuccessHandler๋ฅผ, ์ธ์ฆ์— ์‹คํŒจํ•˜๋ฉด AuthenticationFailureHandler๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

FilterSecurityInterceptor

๊ถŒํ•œ ๋ถ€์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•˜์—ฌ ์ ‘๊ทผ ์ œ์–ด ๊ฒฐ์ •์„ ์‰ฝ๊ฒŒ ํ•˜๋Š” ์ ‘๊ทผ ๊ฒฐ์ • ๊ด€๋ฆฌ์ž ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•œ๋‹ค.

  • AccessDecisionManager๋กœ ๊ถŒํ•œ ๋ถ€์—ฌ ์ฒ˜๋ฆฌ๋ฅผ ์œ„์ž„ํ•จ์œผ๋กœ์จ ์ ‘๊ทผ ์ œ์–ด ๊ฒฐ์ •์„ ์‰ฝ๊ฒŒ ํ•ด์ค€๋‹ค.
  • ์ด ๊ณผ์ •์—์„œ๋Š” ์ด๋ฏธ ์‚ฌ์šฉ์ž๊ฐ€ ์ธ์ฆ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์œ ํšจํ•œ ์‚ฌ์šฉ์ž์ธ์ง€ ์•„๋‹Œ์ง€ ์•Œ ์ˆ˜ ์žˆ๋‹ค. ์ฆ‰, ์ธ๊ฐ€ ๊ด€๋ จ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

ํผ ๋กœ๊ทธ์ธ ๊ธฐ๋ฐ˜ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ์ธ์ฆ ์ฒ˜๋ฆฌ ํ๋ฆ„

๋‹ค์Œ ์ ˆ์ฐจ๋ฅผ ์ฐจ๊ทผํžˆ ๋”ฐ๋ผ์™€ ๋ณด์ž๐Ÿฆพ

  1. ์‚ฌ์šฉ์ž๊ฐ€ ํผ์— ์•„์ด๋””์™€ ํŒจ์Šค์›Œ๋“œ๋ฅผ ์ž…๋ ฅํ•˜๋ฉด, HTTPServletRequest์— ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ ์ •๋ณด๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค. ์ด๋•Œ AuthenticationFilter๊ฐ€ ๋„˜์–ด์˜จ ์•„์ด๋””์™€ ๋น„๋ฐ€๋ฒˆํ˜ธ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์ง„ํ–‰ํ•œ๋‹ค.
  2. ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๊ฐ€ ๋๋‚˜๋ฉด์‹ค์ œ ๊ตฌํ˜„์ฒด์ธ UsernamePasswordAuthenticationToken์„ ๋งŒ๋“ค์–ด ๋„˜๊ฒจ์ค€๋‹ค.
  3. ์ „๋‹ฌ๋ฐ›์€ ์ธ์ฆ์šฉ ๊ฐ์ฒด์ธ UsernamePasswordAuthenticationToken์„ AuthenticationManager์—๊ฒŒ ๋ณด๋‚ธ๋‹ค.
  4. UsernamePasswordAuthenticationToken์„ AuthenticationProvider์— ๋ณด๋‚ธ๋‹ค.
  5. ์‚ฌ์šฉ์ž ์•„์ด๋””๋ฅผ UserDetailService์— ๋ณด๋‚ธ๋‹ค. UserDetailService๋Š” ์‚ฌ์šฉ์ž ์•„์ด๋””๋กœ ์ฐพ์€ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ UserDetails ๊ฐ์ฒด๋กœ ๋งŒ๋“ค์–ด AuthenticationProvider์—๊ฒŒ ์ „๋‹ฌํ•œ๋‹ค.
  6. DB์— ์žˆ๋Š”์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค.
  7. ์ž…๋ ฅ ์ •๋ณด์™€ UserDetails์˜ ์ •๋ณด๋ฅผ ๋น„๊ตํ•ด ์‹ค์ œ ์ธ์ฆ ์ฒ˜๋ฆฌ๋ฅผ ํ•œ๋‹ค.
  8. ~ 10.๊นŒ์ง€ ์ธ์ฆ์ด ์™„๋ฃŒ๋˜๋ฉด SecurityContextHolder์— Authentication์„ ์ €์žฅํ•œ๋‹ค. ์ธ์ฆ ์„ฑ๊ณต ์—ฌ๋ถ€์— ๋”ฐ๋ผ ์„ฑ๊ณตํ•˜๋ฉด AuthenticationSucessHandler, ์‹คํŒจํ•˜๋ฉด AuthenticationFailureHandler๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

Spring Security ๊ตฌํ˜„ ์ฝ”๋“œ

  • ์ด๋ฒˆ ์‹ค์Šต์€ Form์„ ์ด์šฉํ•ด์„œ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ์„ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ์ด๋‹ค.

1. ์˜์กด์„ฑ ์ถ”๊ฐ€ํ•˜๊ธฐ(build.gradle)

dependencies{
	// ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์Šคํƒ€ํ„ฐ ์ถ”๊ฐ€
	implementation 'org.springframework.boot:spring-boot-starter-security'
    // ํƒ€์ž„๋ฆฌํ”„์—์„œ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•œ ์˜์กด์„ฑ ์ถ”๊ฐ€
	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
	// ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋ฅผ ํ…Œ์‹œํŠธํ•˜๊ธฐ ์œ„ํ•œ ์˜์กด์„ฑ ์ถ”๊ฐ€
	testImplementation 'org.springframework.security:spring-security-test'
}

2. ์—”ํ‹ฐํ‹ฐ ๋งŒ๋“ค๊ธฐ

package me.shinsunyoung.springbootdeveloper.domain;

import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Table(name = "users")
@Builder
public class User implements UserDetails { // UserDetails๋ฅผ ์ƒ์†๋ฐ›์•„ ์ธ์ฆ ๊ฐ์ฒด๋กœ ์‚ฌ์šฉ
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "email", nullable = false, unique = true)
    private String email;
    @Column(name = "password")
    private String password;

    @Builder
    public User(Long id, String email, String password) {
        this.id = id;
        this.email = email;
        this.password = password;
    }

    @Override // ๊ถŒํ•œ ๋ฐ˜ํ™˜
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    // ์‚ฌ์šฉ์ž์˜ id๋ฅผ ๋ฐ˜ํ™˜(๊ณ ์œ ํ•œ ๊ฐ’)
    @Override
    public String getUsername() {
        return email;
    }

    // ์‚ฌ์šฉ์ž์˜ ํŒจ์Šค์›Œ๋“œ ๋ฐ˜ํ™˜
    @Override
    public String getPassword(){
        return password;
    }
    // ๊ณ„์ • ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜
    @Override
    public boolean isAccountNonExpired() {
        return true; // true๋ฉด ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜์Œ
    }

    // ๊ณ„์ฉก ์ž ๊ธˆ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜
    @Override
    public boolean isAccountNonLocked() {
        return true; // true๋ฉด ๊ณ„์ • ์ž ๊ธˆ X
    }

    // ํŒจ์Šค์›Œ๋“œ์˜ ๋งŒ๋ฃŒ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    // ๊ณ„์ • ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ๋ฐ˜ํ™˜
    @Override
    public boolean isEnabled() {
        return true;
    }
}
  • ์œ ์˜ํ•ด์•ผํ•  ์ ์€ UserDetails๋ฅผ ๊ผญ implements ๋ฐ›์•„์„œ ๊ตฌํ˜„ํ•˜์—ฌ์•ผ ํ•œ๋‹ค.

UserDetail ์ธํ„ฐํŽ˜์ด์Šค์— ์„ ์–ธ๋œ ๋ฉ”์„œ๋“œ๐Ÿ“’

๋ฉ”์„œ๋“œ๋ฐ˜ํ™˜ ํƒ€์ž…์„ค๋ช…
getAuthoritiesCollection< ? extends GrantedAuthority>์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๊ถŒํ•œ์˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
getUsername()String์‚ฌ์šฉ์ž๋ฅผ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋Š” ์‚ฌ์šฉ์ž ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด๋•Œ ์‚ฌ์šฉ๋˜๋Š” ์‚ฌ์šฉ์ž์˜ ์ด๋ฆ„์€ ๋ฐ˜๋“œ์‹œ ๊ณ ์œ ํ•ด์•ผ ํ•œ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” email์ด ๊ณ ์œ  ์ฝ”๋“œ์ด๋‹ค.
getPassword()String์‚ฌ์šฉ์ž์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. ์ด๋•Œ ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ์•”ํ˜ธํ™”ํ•ด์„œ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค.
isAccountNonExpired()boolean๊ณ„์ •์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค. ๋งŒ์•ฝ ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜๋”ฐ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
isAccountNonLocked()boolean๊ณ„์ •์ด ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค. ๋งŒ์•ฝ ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜๋”ฐ๋ฉด true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
isCredetialsNonExpired()boolean๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค. ๋งŒ์•ฝ ๋งŒ๋ฃŒ๋˜์ง€ ์•Š์•˜์„ ๋•Œ๋Š” true๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
isEnabled()boolean๊ณ„์ •์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•˜๋Š” ๋ฉ”์„œ๋“œ์ด๋‹ค. ๋งŒ์•ฝ ์‚ฌ์š” ๊ฐ€๋Šฅํ•˜๋‹ค๋ฉด true ๋ฐ˜ํ™˜ํ•˜๋‹ค.

3. ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ ๋งŒ๋“ค๊ธฐ

  • User ์—”ํ‹ฐํ‹ฐ์— ๋Œ€ํ•œ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋งŒ๋“ค๊ธฐ
package me.shinsunyoung.springbootdeveloper.repository;

import me.shinsunyoung.springbootdeveloper.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
}

4. ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

  • ์—”ํ‹ฐํ‹ฐ์™€ ๋ฆฌํฌ์ง€ํ„ฐ๋ฆฌ๊ฐ€ ์™„์„ฑ๋˜์—ˆ์œผ๋‹ˆ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•  ๋•Œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ ์ž‘์„ฑ
package me.shinsunyoung.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.repository.UserRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
// ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ์ธํ„ฐํŽ˜์ด์Šค
public class UserDetailService implements UserDetailsService {
    private final UserRepository userRepository;
    
    // ์‚ฌ์šฉ์ž ์ด๋ฆ„(email)์œผ๋กœ ์‚ฌ์š”์•š์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฉ”์„œ๋“œ
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException(email));
    }
}
  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” UserDetailsService ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•œ๋‹ค.
  • loadUserByUsername() ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•ด์„œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง ์ž‘์„ฑ

5. ์‹œํ๋ฆฌํ‹ฐ ์„ค์ •ํ•˜๊ธฐ

  • ์‹ค์ œ ์ธ์ฆ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋Š” ์‹œํ๋ฆฌํ‹ฐ ์„ค์ • ํŒŒ์ผ WebSecurityConfigํŒŒ์ผ์„ ์ž‘์„ฑํ•œ๋‹ค.
package me.shinsunyoung.springbootdeveloper.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
    private final UserDetailsService userService;

    @Bean // 1. ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ ๊ธฐ๋Šฅ ๋น„ํ™œ์„ฑํ™”
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(new AntPathRequestMatcher("/static/**"));
    }

    @Bean // 2. ํŠน์ • HTTP ์š”์ฒญ์— ๋Œ€ํ•œ ์›น ๊ธฐ๋ฐ˜ ๋ณด์•ˆ ๊ตฌ์„ฑ
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth // 3. ์ธ์ฆ, ์ธ๊ฐ€ ์„ค์ •
                        .requestMatchers(
                                new AntPathRequestMatcher("/login"),
                                new AntPathRequestMatcher("/signup"),
                                new AntPathRequestMatcher("/user")
                        ).permitAll()
                        .anyRequest().authenticated())
                .formLogin(formLogin -> formLogin // 4. ํผ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ ์„ค์ •
                        .loginPage("/login")
                        .defaultSuccessUrl("/articles")
                )
                .logout(logout -> logout.logoutSuccessUrl("/login") // 5. ๋กœ๊ทธ์•„์›ƒ ์„ค์ •
                        .invalidateHttpSession(true)
                )
                .csrf(AbstractHttpConfigurer::disable) // 6. CSRF ๋น„ํ™œ์„ฑํ™”
                .build();
    }
    // 7. ์ธ์ฆ ๊ด€๋ฆฌ์ž ๊ด€๋ จ ์„ค์ •
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailsService userDetailsService) throws Exception {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
        authProvider.setUserDetailsService(userService);
	// 8. ์‚ฌ์šฉ์ž ์ •๋ณด ์„œ๋น„์Šค๋ฅผ ์„ค์ •
        authProvider.setPasswordEncoder(bCryptPasswordEncoder);
        return new ProviderManager(authProvider);
    }
	// 9. ํŒจ์Šค์›Œ๋“œ ์ธ์ฝ”๋”๋กœ ์‚ฌ์šฉํ•  ๋นˆ ๋“ฑ๋ก
    @Bean 
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

๋ฒˆํ˜ธ์— ๋Œ€ํ•œ ์„ค๋ช…

  • 1๋ฒˆ : ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. ํ•ด๋‹น ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•œ ์ด์œ ๋Š” ์ธ์ฆ๊ณผ ์ธ๊ฐ€ ์„œ๋น„์Šค๋ฅผ ๋ชจ๋“  ๊ณณ์— ์ ์šฉํ•˜์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ static resources์— ์„ค์ •ํ•œ๋‹ค.
  • 2๋ฒˆ : ํŠน์ • HTTP ์š”์ฒญ์— ๋Œ€ํ•ด ์›น ๊ธฐ๋ฐ˜ ๋ณด์•ˆ์„ ๊ตฌ์„ฑํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. ์ด ๋ฉ”์„œ๋“œ์—์„œ ์ธ์ฆ/์ธ๊ฐ€ ๋ฐ ๋กœ๊ทธ์ธ, ๋กœ๊ทธ์•„์›ƒ ๊ด€๋ จ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ๋‹ค.
  • 3๋ฒˆ : ํŠน์ • ๊ฒฝ๋กœ์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์„ค์ •ํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค.
    - requestMatchers() : ํŠน์ • ์š”์ฒญ๊ณผ ์ผ์น˜ํ•˜๋Š” url์— ๋Œ€ํ•œ ์•ก์„ธ์Šค๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    - permitAll() : ๋ˆ„๊ตฌ๋‚˜ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๊ฒŒ ์„ค์ •ํ•œ๋‹ค. ์ฆ‰, "/login", "/signup", "/user" ๋กœ ์š”์ฒญ์ด ์˜ค๋ฉด ์ธ์ฆ/์ธ๊ฐ€ ์—†์ด๋„ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
    - anyRequest() : ์œ„์—์„œ ์„ค์ •ํ•œ url์ด์™ธ์˜ ์š”์ฒญ์— ๋Œ€ํ•ด ์„ค์ •ํ•œ๋‹ค.
    - authenticated() : ๋ณ„๋„์˜ ์ธ๊ฐ€๋Š” ํ•„์š”ํ•˜์ง€ ์•Š์ง€๋งŒ ์ธ์ฆ์ด ์„ฑ๊ณต๋œ ์ƒํƒœ์—ฌ์•ผ ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.
  • 4๋ฒˆ : ํผ ๊ธฐ๋ฐ˜ ๋กœ๊ทธ์ธ ์„ค์ •์„ ์ง„ํ–‰ํ•œ๋‹ค.
    - loginPage() : ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    - defaultSucessUrl() : ๋กœ๊ทธ์ธ์ด ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ด๋™ํ•  ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • 5๋ฒˆ : ๋กœ๊ทธ์•„์›ƒ ์„ค์ •์„ ์ง„ํ–‰ํ•œ๋‹ค.
    - logoutSuccessUrl() : ๋กœ๊ทธ์•„์›ƒ์ด ์™„๋ฃŒ๋˜์—ˆ์„ ๋•Œ ์ด๋™ํ•  ๊ฒฝ๋กœ๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    - invalidateHttpSession() : ๋กœ๊ทธ์•„์›ƒ ์ดํ›„์— ์„ธ์…˜์„ ์ „์ฒด ์‚ญ์ œํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • 6๋ฒˆ : CSRF ์„ค์ •์„ ๋น„ํ™œ์„ฑํ™”ํ•œ๋‹ค. CSRF ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ํ™œ์„ฑํ™”๋ฅผ ํ•ด์•ผํ•œ๋‹ค.
  • 7๋ฒˆ : ์ธ์ฆ ๊ด€๋ฆฌ์ž ๊ด€๋ จ ์„ค์ •์„ ์ง„ํ–‰ํ•œ๋‹ค. ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์„œ๋น„์‹œ๋ฅผ ์žฌ์ •์˜ํ•˜๊ฑฐ๋‚˜, ์ธ์ฆ ๋ฐฉ๋ฒ•, ์˜ˆ๋ฅผ ๋“ค์–ด JDBC ๊ธฐ๋ฐ˜ ์ธ ๋“ฑ์„ ์„ค์ •ํ•œ๋‹ค.
  • 8๋ฒˆ : ์‚ฌ์šฉ์ž ์„œ๋น„์Šค๋ฅผ ์„ค์ •ํ•œ๋‹ค.
    - userDetailsService() : ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ์„œ๋น„์Šค๋ฅผ ์„ค์ •ํ•œ๋‹ค. ์ด๋•Œ ์„ค์ •ํ•˜๋Š” ์„œ๋น„์Šค ํด๋ž˜์Šค๋Š” UserDetailsService๋ฅผ ์ƒ์†๋ฐ›์€ ํด๋ž˜์Šค์—ฌ์•ผ ํ•œ๋‹ค.
    - passwrodEncoder() : ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์•”ํ˜ธํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์ธ์ฝ”๋”๋ฅผ ์„ค์ •ํ•œ๋‹ค.
  • 9๋ฒˆ : ํŒจ์Šค์›Œ๋“œ ์ธ์ฝ”๋”๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•œ๋‹ค.

6. ํšŒ์› ๊ฐ€์ž… ๊ตฌํ˜„ํ•˜๊ธฐ

6-1. ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

package me.shinsunyoung.springbootdeveloper.dto;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class AddUserRequest {
    private String email;
    private String password;
}
  • ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ๋Š” ๊ฐ์ฒด๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.
package me.shinsunyoung.springbootdeveloper.service;

import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.domain.User;
import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest;
import me.shinsunyoung.springbootdeveloper.repository.UserRepository;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    public Long save(AddUserRequest dto){
        return userRepository.save(User.builder()
                .email(dto.getEmail())
                // ํŒจ์Šค์›Œ๋“œ ์•”ํ˜ธํ™”
                .password(bCryptPasswordEncoder.encode(dto.getPassword()))
                .build()).getId();
    }
}
  • AddUserRequest ๊ฐ์ฒด๋ฅผ ์ธ์ˆ˜๋กœ ๋ฐ›๋Š” ํšŒ์› ์ •๋ณด ์ถ”๊ฐ€ ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค. UserService๋ฅผ ๋งŒ๋“ ๋‹ค.

6-2 ์ปจํŠธ๋กค๋Ÿฌ ์ฝ”๋“œ ์ž‘์„ฑํ•˜๊ธฐ

package me.shinsunyoung.springbootdeveloper.controller;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import me.shinsunyoung.springbootdeveloper.dto.AddUserRequest;
import me.shinsunyoung.springbootdeveloper.service.UserService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@RequiredArgsConstructor
@Controller
public class UserApiController {
    private final UserService userService;

    @PostMapping("/user")
    public String signup(AddUserRequest request) {
        userService.save(request); // ํšŒ์› ๊ฐ€์ž… ๋ฉ”์„œ๋“œ ํ˜ธ์ถœ
        return "redirect:/login"; // ํšŒ์› ๊ฐ€์ž…์ด ์™„๋ฃŒ๋œ ์ดํ›„์— ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค.
    }

    @GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        return "redirect:/login";
    }
}
  • ํšŒ์› ๊ฐ€์ž… ํผ์—์„œ ํšŒ์› ๊ฐ€์ž…์š”์ฒญ์„ ๋ฐ›์œผ๋ฉด ์„œ๋น„์Šค ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๋ฅผ ์ €์žฅํ•œ๋’ค ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ redirectํ•˜๋Š” ๋ฉ”์„œ๋“œ๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

์‹ค์ œ ๊ตฌํ˜„ํ•œ ์ฝ”๋“œ์˜ ์‹คํ–‰ ๊ฒฐ๊ณผ๐Ÿ”ซ

http://localhost:8080/articles๋กœ ์ ‘๊ทผํ•˜๊ฒŒ ๋˜๋ฉด articles๋Š” ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๋งŒ ๋“ค์–ด๊ฐˆ ์ˆ˜ ์žˆ๋Š” ํŽ˜์ด์ง€์ด๋ฏ€๋กœ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ redirect ๋œ๋‹ค.

ํšŒ์›๊ฐ€์ž… ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ฒŒ ๋˜๋ฉด ํšŒ์› ๊ฐ€์ž… ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ณ  ํšŒ์› ๊ฐ€์ž… ํŽ˜์ด์ง€๋Š” permitAll() ๋ฉ”์„œ๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— ๋ณ„๋„ ์ธ์ฆ ์—†์ด ์ ‘๊ทผ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์•”ํ˜ธํ™”๋˜์–ด ์ €์žฅ๋˜์–ด์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค..

ํšŒ์›๊ฐ€์ž…์„ ์ง„ํ–‰ํ•˜๊ฒŒ ๋˜๋ฉด ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ฒŒ ๋˜๊ณ  ๊ฐ€์ž…ํ•œ ์ด๋ฉ”์ผ๊ณผ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•˜์—ฌ ๋กœ๊ทธ์ธ์„ ์ง„ํ–‰ํ•œ๋‹ค.

์ •์ƒ์ ์œผ๋กœ ๋กœ๊ทธ์ธ์ด ์„ฑ๊ณตํ•˜๋ฉด ๊ธ€ ๋ชฉ๋ก ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•˜๊ฒŒ ๋œ๋‹ค.

๋กœ๊ทธ์•„์›ƒ ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๊ฒŒ ๋””๋ฉด ์ธ์ฆ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฏ€๋กœ ๋‹ค์‹œ ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ์ด๋™ํ•œ๋‹ค.


๊ทธ๋Ÿผ ์–ด๋””์— Spring Security๊ฐ€ ์‚ฌ์šฉ์ด ๋ ๊นŒ?๐Ÿง

์ „์ฒด ํ”Œ๋žซํผ์˜ ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ๊ณผ ํŠน์ • ํŽ˜์ด์ง€์— ๋Œ€ํ•œ ์ ‘๊ทผ์„ ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ๋งŒ ํ•ด์ค„ ๋•Œ ์œ ์šฉํ•˜๊ฒŒ ์‚ฌ์šฉ๋  ๊ฒƒ ๊ฐ™๋‹ค.


์ •๋ฆฌ

์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ์Šคํ”„๋ง ๊ธฐ๋ฐ˜์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ(์ธ์ฆ, ์ธ๊ฐ€, ๊ถŒํ•œ)์„ ๋‹ด๋‹นํ•˜๋Š” ์Šคํ”„๋ง ํ•˜์œ„ ํ”„๋ ˆ์ž„์›Œํฌ์ด๋ฉฐ ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ๋Š” ํ•„ํ„ฐ ๊ธฐ๋ฐ˜์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค.
๊ฐ ํ•„ํ„ฐ์—์„œ๋Š” ์ธ์ฆ๊ณผ ์ธ๊ฐ€์™€ ๊ด€๋ จ๋œ ์ž‘์—…์„ ์ฒ˜๋ฆฌํ•˜๊ณ  ๊ธฐ๋ณธ์ ์œผ๋กœ ์„ธ์…˜๊ณผ ์ฟ ๊ธฐ ๋ฐฉ์‹์œผ๋กœ ์ธ์ฆ์„ ์ฒ˜๋ฆฌํ•œ๋‹ค.

  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ๋Š” ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ๊ณผ์ธ๊ฐ€ ์ •๋ณด๋ฅผ UserDetails ๊ฐ์ฒด์— ๋‹ด๋Š”๋‹ค. ์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์€ ๋’ค ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•ด ์‚ฌ์šฉํ•˜๋ฉด ๋œ๋‹ค.
  • ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‚ฌ์šฉํ•˜๋Š” UserDetailService๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์€ ๋’ค loadUserByUsename()์„ ์˜ค๋ฒ„๋ผ์ด๋“œํ•˜๋ฉด ์Šคํ”„๋ง ์‹œํ๋ฆฌํ‹ฐ์—์„œ ์‚ฌ์šฉ์ž์˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ ์˜ค๋ฒ„๋ผ์ด๋“œ๋œ ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

์ฐธ๊ณ  ๋ฌธํ—Œ ๋ฐ ๊ต์žฌ๐Ÿ“’

http://atin.tistory.com/590?pidx=0
https://taetoungs-branch.tistory.com/204?pidx=1
์Šคํ”„๋ง ๋ถ€ํŠธ 3 ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž ๋˜๊ธฐ: ์ž๋ฐ” ํŽธ - ์‹ ์„ ์˜ -

profile
์ฒœ์ฒœํžˆ, ๊พธ์ค€ํžˆ, ๊ทธ๋ฆฌ๊ณ  ๋๊นŒ์ง€

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

comment-user-thumbnail
2024๋…„ 7์›” 11์ผ

spring security ๋งˆ์Šคํ„ฐํ–ˆ๋„ค... ๋•๋ถ„์— ์ข‹์€ ์ธ์‚ฌ์ดํŠธ ์–ป๊ณ  ๊ฐ‘๋‹ˆ๋‹ค!

๋‹ต๊ธ€ ๋‹ฌ๊ธฐ