🌱 Spring Security - μ„Έμ…˜μ„ μ΄μš©ν•œ 둜그인 처리

Kim Dae HyunΒ·2021λ…„ 7μ›” 29일
11

Spring-Security

λͺ©λ‘ 보기
1/2
post-thumbnail

전체 μ†ŒμŠ€μ½”λ“œ Github

πŸ”ŽπŸ”ŽπŸ”Ž

Spring Securityλ₯Ό μ΄μš©ν•΄μ„œ μ„Έμ…˜ 기반 둜그인 및 κΆŒν•œ 검증을 κ΅¬ν˜„ν•©λ‹ˆλ‹€.
λ””ν…ŒμΌν•œ μ„€μ •λ³΄λ‹€λŠ” 전체적인 흐름을 보기 μœ„ν•œ ν”„λ‘œμ νŠΈμž…λ‹ˆλ‹€.

πŸ”Ž Dependencies

Spring Initializr

  • Spring web
  • Spring security
  • Spring Data JPA
  • Lombok
  • MySQL Driver

πŸ”Ž Member Entity μž‘μ„±

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "MEMBER")
@Getter
public class Member {

    @Id @GeneratedValue
    private Long id;

    private String username;

    private String password;

    private boolean enabled;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Builder
    public Member(String username, String password, boolean enabled, Role role) {
        this.username = username;
        this.password = password;
        this.enabled = enabled;
        this.role = role;
    }
}

Role (Enum class)

spring securityκ°€ μ œκ³΅ν•˜λŠ” ROLE 넀이밍 정책이 ROLE_κΆŒν•œ μ΄λ―€λ‘œ λ§žμΆ°μ„œ μž‘μ„±ν•΄μ€λ‹ˆλ‹€.

@Getter
public enum Role {

    ROLE_ADMIN("κ΄€λ¦¬μž"), ROLE_MANAGER("λ§€λ‹ˆμ €"), ROLE_MEMBER("μΌλ°˜μ‚¬μš©μž");

    private String description;

    Role(String description) {
        this.description = description;
    }
}

Spring security의 User 클래슀λ₯Ό 보면 "ROLE_"둜 μ‹œμž‘ν•˜λŠ” κΆŒν•œμ„ μ°ΎλŠ” 것을 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.


πŸ”Ž MemberRepository μž‘μ„±

username을 μ΄μš©ν•΄ 둜그인 처리λ₯Ό ν•  것이기 λ•Œλ¬Έμ— JPA 넀이밍 쿼리λ₯Ό μ΄μš©ν•΄ findByUsername λ©”μ„œλ“œ ν•˜λ‚˜λ₯Ό μ •μ˜ν•©λ‹ˆλ‹€.

κ°„λ‹¨ν•œ μ‹€μŠ΅μ„ μœ„ν•΄ μ„œλΉ„μŠ€ 계측은 두지 μ•Šμ•˜μŠ΅λ‹ˆλ‹€.

public interface MemberRepository extends CrudRepository<Member, Long> {

    Optional<Member> findByUsername(String username);
}

πŸ”Ž User 클래슀 μ»€μŠ€ν…€

spring securityκ°€ μ œκ³΅ν•˜λŠ” User 클래슀λ₯Ό μš°λ¦¬κ°€ μ •μ˜ν•œ Member둜 μ‚¬μš©ν•˜κΈ° μœ„ν•΄ μ»€μŠ€ν…€ν•©λ‹ˆλ‹€.

이후 SecurityUserλ₯Ό 톡해 Member에 μ ‘κ·Όν•  κ²ƒμ΄λ―€λ‘œ Memberλ₯Ό ν•„λ“œλ‘œ κ°–κ²Œ ν•˜κ³  μƒμ„±μžλ₯Ό 톡해 값을 μœ μ§€μ‹œμΌœ μ€λ‹ˆλ‹€.

super ν‚€μ›Œλ“œλ₯Ό μ΄μš©ν•΄ λΆ€λͺ¨ 클래슀(User)의 μƒμ„±μžλ‘œ username, password, role 을 λ„˜κ²¨μ€λ‹ˆλ‹€.

μš”μ²­λ˜λŠ” 데이터λ₯Ό ν™•μΈν•˜κΈ° μœ„ν•΄ λ‘œκ·Έλ„ μ°μ–΄λ³΄μ•˜μŠ΅λ‹ˆλ‹€.

@Slf4j
@Getter @Setter
public class SecurityUser extends User {

    private Member member;

