개인 공부

Spring Security를 활용한 로그인!?!?

orin602 2024. 12. 21. 17:46

예전에 한 회사에서 면접을 볼 때 "Spring Security 사용해 본 적이 있나요?" 라고 했을 때 없다고 했었는데 개인 공부를 하면서 웹 개발 중 제일 처음 배웠던 로그인 기능을 Spring Security에 대해서 공부도 할 겸 활용해 보겠습니다..

(그 때, 사용한 적 없다고 했지만 어떤 기능인지 알고 잘 대답했더라면 합격 했을 수도 있었을 것 같네....)

 

Spring Security???

Spring 기반 애플리케이션에서 인증(Authentication)과 권한 부여(Authorization)를 처리하는 강력한 보안 프레임워크.

ㅇㅐ플리케이션을 외부 공격으로부터 보호하고, 사용자의 접근 권한을 제어하는데 필요한 다양한 기능을 제공합니다.

 

Spring Security의 주요 기능

  • 인증(Authentication) : 사용자가 누구인지 확인. =>입력한 ID와 Password를 확인하고 사용자의 신원을 검증.
  • 권한 부여(Authorization) : 사용자의 접근을 제어. => 특정 URL에 대해 권한이 있는 사용자만 접근할 수 있도록 설정.(관리자 페이지에 적합할 것 같아요)
  • CSRF(Cross-Site Request Forgery) 보호 : Spring Security는 CSRF 공격으로부터 애플리케이션을 보호하기 위해 기본적으로 CSRF 방어 기능을 제공.
  • 세션 관리 : 사용자가 로그인한 세션을 관리하며, 세션 타임아웃, 동시 세션 제한 등을 설정할 수 있음.
  • HTTP 보안 : HTTP 요청에 대한 보안 설정을 쉽게 할 수 있도록 도와줌. => HTTPS 강제, 특정 경로에 대한 접근 제한 등을 설정.
  •  

프로젝트 생성

프로젝트 생성

(Spring Boot 프로젝트 생성할 때 Spring Security를 포함!!! -- 의존성 추가를 따로 하지 않아도 되서 편해요!!!!)

 

application.properties 설정 (이전 MVC공부할 때 사용한 서버포트를 사용할게요)

초기 설정

(기능 구현 도중 설정이 필요한 부분은 그때마다 추가할게요)

 

엔티티 클래스 생성

package com.demo.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Member {

    @Id //기본키
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    public int memberSeq;

    private String id;
    private String password;
    private String name;

    private Role roles; // 관리자, 일반회원 구분

    public enum Role {
        ADMIN, USER
    }
}
  • @Entity : Member 클래스가 JPA 엔티티임을 나타낸다. => 데이터베이스의 테이블과 매핑되어서 JPA가 관리한다.
  • @Getter : 클래스의 모든 필드에 대한 getter 메서드를 자동으로 생성합니다.
  • @Setter : 클래스의 모든 필드에 대한 setter 메서드를 자동으로 생성합니다.
  • @ToString : toString() 메서드를 자동으로 생성하여 객체의 문자열 표현을 출력할 수 있게 합니다.
  • @NoArgsConstructor : 인자가 없는 기본 생성자를 자동으로 생성합니다.
  • @AllArgsConstructor : 모든 필드를 매개변수로 받는 생성자를 자동으로 생성합니다.
  • public enum Role { ADMIN, USER } : Role은 회원의 역할을 정의하는 enum입니다.

 

Repository 생성

package com.demo.repository;

import java.util.Optional;

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

import com.demo.domain.Member;

public interface MemberRepository extends JpaRepository<Member, Integer> {
	Optional<Member> findById(String id); // id 컬럼을 기준으로 회원을 조회
}
  • MemberRepository extends JpaRepository<Member, Integer> : JpaRepository는 Spring Data JPA에서 제공하는 기본적인 인터페이스로, Member 엔티티에 대한 기본적인 CRUD 기능을 자동으로 제공합니다.
  • Optional<Member> : Optional은 값이 존재할 수도 있고, 존재하지 않을 수도 있는 경우를 처리하기 위해 사용됩니다. 즉, id에 해당하는 회원이 없을 경우, Optional.empty()를 반환하고, 있으면 해당 회원 객체를 반환합니다.

Service 생성

package com.demo.service;

import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import com.demo.domain.Member;
import com.demo.repository.MemberRepository;

@Service
public class MemberService {

