[SpringBoot] ๐Ÿ”’ Spring Security

์œค๊ฒฝยท2021๋…„ 11์›” 2์ผ
0

Spring Boot

๋ชฉ๋ก ๋ณด๊ธฐ
55/79


Spring Security?

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

spring security์—์„œ ์ œ๊ณตํ•ด์ฃผ๋Š” ๋ณด์•ˆ ์†”๋ฃจ์…˜์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐœ๋ฐœ์ž๊ฐ€ ๊ด€๋ จ ๋ณด์•ˆ ๋กœ์ง์„ ๊ตฌํ˜„ํ•  ํ•„์š”๊ฐ€ ์—†์–ด์ง„๋‹ค.

์ธ์ฆ๊ณผ ๊ถŒํ•œ, ์ธ๊ฐ€

- ์ธ์ฆ(Authentication)

: ํ•ด๋‹น ์‚ฌ์šฉ์ž๊ฐ€ ๋ณธ์ธ์ด ๋งž๋Š”์ง€ ํ™•์ธํ•˜๋Š” ์ ˆ์ฐจ

- ๊ถŒํ•œ(Authorization)

: ํŠน์ • ๋ถ€๋ถ„์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š”์ง€์— ๋Œ€ํ•œ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ์ ˆ์ฐจ

- ์ธ๊ฐ€(Authorization)

: ์ธ์ฆ๋œ ์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•œ ์ž์›์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒฐ์ •ํ•˜๋Š” ์ ˆ์ฐจ

Spring Security๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ธ์ฆ ์ ˆ์ฐจ๋ฅผ ๊ฑฐ์นœ ๋’ค ์ธ๊ฐ€ ์ ˆ์ฐจ๋ฅผ ์ง„ํ–‰ํ•˜๋ฉฐ, ์ธ๊ฐ€ ๊ณผ์ •์—์„œ ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•œ๋‹ค.

Spring Security์—์„œ๋Š” ์ด๋Ÿฌํ•œ ์ธ์ฆ๊ณผ ์ธ๊ฐ€๋ฅผ ์œ„ํ•ด Principal ์„ ์•„์ด๋””๋กœ, Credential ์„ ๋น„๋ฐ€๋ฒˆํ˜ธ๋กœ ์‚ฌ์šฉํ•˜๋Š” Credential ๊ธฐ๋ฐ˜์˜ ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ๋‹ค.

principal: ์ ‘๊ทผ ์ฃผ์ฒด. ๋ณดํ˜ธ๋ฐ›๋Š” ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋Š” ๋Œ€์ƒ
credential: ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋Š” ๋Œ€์ƒ์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ

Spring Security ์ฃผ์š” ๋ชจ๋“ˆ

- SecurityContextHolder

: ๋ณด์•ˆ ์ฃผ์ฒด์˜ ์„ธ๋ถ€ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์—ฌ ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์˜ ํ˜„์žฌ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ์— ๋Œ€ํ•œ ์„ธ๋ถ€ ์ •๋ณด๊ฐ€ ์ €์žฅ๋จ.

SecurityContextHolder๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ SecurityContextHolder.MODE_INHERITABLETHREADLOCAL ๋ฐฉ๋ฒ•๊ณผ SecurityContextHolder.MODE_THREADLOCAL ๋ฐฉ๋ฒ•์„ ์ œ๊ณต

- SecurityContext

: Authentication(์ธ์ฆ)๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ์—ญํ• 

SecurityContext๋ฅผ ํ†ตํ•ด Authentication ๊ฐ์ฒด๋ฅผ ๊บผ๋‚ด์˜ฌ ์ˆ˜ ์žˆ์Œ

- Authentication

: ํ˜„์žฌ ์ ‘๊ทผํ•˜๋Š” ์ฃผ์ฒด์˜ ์ •๋ณด์™€ ๊ถŒํ•œ์„ ๋‹ด๋Š” ์ธํ„ฐํŽ˜์ด์Šค

Authentication ๊ฐ์ฒด๋Š” SecurityContext์— ์ €์žฅ๋˜๋ฉฐ, SecurityContextHolder๋ฅผ ํ†ตํ•ด SecurityContext์— ์ ‘๊ทผํ•˜๊ณ , SecurityContext๋ฅผ ํ†ตํ•ด Authentication์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Œ

- UsernamePasswordAuthenticationToken

: Authentication(์ธ์ฆ)์„ ๊ตฌํ˜„ํ•œ AbstractAuthenticationToken์˜ ํ•˜์œ„ ํด๋ž˜์Šค