    public SecurityUser(Member member) {
        super(member.getUsername(), member.getPassword(), AuthorityUtils.createAuthorityList(member.getRole().toString()));

        log.info("SecurityUser member.username = {}", member.getUsername());
        log.info("SecurityUser member.password = {}", member.getPassword());
        log.info("SecurityUser member.role = {}", member.getRole().toString());

        this.member = member;
    }

}

πŸ”Ž UserDetailsService κ΅¬ν˜„μ²΄ μž‘μ„±

λ‘œκ·ΈμΈμ„ μœ„ν•œ username (ν˜Ήμ€ id, email) 이 DB에 μžˆλŠ”μ§€ ν™•μΈν•˜λŠ” λ©”μ„œλ“œ loadUserByUsername λ©”μ„œλ“œλ₯Ό μž‘μ„±ν•©λ‹ˆλ‹€.

이 λ©”μ„œλ“œμ—μ„œ μ‚¬μš©ν•˜κΈ° μœ„ν•΄ repository에 findByUsername λ©”μ„œλ“œλ₯Ό μ •μ˜ν•œ 것 μž…λ‹ˆλ‹€.

memberRepositoryλ₯Ό μ£Όμž…λ°›κ³  findByUsername λ©”μ„œλ“œλ₯Ό μ΄μš©ν•΄ μž…λ ₯된 username이 μœ νš¨ν•œμ§€ ν™•μΈν•©λ‹ˆλ‹€.

username이 μœ νš¨ν•˜μ§€ μ•Šλ‹€λ©΄ μ˜ˆμ™Έλ₯Ό λ°œμƒμ‹œν‚€κ³  μœ νš¨ν•˜λ‹€λ©΄ username으둜 μ°Ύμ•„μ˜¨ Memberλ₯Ό μ΄μš©ν•΄ Custom User인 SecurityUserλ₯Ό μƒμ„±ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€.

@Slf4j
@Component
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final MemberRepository memberRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Member> findMember = memberRepository.findByUsername(username);
        if (!findMember.isPresent()) throw new UsernameNotFoundException("μ‘΄μž¬ν•˜μ§€ μ•ŠλŠ” username μž…λ‹ˆλ‹€.");

        log.info("loadUserByUsername member.username = {}", username);

        return new SecurityUser(findMember.get());
    }
}

πŸ”Ž SecurityConfig μž‘μ„±

이제 인증 및 κΆŒν•œ 처리λ₯Ό μœ„ν•œ SecutiryConfig클래슀λ₯Ό μž‘μ„±ν•©λ‹ˆλ‹€.

WebSecurityConfigurerAdapterλ₯Ό 상속받고 configure λ©”μ„œλ“œλ₯Ό μ˜€λ²„λΌμ΄λ”© ν•©λ‹ˆλ‹€.

κ°€μž₯ λ¨Όμ € authorizaRequests λ₯Ό μ΄μš©ν•΄ κΆŒν•œ 검증을 μˆ˜ν–‰ν•©λ‹ˆλ‹€.
antMatchersμ—λŠ” κ³΅ν†΅λœ κΆŒν•œμ„ κ°€μ§€λŠ” endpointλ₯Ό λ¬Άμ–΄μ€λ‹ˆλ‹€. 그리고 λ‹€μŒμ˜ λ©”μ„œλ“œλ₯Ό μ΄μš©ν•΄μ„œ 묢어진 endpoint에 λŒ€ν•œ κΆŒν•œ λΆ„κΈ°λ₯Ό μˆ˜ν–‰ν•©λ‹ˆλ‹€.

  • permitAll : 인증 없이 μ ‘κ·Ό κ°€λŠ₯
  • authentication : 인증된 μ‚¬μš©μžλΌλ©΄ νŠΉλ³„ν•œ κΆŒν•œ 없이 μ ‘κ·Ό κ°€λŠ₯
  • hasAnyRole : λͺ…μ‹œν•œ κΆŒν•œμ„ 가진 인증된 μ‚¬μš©μžλ§Œ μ ‘κ·Ό κ°€λŠ₯
    • hasAnyRole("ADMIN", "MANAGER") => Role.ADMIN, Role.MANAGER μ ‘κ·Όκ°€λŠ₯
  • hasRole : hasAnyRoleκ³Ό λ™μΌν•œ κΈ°λŠ₯μ΄μ§€λ§Œ ν•˜λ‚˜μ˜ κΆŒν•œκ³Ό 맀핑