    @Autowired
    private MemberRepository memberRepo;

    private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    // 회원가입
    public Member register(Member member) {
        Member newMember = new Member();

        newMember.setId(member.getId());
        // 비밀번호 암호화
        String encryptedPassword = passwordEncoder.encode(member.getPassword());
        newMember.setPassword(encryptedPassword);
        newMember.setName(member.getName());
        // 회원 설정
        if(member.getRoles() == null) {
            newMember.setRoles(Member.Role.USER); // 기본 일반회원으로 설정
        } else {
            newMember.setRoles(member.getRoles()); // 요청 회원이 있으면 그 역할로 설정.
        }

        return memberRepo.save(newMember);
    }


    // 사용자 아이디로 회원 찾기
    public Optional<Member> findById(String id) {
        return memberRepo.findById(id);
    }

    // 전체 회원 조회
    public List<Member> getAllMembers() {
        return memberRepo.findAll();
    }
    
}
  • @Service : 이 어노테이션은 이 클래스가 서비스 컴포넌트임을 나타냅니다. 서비스 컴포넌트는 비즈니스 로직을 구현하고, 컨트롤러에서 호출됩니다. Spring이 이 클래스를 **빈(Bean)**으로 관리하여 의존성 주입을 할 수 있게 해줍니다.
  • @Autowired : Spring에서 의존성 주입을 자동으로 처리하는 어노테이션입니다.
  • new BCryptPasswordEncoder(); : 는 비밀번호를 암호화하는 데 사용됩니다. encode() 메서드를 통해 사용자가 입력한 비밀번호를 암호화된 비밀번호로 변환하여 저장합니다.

 

Contorller 구현

package com.demo.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.demo.domain.Member;
import com.demo.service.MemberService;

@Controller
public class MemberController {

    @Autowired
    private MemberService memberService;

    // 메인페이지
    @GetMapping("/main")
    public String mainPage(Model model) {
        // 전체 회원 조회
        List<Member> members = memberService.getAllMembers();
        model.addAttribute("members", members);

        return "main"; // main.html 반환
    }

    // 회원가입 페이지
    @GetMapping("/register")
    public String registerView() {
        return "register"; // register.html 반환
    }

    // 회원가입 처리
    @PostMapping("/register")
    public String registerAction(@RequestParam String id, @RequestParam String password,
            @RequestParam String name, @RequestParam(required = false) String roles) {

        Member.Role role = (roles == null || roles.isEmpty()) ? Member.Role.USER : Member.Role.valueOf(roles.toUpperCase());

        Member newMember = new Member();
        newMember.setId(id);
        newMember.setPassword(password); // 원본 비밀번호 전달
        newMember.setName(name);
        newMember.setRoles(role);

        memberService.register(newMember);

        return "login"; // login.html 반환
    }


    // 로그인 페이지
    @GetMapping("/login")
    public String loginView() {
        return "login";
    }

    @GetMapping("/mypage")
    public String mypageView(Model model, Authentication authentication) {
        // 인증된 사용자 정보 가져오기
        Object principal = authentication.getPrincipal();  // 인증된 사용자의 전체 정보 (UserDetails)

        if (principal instanceof User) {
            User user = (User) principal;
            // 인증된 사용자의 ID와 역할 등을 모델에 추가
            model.addAttribute("member", user);
        }

        return "mypage";  // mypage.html 페이지 반환
    }


}
  • @Controller : 웹 컨트롤러임을 나타냅니다. 즉, HTTP 요청을 받고, 해당 요청에 대한 응답을 반환하는 역할을 합니다.
    이 클래스는 Spring MVC의 컨트롤러로 동작하며, URL 패턴에 따라 적절한 메서드를 호출합니다.
  • @RequestParam : HTML 폼에서 제출된 id, password, name, roles 값을 받습니다.
  • Authentication authentication : 현재 인증된 사용자의 정보를 가져옵니다.
  • Object principal = authentication.getPrincipal(); : 현재 인증된 사용자의 UserDetails 객체를 반환합니다.

 

MemberDetailService

package com.demo.service;

import java.util.List;
import java.util.Optional;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import com.demo.domain.Member;
import com.demo.repository.MemberRepository;

@Service
public class MemberService {

    private final MemberRepository memberRepo;
    private final PasswordEncoder passwordEncoder;