user์˜ ID๊ฐ€ principal(์ ‘๊ทผ ์ฃผ์ฒด) ์—ญํ• ์„ ํ•˜๊ณ ,
pw๊ฐ€ Credential(๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋Š” ๋Œ€์ƒ์˜ ๋น„๋ฐ€๋ฒˆํ˜ธ) ์—ญํ• ์„ ํ•จ

UsernamePasswordAuthenticationToken์˜ ์ฒซ ๋ฒˆ์งธ ์ƒ์„ฑ์ž๋Š” ์ธ์ฆ ์ „์˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜๊ณ , ๋‘ ๋ฒˆ์งธ ์ƒ์„ฑ์ž๋Š” ์ธ์ฆ์ด ์™„๋ฃŒ๋œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑ

- AuthenticationProvider

: ์‹ค์ œ ์ธ์ฆ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•จ

์ธ์ฆ ์ „ Authentication(์ธ์ฆ) ๊ฐ์ฒด๋ฅผ ๋ฐ›์•„ ์ธ์ฆ์ด ์™„๋ฃŒ๋œ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์—ญํ• 

AuthenticationProvider ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด ์ปค์Šคํ…€ํ•œ AuthenticationProvider๋ฅผ ์ž‘์„ฑํ•ด AuthenticationManager์— ๋“ฑ๋กํ•˜๋ฉด ๋จ

- Authentication Manager

: ์ธ์ฆ์— ๋Œ€ํ•œ ๋ถ€๋ถ„์€ SpringSecurity์˜ AuthenticationManager๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋˜๋Š”๋ฐ,
์‹ค์งˆ์ ์œผ๋กœ AuthenticationManager์— ๋“ฑ๋ก๋œ AuthenticationProvider์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋จ

์ธ์ฆ์— ์„ฑ๊ณตํ•˜๋ฉด ๋‘ ๋ฒˆ์งธ ์ƒ์„ฑ์ž๋ฅผ ์ด์šฉํ•ด ์ธ์ฆ์ด ์„ฑ๊ณตํ•œ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ SecurityContext์— ์ €์žฅ.
๊ทธ๋ฆฌ๊ณ  ์ธ์ฆ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์„ธ์…˜์— ๋ณด๊ด€ํ•˜๋ฉฐ, ์ธ์ฆ์ด ์‹คํŒจํ•œ ๊ฒฝ์šฐ์—๋Š” AuthenticationException๋ฅผ ๋ฐœ์ƒ

- UserDetails

: ์ธ์ฆ์— ์„ฑ๊ณตํ•ด ์ƒ์„ฑ๋œ UserDetails ๊ฐ์ฒด๋Š” Authentication(์ธ์ฆ) ๊ฐ์ฒด๋ฅผ ๊ตฌํ˜„ํ•œ UsernamePasswordAuthenticationToken์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋จ

- UserDetailsService

: ์ด ์ธํ„ฐํŽ˜์ด์Šค๋Š” UserDetails ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋‹จ ํ•˜๋‚˜์˜ ๋ฉ”์†Œ๋“œ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”๋ฐ,
์ผ๋ฐ˜์ ์œผ๋กœ ์ด๋ฅผ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค ๋‚ด๋ถ€์— UserRepository๋ฅผ ์ฃผ์ž…๋ฐ›์•„ DB์™€ ์—ฐ๊ฒฐํ•ด ์ฒ˜๋ฆฌํ•จ

- Password Encoding

: AuthenticationManagerBuilder.userDetailsService().passwordEncoder()๋ฅผ ํ†ตํ•ด ํŒจ์Šค์›Œ๋“œ ์•”ํ˜ธํ™”์— ์‚ฌ์šฉ๋  PasswordEncoder ๊ตฌํ˜„์ฒด๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์Œ

- GrantedAuthority

: ํ˜„์žฌ ์‚ฌ์šฉ์ž(principal)๊ฐ€ ๊ฐ€์ง„ ๊ถŒํ•œ

ํŠน์ • ์ž์›์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•ด ์ ‘๊ทผ ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •

์™œ ์‚ฌ์šฉํ•˜๋Š”๊ฐ€?

  • ๋ชจ๋“  URL์— ๋Œ€ํ•œ ์ธ์ฆ์„ ์š”๊ตฌ
  • ๋กœ๊ทธ์ธ ํผ ์ƒ์„ฑ, ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ
  • CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด
  • Session Fixation ๊ณต๊ฒฉ ๋ฐฉ์–ด
  • ์š”์ฒญ ํ—ค๋” ๋ณด์•ˆ
  • ์„œ๋ธ”๋ฆฟ API ๋ฉ”์†Œ๋“œ์™€ ํ†ตํ•ฉ