λ‹€μŒμ€ formLogin을 μ΄μš©ν•΄ 둜그인 κ΄€λ ¨ κΈ°λŠ₯을 μ •μ˜ν•©λ‹ˆλ‹€.

  • loginPage: μΈμ¦λ˜μ§€ μ•Šμ€ μ‚¬μš©μžκ°€ 인증을 ν•„μš”λ‘œ ν•˜λŠ” endpoint에 μ ‘κ·Όμ‹œ μ„€μ •ν•œ 경둜둜 μ΄λ™μ‹œμΌœμ€€λ‹€. (보톡 둜그인 νŽ˜μ΄μ§€λ‘œ 이동)
  • loginProcessingUrl : μ§€μ •ν•œ endpoint둜 POST μš”μ²­μ΄ 올 λ•Œ μš”μ²­λœ 정보λ₯Ό 기반으둜 둜그인 처리λ₯Ό μˆ˜ν–‰
  • exceptionHandling().accessDiniedPage : 인증된 μ‚¬μš©μžμ΄μ§€λ§Œ κΆŒν•œ λ°–μ˜ endpoint에 μ ‘κ·Όμ‹œ μ΄λ™μ‹œν‚¬ uri λͺ…μ‹œ
  • logout().logoutUrl : ν•΄λ‹Ή URL둜 μš”μ²­μ΄ μ˜€λŠ” 경우 λ‘œκ·Έμ•„μ›ƒ μˆ˜ν–‰
http.userDetailsService(userDetailsService);

μ‹€μ œ 둜그인 처리λ₯Ό μˆ˜ν–‰ν•˜λŠ” UserDetailsServiceλ₯Ό 지정

@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    private final UserDetailsServiceImpl userDetailsService;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable();
        http.authorizeRequests()
                .antMatchers("/","/registry","/login", "/css/**").permitAll()
                .antMatchers("/member/**").authenticated() // μΌλ°˜μ‚¬μš©μž μ ‘κ·Ό κ°€λŠ₯
                .antMatchers("/manager/**").hasAnyRole("MANAGER", "ADMIN") // λ§€λ‹ˆμ €, κ΄€λ¦¬μž μ ‘κ·Ό κ°€λŠ₯
                .antMatchers("/admin/**").hasRole("ADMIN"); // κ΄€λ¦¬μžλ§Œ μ ‘κ·Ό κ°€λŠ₯
        // 인증 ν•„μš”μ‹œ 둜그인 νŽ˜μ΄μ§€μ™€ 둜그인 μ„±κ³΅μ‹œ λ¦¬λ‹€μ΄λž™νŒ… 경둜 지정
        http.formLogin().loginPage("/login").defaultSuccessUrl("/", true);
        // 둜그인이 μˆ˜ν–‰λ  uri 맀핑 (post μš”μ²­μ΄ κΈ°λ³Έ)
        http.formLogin().loginProcessingUrl("/login").defaultSuccessUrl("/", true);
        // 인증된 μ‚¬μš©μžμ΄μ§€λ§Œ μΈκ°€λ˜μ§€ μ•Šμ€ κ²½λ‘œμ— μ ‘κ·Όμ‹œ λ¦¬λ‹€μ΄λž™νŒ… μ‹œν‚¬ uri 지정
        http.exceptionHandling().accessDeniedPage("/forbidden");
        // logout
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/");

        http.userDetailsService(userDetailsService);
    }
}

πŸ”Ž Home HTML μž‘μ„±

인증된 μ‚¬μš©μžμ—κ²Œ viewλ₯Ό λ‹¬λ¦¬ν•˜κΈ° μœ„ν•΄ thymeleaf extras Springsecurity5 λ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. (인증된 μ‚¬μš©μžμ—κ²Œλ§Œ λ‘œκ·Έμ•„μ›ƒ λ²„νŠΌ ν‘œμ‹œ, 둜그인 λ²„νŠΌ λ―Έν‘œμ‹œ λ“± ..)
Maven Repository 링크

