예전에 한 회사에서 면접을 볼 때 "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>
***** 테스트 *****
***** 테스트 *****
***** 테스트 *****
***** 테스트 *****
회원가입 테스트
회원가입 결과
로그인 테스트
<div>
<input type="text" id="username" name="username" placeholder="아이디" required/>
</div>
* id와 name을 username으로 수정해주세요.. *
id로 하고 로그인 했더니 계속 실패해서....
spring security에서 로그인 처리 시 로그인 데이터(username,password)를 검색해서 처리하기 때문!!!
SecurityConfig에서 커스터마이징 해서 파라미터 이름 변경도 가능한데... 다음에 해볼게요!
중간 중간 오류도 많이 나고, 천천히 공부하면서 하다보니 계속 수정하고 시간이 오래 걸렸지만...
어쨌든 성공!
- 끝 -
'개인 공부' 카테고리의 다른 글
JWT를 사용한 Spring Security 기반 인증 시스템 구현 (0) | 2024.12.27 |
---|---|
Spring Security를 활용한 사용자 권한 기반 접근 제어 (0) | 2024.12.24 |
MVC 패턴이 뭔데... (1) | 2024.12.18 |