    // 단일 생성자로 의존성 자동 주입
    public MemberService(MemberRepository memberRepo, PasswordEncoder passwordEncoder) {
        this.memberRepo = memberRepo;
        this.passwordEncoder = passwordEncoder;
    }

    // 회원가입
    public Member register(Member member) {
        // 비밀번호 암호화
        member.setPassword(passwordEncoder.encode(member.getPassword()));

        // 기본 역할 설정
        if (member.getRoles() == null) {
            member.setRoles(Member.Role.USER);
        }

        return memberRepo.save(member);
    }


    // 사용자 아이디로 회원 찾기
    public Optional<Member> findById(String id) {
        return memberRepo.findById(id);
    }
    // 전체 회원 조회
    public List<Member> getAllMembers() {
        return memberRepo.findAll();
    }
}
  • implements UserDetailsService : Spring Security에서 사용자 인증 정보를 로드하기 위한 인터페이스입니다.

 

SecurityConfig

package com.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import com.demo.service.MemberDetailService;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
	
    @Autowired
    private MemberDetailService memberDetailService;

    // 비밀번호 암호화
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    // SecurityFilterChain 설정
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .requestMatchers("/login", "/register").permitAll() // 로그인, 회원가입은 누구나 접근 가능
                .requestMatchers("/admin/**").hasRole("ADMIN") // 관리자 페이지는 관리자만 접근 가능
                .anyRequest().authenticated() // 나머지 페이지는 인증된 사용자만 접근 가능
            .and()
            .formLogin()
                .loginPage("/login") // 커스텀 로그인 페이지 URL
                .loginProcessingUrl("/login") // 로그인 처리 URL
                .defaultSuccessUrl("/mypage", true) // 로그인 성공 후 마이페이지로 리다이렉트
                .failureUrl("/login?error=true") // 로그인 실패 시 에러 URL
            .and()
            .logout()
                .logoutUrl("/logout") // 로그아웃 URL
                .logoutSuccessUrl("/login?logout=true") // 로그아웃 후 리다이렉트 URL
            .and()
            .csrf().disable(); // CSRF 비활성화 (개발 환경에서는 활성화 권장)

        return http.build();
    }

    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberDetailService).passwordEncoder(passwordEncoder());
    }
}
  • @Configuration :이 클래스가 Spring의 설정 클래스임을 나타냅니다.
    Bean 정의를 포함하고, Spring 컨테이너에 필요한 객체를 등록합니다.
  • @EnableWebSecurity : Spring Security를 활성화하는 어노테이션입니다.
  • passwordEncoder() : new BCryptPasswordEncoder();를 사용해 안전한 방식으로 비밀번호를 암호화합니다.
  • .authorizeRequests() : HTTP 요청에 대한 접근 제어를 설정합니다
    .formLogin() : 로그인 폼 관련 설정을 정의합니다.
    .logout() : 로그아웃 설정
    .csrf().disable(); :  CSRF 보호 비활성화.
  • configure(AuthenticationManagerBuilder auth) : UserDetailsService와 PasswordEncoder를 설정하여 사용자 인증 로직과 비밀번호 암호화 방식을 Spring Security에 등록합니다.

Html (css는 생략)

- main.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>메인 페이지</title>
</head>
<body>
<div class="main">
	<h1>메인 화면</h1>
	<a th:href="@{/login}">로그인</a>
	<div th:if="${#lists.isEmpty(members)}">
		<h3>회원이 없습니다.</h3>
	</div>
	<div th:unless="${#lists.isEmpty(members)}">
		<table border="1">
			<thead>
				<tr>
					<th>아이디</th>
					<th>이름</th>
					<th>역할</th>
				</tr>
			</thead>
			<tbody>
				<tr th:each="member : ${members}">
					<td th:text="${member.id}"></td>
					<td th:text="${member.name}"></td>
					<td th:text="${member.roles}"></td>
				</tr>
			</tbody>
		</table>
	</div>
</div>
</body>
</html>

- login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>로그인 페이지</title>
</head>
<body>
<div class="login">
	<h1>로그인</h1>
	<form action="/login" method="post">
		<div>
			<input type="text" id="id" name="id" placeholder="아이디" required/>
		</div>
		
		<div>
			<input type="password" id="password" name="password" placeholder="비밀번호" required/>
		</div>
		<button type="submit">로그인</button>
	</form>
	<a th:href="@{/register}">회원가입</a>
</div>
</body>
</html>