* Session Fixation
: ๊ณต๊ฒฉ์ž๊ฐ€ ์‚ฌ์ดํŠธ์— ์ ‘์†ํ•ด ์„ธ์…˜ ์ƒ์„ฑ ํ›„ ์„ธ์…˜ ์•„์ด๋””๋ฅผ ์‚ฌ์šฉ์ž์—๊ฒŒ ์ „๋‹ฌํ•˜๋ฉด, ์‚ฌ์šฉ์ž๋กœ ํ•˜์—ฌ๊ธˆ ๋™์ผ ์„ธ์…˜ ์•„์ด๋””๋ฅผ ์‚ฌ์šฉํ•ด ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธํ•˜๋„๋ก ํ•จ.

์‚ฌ์šฉ์ž๊ฐ€ ๋กœ๊ทธ์ธ์— ์„ฑ๊ณตํ•˜๋ฉด ๊ณต๊ฒฉ์ž๋Š” ์‚ฌ์šฉ์ž์™€ ๋™์ผํ•œ ์„ธ์…˜์„ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋จ. ๊ณต๊ฒฉ์ž๋Š” ์ด ์ƒํƒœ์—์„œ ์•…์˜์  ํ–‰์œ„๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๊ฒƒ.


์‹ค์Šต

ํ”„๋กœ์ ํŠธ ์ƒ์„ฑ

์ดˆ๊ธฐ์„ค์ •

ํ•„์š”ํ•œ ํŒŒ์ผ

๐Ÿ“ build.gradle

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

์œ„ ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ

security: spring security ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•จ
springsecurity5: view ๋‹จ์—์„œ ํ˜„์žฌ ๋กœ๊ทธ์ธ ๋œ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•จ

์ง„ํ–‰์ค‘์ด๋ผ ๋‚ด์ผ ์ค‘์œผ๋กœ ๋งˆ์ € ์˜ฌ๋ฆฌ๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.๐Ÿ™‡ (21.11.02)

๐Ÿ“ Login

Config

: ๊ด€๋ จ ์„ค์ •์„ ํ•ด์ฃผ๊ธฐ ์œ„ํ•œ ํŒŒ์ผ

WebSecurityConfig.java

@RequiredArgsConstructor
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserService userService;

    @Override
    public void configure(WebSecurity web) {
        web.ignoring().antMatchers("/css/**", "/js/**", "/img/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login", "/signup", "/user")
                .permitAll()
                .antMatchers("/")
                .hasRole("USER")
                .antMatchers("/admin")
                .hasRole("ADMIN")
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/")
                .and()
                .logout()
                .logoutSuccessUrl("/login")
                .invalidateHttpSession(true);
    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

UserEntity

UserInfo.java

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
public class UserInfo implements UserDetails {

    @Id
    @Column(name = "code")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long code;

    @Column(name = "email", unique = true)
    private String email;

    @Column(name = "password")
    private String password;

    @Column(name = "auth")
    private String auth;

    @Builder
    public UserInfo(String email, String password, String auth) {
        this.email = email;
        this.password = password;
        this.auth = auth;
    }


    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Set<GrantedAuthority> roles = new HashSet<>();

        for(String role : auth.split(",")) {
            roles.add(new SimpleGrantedAuthority(role));
        }

        return roles;
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

UserRepository

: ์‚ฌ์šฉ์ž ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋ชจ๋‘ ๊ตฌํ˜„ํ–ˆ์œผ๋‹ˆ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๊ธฐ ์œ„ํ•œ repository ๊ตฌํ˜„ํ•˜๊ธฐ

UserRepository.java (interface)

package SpringSecurity.FirstSpringSecurity;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface UserRepository extends JpaRepository<UserInfo, Long> {

    Optional<UserInfo> findByEmail(String email);
}

UserService

UserService.java

@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new UsernameNotFoundException((email)));
    }

    public Long save(UserInfoDto infoDto) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        infoDto.setPassword(encoder.encode(infoDto.getPassword()));

        return userRepository.save(UserInfo.builder()
                .email(infoDto.getEmail())
                .auth(infoDto.getAuth())
                .password(infoDto.getPassword())
                .build())
                .getCode();
    }
}

๐Ÿ“ JOIN

UserDTO

: form์œผ๋กœ ๋ฐ›์„ ํšŒ์› ์ •๋ณด๋ฅผ ๋งคํ•‘์‹œ์ผœ์ค„ ๊ฐ์ฒด ์ƒ์„ฑ

UserInfoDto.java