μ˜μ‘΄μ„± μΆ”κ°€ ν›„ μ•„λž˜μ™€ 같이 λ„€μž„μŠ€νŽ˜μ΄μŠ€λ₯Ό μΆ”κ°€ν•΄μ€λ‹ˆλ‹€.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Home</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container">
  <div class="my-4">
    <h2>ν™ˆ</h2>
      <div sec:authorize="isAuthenticated()">
          <p th:text="|${principal.username}λ‹˜ ν™˜μ˜ν•©λ‹ˆλ‹€.|"></p>
          <p th:text="|ν˜„μž¬ ${principal.username} λ‹˜μ˜ κΆŒν•œ : ${role}|"></p>
          <ul>
              <li th:text="|session.getId = ${#session.getId()}|"></li>
              <li th:text="|session.getAttributeNames = ${#session.getAttributeNames()}|"></li>
              <li th:text="|session.getCreationTime = ${#session.getCreationTime()}|"></li>
          </ul>
      </div>
      <div class="btn">
        <a th:href="@{/registry}">
            <button class="btn btn-lg btn-secondary">νšŒμ›κ°€μž…</button>
        </a>
        <a th:href="@{/login}">
            <button sec:authorize="!isAuthenticated()" class="btn btn-lg btn-dark">둜그인</button>
        </a>
          <a th:href="@{/logout}">
              <button sec:authorize="isAuthenticated()" class="btn btn-danger btn-lg">λ‘œκ·Έμ•„μ›ƒ</button>
          </a>

        <hr class="my-4">
          <a th:href="@{/admin/home}"><button class="btn btn-lg btn-dark">ADMIN page μ ‘κ·Ό </button></a>
          <a th:href="@{/manager/home}"><button class="btn btn-lg btn-dark">MANAGER page μ ‘κ·Ό</button></a>
      </div>
  </div>
</div>
</body>
</html>

πŸ”Ž νšŒμ›κ°€μž…, 둜그인 폼

[ νšŒμ›κ°€μž… 폼 ]
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>νšŒμ›κ°€μž…</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>

<div class="container">
    <div class="my-4">
        <h2>νšŒμ›κ°€μž…</h2>
    </div>
    <form th:action method="post" th:object="${member}">
        <div class="mb-3">
            <label for="username" class="form-label">username</label>
            <input type="text" class="form-control" id="username" th:field="*{username}">
        </div>
        <div class="mb-3">
            <label for="password" class="form-label">Password</label>
            <input type="password" class="form-control" id="password" th:field="*{password}">
        </div>

        <hr class="my-3">
        <div class="mb-3">
            <div th:each="r : ${roles}" class="form-check form-check-inline">
                <input type="radio" th:field="*{role}" th:value="${r.value}" class="form-check-input">
                <label th:for="${#ids.prev('role')}" th:text="${r.key}" class="form-check-label"></label>
            </div>
        </div>
        <button type="submit" class="btn btn-primary">νšŒμ›κ°€μž…</button>
    </form>
</div>
</body>
</html>
[ 둜그인 폼 ]

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
</head>
<body>
<div class="container">
  <div class="my-4">
    <h2>둜그인</h2>
  </div>
  <form th:action method="post" th:object="${member}">
    <div class="mb-3">
      <label for="username" class="form-label">username</label>
      <input type="text" class="form-control" id="username" th:field="*{username}">
    </div>
    <div class="mb-3">
      <label for="password" class="form-label">Password</label>
      <input type="password" class="form-control" id="password" th:field="*{password}">
    </div>
    <button type="submit" class="btn btn-primary">둜그인</button>
  </form>
</div>
</body>
</html>

πŸ”Ž νšŒμ›κ°€μž… 컨트둀러 μž‘μ„±

μ„œλΉ„μŠ€ 계측을 λ”°λ‘œ 두지 μ•Šμ•„ μ»¨νŠΈλ‘€λŸ¬μ—μ„œ λ°”λ‘œ λ ˆν¬μ§€ν† λ¦¬λ₯Ό μ£Όμž…λ°›μ•„ μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€.

password μ €μž₯μ‹œ BCryptPasswordEncoderλ₯Ό μ΄μš©ν•΄ 단방ν–₯ ν•΄μ‹œλ‘œ μΈμ½”λ”©ν–ˆμŠ΅λ‹ˆλ‹€.

@Slf4j
@RequiredArgsConstructor
@Controller
public class RegistryController {

    private final MemberRepository memberRepository;
    private final BCryptPasswordEncoder passwordEncoder;

    @GetMapping("/registry")
    public String registryForm(Model model) {
        model.addAttribute("member", new RegistryRequest());
        return "registration";
    }

    @PostMapping("/registry")
    public String registry(@ModelAttribute RegistryRequest registryRequest) {
        Member member = Member.builder()
                .username(registryRequest.getUsername())
                .password(passwordEncoder.encode(registryRequest.getPassword()))
                .role(registryRequest.getRole())
                .build();
        memberRepository.save(member);

        return "redirect:/login";
    }

    @ModelAttribute("roles")
    public Map<String, Role> roles() {
        Map<String, Role> map = new LinkedHashMap<>();
        map.put("κ΄€λ¦¬μž", Role.ROLE_ADMIN);
        map.put("λ§€λ‹ˆμ €", Role.ROLE_MANAGER);
        map.put("일반 μ‚¬μš©μž", Role.ROLE_MEMBER);
        return map;
    }
}