- register.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>회원가입 페이지</title>
</head>
<body>
<div class="register">
	<h1>회원가입</h1>
	<form action="/register" method="post">
		<div>
			<input type="text" id="id" name="id" placeholder="아이디" required/>
		</div>
		<div>
			<input type="password" id="password" name="password" placeholder="비밀번호" required/>
		</div>
		<div>
			<input type="text" id="name" name="name" placeholder="이름" required/>
		</div>
		<button type="submit">회원가입</button>
	</form>
	<a th:href="@{/login}">로그인</a>
</div>
</body>
</html>

- mypage.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>마이페이지</title>
</head>
<body>
<div class="mypage">
    <h1>마이페이지</h1>
    <div class="card-container">
        <div class="card">
            <h2>아이디 : <span th:text="${member.id}"></span></h2>
            <p>이름 : <span th:text="${member.name}"></span></p>
            <p class="member-seq">회원 번호 : <span th:text="${member.memberSeq}"></span></p>
            <p>회원 정보 : <span th:text="${member.roles}"></span></p>
        </div>
    </div>
</div>
</body>
</html>

 

++ 추가 ++

Member의 정보를 담기 위한 Spring Security의 UserDetails 확장.. (mypage 구현에서 오류가 생겨서...)

package com.demo.domain;

import java.util.Collection;
import java.util.Collections;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class CustomUserDetails implements UserDetails {

	private final Member member;
	
	public CustomUserDetails(Member member) {
	    this.member = member;
	}
	
	public int getMemberSeq() {
	    return member.getMemberSeq();
	}
	
	public String getName() {
	    return member.getName();
	}
	
	public Member.Role getRoles() {
	    return member.getRoles();
	}
	
	@Override
	public String getUsername() {
	    return member.getId();
	}
	
	@Override
	public String getPassword() {
	    return member.getPassword();
	}
	
	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
	    return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + member.getRoles().name()));
	}
	
	@Override
	public boolean isAccountNonExpired() {
	    return true;
	}
	
	@Override
	public boolean isAccountNonLocked() {
	    return true;
	}
	
	@Override
	public boolean isCredentialsNonExpired() {
	    return true;
	}
	
	@Override
	public boolean isEnabled() {
	    return true;
	}

}

 

++ MemberDetailService 수정!!!

@Override
public UserDetails loadUserByUsername(String id) throws UsernameNotFoundException {
    // 회원 아이디로 조회
    Member member = memberRepo.findById(id)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + id));

    return new CustomUserDetails(member);
}

 

++ Controller 수정!!!

// 마이페이지
@GetMapping("/mypage")
public String mypageView(Model model, Authentication authentication) {
    Object principal = authentication.getPrincipal();

    if (principal instanceof CustomUserDetails) {
        CustomUserDetails userDetails = (CustomUserDetails) principal;
        model.addAttribute("memberSeq", userDetails.getMemberSeq());
        model.addAttribute("id", userDetails.getUsername());
        model.addAttribute("name", userDetails.getName());
        model.addAttribute("roles", userDetails.getRoles());
    }

    return "mypage";
}

 

++ mypage.html 수정!!!

<h2>아이디 : <span th:text="${id}"></span></h2>
<p>이름 : <span th:text="${name}"></span></p>
<p>회원 번호 : <span th:text="${memberSeq}"></span></p>
<p>회원 정보 : <span th:text="${roles}"></span></p>

 

***** 테스트 *****

***** 테스트 *****

***** 테스트 *****

***** 테스트 *****

로그인, 회원가입 페이지

 

회원가입 테스트

회원가입 폼 작성

 

회원가입 결과

console 창
데이터베이스에서는 비밀번호가 암호화 됨

 

로그인 테스트

<div>
    <input type="text" id="username" name="username" placeholder="아이디" required/>
</div>

* id와 name을 username으로 수정해주세요.. *

id로 하고 로그인 했더니 계속 실패해서....

spring security에서 로그인 처리 시 로그인 데이터(username,password)를 검색해서 처리하기 때문!!!

SecurityConfig에서 커스터마이징 해서 파라미터 이름 변경도 가능한데... 다음에 해볼게요!

로그인 폼 작성 후 로그인!
로그인 성공 >> 마이페이지

 

중간 중간 오류도 많이 나고, 천천히 공부하면서 하다보니 계속 수정하고 시간이 오래 걸렸지만...

어쨌든 성공!

- 끝 -