package SpringSecurity.FirstSpringSecurity;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class UserInfoDto {

    private String email;
    private String password;

    private String auth;
}

UserService ์ถ”๊ฐ€

UserService.java

    public Long save(UserInfoDto infoDto) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        infoDto.setPassword(encoder.encode(infoDto.getPassword()));

        return userRepository.save(UserInfo.builder()
                .email(infoDto.getEmail())
                .auth(infoDto.getAuth())
                .password(infoDto.getPassword())
                .build())
                .getCode();
    }

Controller

UserController.java

@RequiredArgsConstructor
@Controller
public class UserController {

    private final UserService userService;

    @PostMapping("/user")
    public String signup(UserInfoDto infoDto) {
        userService.save(infoDto);

        return "redirect:/login";
    }
}

๐Ÿ“ LOGOUT

Controller ์ถ”๊ฐ€

UserController.java

    @GetMapping(value = "/logout")
    public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler()
                .logout(request, response, SecurityContextHolder.getContext().getAuthentication());

        return "redirect:/login";
    }

๐Ÿ“ +

request - view ์—ฐ๊ฒฐ

MvcConfig.java

package SpringSecurity.FirstSpringSecurity;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("main");
        registry.addViewController("/login").setViewName("login");
        registry.addViewController("/admin").setViewName("admin");
        registry.addViewController("/signup").setViewName("signup");
    }
}

๐Ÿ“ View ์ž‘์„ฑ(html)

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8">
  <title>login</title>
</head>
<body>
<h1>Login</h1> <hr>
<img src="/img/info.jpg" alt="ํ™˜์˜ ์ด๋ฏธ์ง€" style="max-width: 500px; height: 250px"/>

<form action="/login" method="POST">
  <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
  email : <input type="text" name="username"> <br>
  password : <input type="password" name="password"> <br>
  <button type="submit">Login</button>
</form> <br>

<a href="/signup">Go to join! โ†’</a>
</body>
</html>

signup.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
  <meta charset="UTF-8">
  <title>sign up</title>
</head>
<body>
<h1>Sign Up</h1> <hr>

<form th:action="@{/user}" method="POST">
  email : <input type="text" name="email"> <br>
  password : <input type="password" name="password"> <br>
  <input type="radio" name="auth" value="ROLE_ADMIN,ROLE_USER"> admin
  <input type="radio" name="auth" value="ROLE_USER" checked="checked"> user <br>
  <button type="submit">Join</button>
</form> <br>

<a href="/login">Go to login โ†’</a>
</body>
</html>

main.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3"
      xmlns:th="http://www.w3.org/1999/xhtml">
<head>
  <title>main</title>
</head>
<body>
<h2>ํšŒ์› ์ „์šฉ ํŽ˜์ด์ง€</h2>
ID : <span sec:authentication="name"></span> <br>
์†Œ์œ  ๊ถŒํ•œ : <span sec:authentication="authorities"></span> <br>

<form id="logout" action="/logout" method="POST">
  <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
  <input type="submit" value="๋กœ๊ทธ์•„์›ƒ"/>
</form>

</body>
</html>

admin.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
  <meta charset="UTF-8">
  <title>admin</title>
</head>
<body>
<h2>๊ด€๋ฆฌ์ž ์ „์šฉ ํŽ˜์ด์ง€</h2>
ID : <span sec:authentication="name"></span> <br>
์†Œ์œ  ๊ถŒํ•œ : <span sec:authentication="authorities"></span> <br>

<form id="logout" action="/logout" method="POST">
  <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
  <input type="submit" value="๋กœ๊ทธ์•„์›ƒ"/>
</form>
</body>
</html>

๊ฒฐ๊ณผ

์žฌ์ž…ํ•™์€ ์ œ๊ฐ€ ๋„ฃ์—ˆ์–ด์š”.

user๋กœ ๊ฐ€์ž…ํ•ด ๋กœ๊ทธ์ธ ํ–ˆ์„ ๋•Œ

admin์œผ๋กœ ๋กœ๊ทธ์ธ ํ–ˆ์„ ๋•Œ

๊ทธ๋ฆฌ๊ณ  ์ด๋ฏธ ํšŒ์›๊ฐ€์ž… ํ•œ user@abc.com, admin@abc.com์œผ๋กœ ๊ฐ€์ž… ๋ถˆ๊ฐ€ํ•จ


์ฐธ๊ณ 
์‹ค์Šต ์ž‘์„ฑ์ž๋‹˜ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ๐Ÿ™‡

profile
๊ฐœ๋ฐœ ๋ฐ”๋ณด ์ด์‚ฌ ์ค‘

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