BCryptPasswordEncoderλŠ” κ°„λ‹¨ν•˜κ²Œ main ν΄λž˜μŠ€μ—μ„œ 빈으둜 λ“±λ‘ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

view와 controller κ°„ 데이터 μΈν„°νŽ˜μ΄μ‹±μ„ μœ„ν•΄ μ‚¬μš©ν•œ μš”μ²­κ°μ²΄μž…λ‹ˆλ‹€.

@Getter @Setter
public class RegistryRequest {

    private String username;
    private String password;
    private Role role = Role.ROLE_MEMBER;
}

πŸ”Ž 둜그인 컨트둀러 μž‘μ„±

μ „λΆ€μž…λ‹ˆλ‹€..

둜그인 μͺ½ μ»¨νŠΈλ‘€λŸ¬λŠ” 둜그인 폼 λžœλ”λ§λ§Œμ„ λ‹΄λ‹Ήν•©λ‹ˆλ‹€.
둜그인 μ²˜λ¦¬λŠ” SecurityConfig의 loginProcessingUrl둜 λ³΄λ‚΄μ£ΌκΈ°λ§Œ ν•˜λ©΄ λ©λ‹ˆλ‹€.

@Controller
public class LoginController {

    @GetMapping("/login")
    public String loginForm(Model model) {

        model.addAttribute("member", new LoginRequest());
        return "login";
    }
}

πŸ”Ž ν…ŒμŠ€νŠΈ !

ν™ˆ ν™”λ©΄

일반 μ‚¬μš©μž (Role_MEMBER)둜 νšŒμ›κ°€μž…

일반 μ‚¬μš©μž κΆŒν•œμœΌλ‘œ MANAGER, ADMIN νŽ˜μ΄μ§€μ— μ ‘κ·Ό (κΆŒν•œX)

ADMIN κΆŒν•œμœΌλ‘œ νšŒμ›κ°€μž… / 둜그인 ν›„ MANAGER, ADMIN νŽ˜μ΄μ§€μ— μ ‘κ·Ό (κΆŒν•œO)

μ΄μƒμœΌλ‘œ Spring Securityλ₯Ό μ΄μš©ν•œ κ°„λ‹¨ν•œ μ„Έμ…˜λ°©μ‹ λ‘œκ·ΈμΈμ„ μ•Œμ•„λ³΄μ•˜μŠ΅λ‹ˆλ‹€.
사싀 이런 방식은 μ΄μ œλŠ” 잘 쓰이지 μ•ŠλŠ”λ‹€κ³  ν•©λ‹ˆλ‹€. 주둜 JWT 토큰방식을 μ‚¬μš©ν•˜μ£ . λ³΄μ•ˆμ μΈ λ¬Έμ œκ°€ κ°€μž₯ ν¬κ² μ§€λ§Œ 이런 μ„Έμ…˜λ°©μ‹μ„ μ‚¬μš©ν•œλ‹€λ©΄ 이후 Oauth2 μ‚¬μš©μ‹œ λ¬Έμ œκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€.
κ·Έλž˜λ„ 기본은 μ•Œκ³  μžˆμ–΄μ•Όν•œλ‹€κ³  μƒκ°ν•˜κΈ°μ— ν•œ 번 κ°„λ‹¨ν•˜κ²Œ κ΅¬ν˜„ν•΄λ΄€μŠ΅λ‹ˆλ‹€. γ…Ž

λ‹€μŒμ—λŠ” JWTλ₯Ό μ΄μš©ν•œ 인증을 κ΅¬ν˜„ν•΄λ³΄κ² μŠ΅λ‹ˆλ‹€ !

전체 μ†ŒμŠ€μ½”λ“œλŠ” κΉƒν—ˆλΈŒλ₯Ό μ°Έκ³ ν•΄μ£Όμ„Έμš” κ°μ‚¬ν•©λ‹ˆλ‹€ γ…Ž πŸ˜„

profile
μ’€ 더 천천히 까먹기 μœ„ν•΄ κΈ°λ‘ν•©λ‹ˆλ‹€. 🧐

1개의 λŒ“κΈ€

comment-user-thumbnail
2022λ…„ 8μ›” 21일

μ•ˆλ…•ν•˜μ„Έμš”.
OAuth2 μ‚¬μš©μ‹œ μ™œ λ¬Έμ œκ°€ μžˆλ‹€κ³  μƒκ°ν•˜μ‹œλ‚˜μš”?

λ‹΅κΈ€ 달기