본문 바로가기

SpringBoot

SpringBoot(24) - 복습 예제(개인정보 수정), 추가 정리사항(AuthenticatedUser, @Transactional)

728x90
반응형

1) SpringBoot

   1-1) 복습 예제

      1-1-1) 개인정보 수정 기능 구현

   1-2) 추가 정리사항(AuthenticatedUser, @Transactional) 

 

 

 

 

 

1) SpringBoot

1-1) 복습 예제

1-1-1) 개인정보 수정 기능 구현

(1) [src/main/resources] - [templates] 안에 home.html 파일 생성 후 아래와 같이 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
		xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
	<meta charset="UTF-8">
	<title>web5</title>
</head>
<body>
	<h1>[ web5 ]</h1>
	<p sec:authorize="isAuthenticated()">
		<span th:text="${#authentication.name}"></span>
		님 환영합니다~~!
	</p>
	
	<div sec:authorize="not isAuthenticated()">
		<p>
			<a th:href="@{/member/joinForm}">회원가입</a>
		</p>
		<p>
			<a th:href="@{/member/loginForm}">로그인</a>
		</p>
	</div>
	
	<div sec:authorize="isAuthenticated()">
		<p>
			<a th:href="@{/member/logout}">로그아웃</a>
		</p>
		<p>
			<a th:href="@{/member/info}">개인정보 수정</a>
		</p>
	</div>
	
	<p>
		<a href="">게시판</a>
	</p>
	
</body>
</html>

 

 

(2) [src/main/java] - [net.datasa.web5.security] 안에 WebSecurityConfig.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
    //로그인 없이 접근 가능한 경로
    private static final String[] PUBLIC_URLS = {
            "/"                 //메인화면
            , "/member/joinForm"          //로그인 없이 접근할 수 있는 페이지
            , "/member/join"
            , "/member/idCheck"
            , "/images/**"
            , "/css/**"
            , "/js/**"
    };

    // Bean : 메모리에 만들어진 객체(즉, 객체를 생성해서 메모리에 로드시켜 놓는 역할을 한다!)
    @Bean
    protected SecurityFilterChain config(HttpSecurity http) throws Exception {
        http
            //요청에 대한 권한 설정
            .authorizeHttpRequests(author -> author
                .requestMatchers(PUBLIC_URLS).permitAll()   //모두 접근 허용
                // 모든 요청은 인증이 필요하다는 코드이며, 바로 위의 코드만 인증 없이 모두 접근 가능하다!
                .anyRequest().authenticated()               //그 외의 모든 요청은 인증 필요
            )
            //HTTP Basic 인증을 사용하도록 설정
            .httpBasic(Customizer.withDefaults())
            //폼 로그인 설정
            .formLogin(formLogin -> formLogin
                    .loginPage("/member/loginForm")              //로그인폼 페이지 경로
                    .usernameParameter("memberId")               //폼의 ID 파라미터 이름
                    .passwordParameter("memberPassword")         //폼의 비밀번호 파라미터 이름
                    .loginProcessingUrl("/member/login")         //로그인폼 제출하여 처리할 경로
                    .defaultSuccessUrl("/")                      //로그인 성공 시 이동할 경로
                    .permitAll()                                 //로그인 페이지는 모두 접근 허용
            )
            //로그아웃 설정
            .logout(logout -> logout
                    .logoutUrl("/member/logout")                 //로그아웃 처리 경로
                    .logoutSuccessUrl("/")                       //로그아웃 성공 시 이동할 경로
            );

        http
            .cors(AbstractHttpConfigurer::disable)
            .csrf(AbstractHttpConfigurer::disable);

        return http.build();
    }

    //비밀번호 암호화를 위한 인코더를 빈으로 등록
    @Bean
    public BCryptPasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

}

 

 

(3) [src/main/java] - [net.datasa.web5.security] 안에 AuthenticatedUser.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.security;

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;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AuthenticatedUser implements UserDetails {
	// 객체 직렬화
	private static final long serialVersionUID = 1562050567301951305L;
	
	String id;
	String password;
	String roleName;
	boolean enabled;
	String name;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		// "ROLE_USER", "ROLE_ADMIN"
		return Collections.singletonList(new SimpleGrantedAuthority(roleName));
	}

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

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

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

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

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

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

}

 

 

(4) [src/main/java] - [net.datasa.web5.security] 안에 AuthenticatedUserDetailsService.java 파일 생성 후

아래와 같이 작성

package net.datasa.web5.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import jakarta.persistence.EntityNotFoundException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.repository.MemberRepository;

@Slf4j
@Service
@RequiredArgsConstructor
public class AuthenticatedUserDetailsService implements UserDetailsService {

	private final BCryptPasswordEncoder passwordEncoder;
	private final MemberRepository memberRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		// 전달받은 아이디로 회원정보 DB에서 조회
		// 없으면 예외
		MemberEntity entity = memberRepository.findById(username)
				.orElseThrow(() -> new EntityNotFoundException("회원정보가 없습니다."));
		
		// 있으면 그 정보로 UserDetails 객체 생성하여 리턴함
		AuthenticatedUser user = AuthenticatedUser.builder()
				.id(username)
				.password(entity.getMemberPassword())
				.name(entity.getMemberName())
				.roleName(entity.getRolename())
				.enabled(entity.getEnabled())
				.build();
		log.debug("인증정보: {}", user);
		
		return user;
	}

}

 

 

(5) [src/main/java] - [net.datasa.web5.controller] 안에 MemberController.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.controller;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.MemberDTO;
import net.datasa.web5.security.AuthenticatedUser;
import net.datasa.web5.service.MemberService;

/**
 * 회원정보 관련 Controller
 * */

@Slf4j
@RequestMapping("member")
@RequiredArgsConstructor
@Controller
public class MemberController {
	private final MemberService memberService;
	
	/**
	 * 회원가입 양식으로 이동
	 * */
	@GetMapping("joinForm")
	public String join() {
		return "memberView/joinForm";
	}
	
	@PostMapping("join")
	public String join(@ModelAttribute MemberDTO member) {
		log.debug("전달된 회원정보 : {}", member);
		
		// 서비스로 전달하여 저장
		memberService.memberJoin(member);
		
		return "redirect:/";
	}
	
	@GetMapping("idCheck")
	public String idCheck() {
		return "memberView/idCheck";
	}
	
	@PostMapping("idCheck")
	public String idCheck(
			@RequestParam("searchId") String searchId,
			Model model) {
		// ID 중복확인 폼에서 전달된 검색할 아이디를 받아서 log 출력
		log.debug("전달된 검색 ID : {}", searchId);
		
		// 서비스의 메소드로 검색할 아이디를 전달해서 조회
		// 해당 아이디를 쓰는 회원이 있으면 false, 없으면 true 리턴 받음
		boolean result = memberService.idCheck(searchId);
		
		log.debug("전달된 ID 검색 결과 : {}", result);
		
		// 검색한 아이디와 조회결과를 모델에 저장
		model.addAttribute("searchId", searchId);
		model.addAttribute("result", result);
		
		// 검색 페이지로 다시 이동
		return "memberView/idCheck";
	}
	
	/**
	 * 로그인 양식으로 이동
	 * */
	@GetMapping("loginForm")
	public String login() {
		return "memberView/loginForm";
	}
	
	/**
	 * 개인정보 수정 페이지로 이동
	 * @param model 모델
	 * @param user 현재 로그인한 사용자의 정보
	 * @return     수정폼 HTML 경로
	 * */
	@GetMapping("info")
	public String info(
			@AuthenticationPrincipal AuthenticatedUser user,
			Model model) {
		// 현재 사용자의 아이디를 서비스로 전달하여 해당 사용자 정보를 MemberDTO 객체로 리턴받는다.
		log.debug("회원 아이디 : {}", user.getUsername());
		MemberDTO memberDTO = memberService.getMember(user.getUsername());
		
		// MemberDTO 객체를 모델에 저장하고 HTML 폼으로 이동
		model.addAttribute("member", memberDTO);
		
		return "memberView/infoForm";
	}
	
	@PostMapping("info")
	public String info(@AuthenticationPrincipal AuthenticatedUser user,
			@ModelAttribute MemberDTO memberDTO) {
		log.debug("수정폼에서 전달된 값 : {}", memberDTO);
		
		// 수정폼에서 전달한 값들을 MemberDTO로 받는다.
		// 현재 로그인한 사용자의 아이디를 MemberDTO 객체에 추가한다.
		memberDTO.setMemberId(user.getUsername());
		
		// MemberDTO 객체를 서비스로 전달하여 DB를 수정한다.
		memberService.updateMember(memberDTO);
		
		// 메인화면으로 redirect한다.
		return "redirect:/";
	}
	
}

 

 

(6) [src/main/resources] - [templates] 안에 infoForm.html 파일 생성 후 아래와 같이 작성

<!DOCTYPE html>
<html lang="ko" xmlns:th="http://thymeleaf.org"
	xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
	<meta charset="UTF-8">
	<title>개인정보 수정</title>
	<style>
		#infoArea {
			width: 700px;
			margin: 0 auto;
			text-align: center;
		}
		
		#updateForm {
			display: flex;
			flex-direction: column;
			justify-content: center;
		}
		
				
		#formArea {
			display: flex;
			justify-content: center;
		}
		
		table, tr, td {
			border: 2px solid black;
        	border-collapse: collapse;
        	padding: 10px;
        	margin: 20px 0 15px 0;
		}
		
		th {
			width: 100px;
			background-color: grey;
			color: white;
		}
		
		td {
			width: 300px;
			text-align: left;
		}
		
		#idCheckBtn {
			margin-left: 20px;
		}
		
		td > .memberInfo {
			width: 200px;
			height: 25px;
		}
		
		#pwInputCol > div {
			height: 10px;
		}
		
		td > #address {
			width: 260px;
			height: 25px;
		}
		
	</style>
	<script th:src="@{/js/jquery-3.7.1.min.js}"></script>
	<script>
	$(document).ready(function() {
		$("#updateForm").submit(check);
	});
	
	function check() {
		let pw = $("#memberPassword").val();
		let pw2 = $("#memberPwCheck").val();
		let name = $("#memberName").val();
		
		if (pw != '' && (pw.length < 8 || pw.length > 12)) {
			alert("비밀번호는 8자 이상 12자 이하의 글자를 반드시 입력해주세요!!");
			$("#memberPassword").focus();
			$("#memberPassword").val('');
			
	        return false;
		}
		
		if (pw != pw2) {
			alert("입력하신 비밀번호가 일치하지 않습니다.\n확인 후 다시 입력해주세요!!");
			$("#memberPwCheck").focus();
			$("#memberPwCheck").val('');
			
	        return false;
		}
		
		if (name.length == 0) {
			alert("이름은 반드시 입력해주세요!!");
			$("#memberName").focus();
			$("#memberName").val('');
			
	        return false;
		}
		
		return true;
		
	}
	
	</script>
</head>
<body>
	<div id="infoArea">
		<h1>[ 개인정보 수정 ]</h1>
		<form th:action="@{/member/info}" method="post" id="updateForm">
			<div id="formArea">
				<table>
					<tr>
						<th>ID</th>
						<td>[[${member.memberId}]]</td>
					</tr>
					<tr>
						<th>
							<label for="memberPassword">비밀번호</label>
						</th>
						<td id="pwInputCol">
							<input type="password" name="memberPassword" id="memberPassword" class="memberInfo" placeholder="비밀번호를 변경하려면 입력" />
							<br>
							<div></div>
							<input type="password" id="memberPwCheck" class="memberInfo" placeholder="비밀번호 다시 입력" />
						</td>
					</tr>
					<tr>
						<th>이름</th>
						<td>
							<input type="text" name="memberName" id="memberName" class="memberInfo" th:value="${member.memberName}" />
						</td>
					</tr>
					<tr>
						<th>이메일</th>
						<td>
							<input type="text" name="email" id="email" class="memberInfo" th:value="${member.email}" />
						</td>
					</tr>
					<tr>
						<th>전화번호</th>
						<td>
							<input type="text" name="phone" id="phone" class="memberInfo" th:value="${member.phone}" />
						</td>
					</tr>
					<tr>
						<th>주소</th>
						<td>
							<input type="text" name="address" id="address" th:value="${member.address}" />
						</td>
					</tr>
				</table>
			</div>
			
	        <div id="btnArea">
	        	<input type="submit" value="수정" />
	        	<input type="reset" value="다시 쓰기" />
	        </div>
		</form>
	</div>
</body>
</html>

 

 

(7) [src/main/java] - [net.datasa.web5.service] 안에 MemberService.java 파일 생성 후 아래와 같이 작성

package net.datasa.web5.service;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import jakarta.persistence.EntityNotFoundException;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.datasa.web5.domain.dto.MemberDTO;
import net.datasa.web5.domain.entity.MemberEntity;
import net.datasa.web5.repository.MemberRepository;

/**
 * 회원정보 관련 처리 서비스
 * */

@Slf4j
@RequiredArgsConstructor
@Service
// @Transactional : Commit, Rollback을 감지하는 역할을 하는 Annotation
@Transactional
public class MemberService {
	private final MemberRepository memberRepository;
	private final BCryptPasswordEncoder passwordEncoder;
	
	/**
	 * 가입 처리
	 * */
	public void memberJoin(MemberDTO dto) {
		MemberEntity entity = MemberEntity.builder()
				// DTO로 전달받은 값들을 Entity에 세팅
				.memberId(dto.getMemberId())
				.memberPassword(passwordEncoder.encode(dto.getMemberPassword()))
				.memberName(dto.getMemberName())
				.email(dto.getEmail())
				.phone(dto.getPhone())
				.address(dto.getAddress())
				// 기타 추가 데이터를 Entity에 세팅
				.enabled(true)
				.rolename("ROLE_USER")
				.build();
		
		// DB에 저장
		memberRepository.save(entity);
		
	}

	/**
	 * 전달받은 아이디를 DB에서 조회하여 사용중인지 여부 리턴
	 * @param searchId 조회할 아이디
	 * @return 있으면 사용불가 false, 없으면 사용가능 true
	 * */
	public boolean idCheck(String searchId) {
		
		return !memberRepository.existsById(searchId);
		
		/**
		 * // 바로 위의 코드와 동일한 기능을 수행하는 코드이다.
		 * // findById() : Primary Key 기준으로 검색을 하는 메서드(findById() : Optional 객체를 반환함)
		 * // orElse(null) : 결과가 없으면 "null" 값을 대입하는 메서드
		 * if (memberRepository.findById(searchId).isPresent()) {
			return false;
		} else {
			return true;
		}
		*/
		
	}

	public MemberDTO getMember(String username) {
		MemberEntity entity = memberRepository.findById(username)
				.orElseThrow(() -> new EntityNotFoundException("아이디가 없습니다."));
		
		MemberDTO dto = MemberDTO.builder()
				.memberId(entity.getMemberId())
				.memberName(entity.getMemberName())
				.email(entity.getEmail())
				.phone(entity.getPhone())
				.address(entity.getAddress())
				.build();
		
		return dto;
		
	}

	/**
	 * 개인정보 수정
	 * @param memberDTO 수정할 정보
	 * */
	public void updateMember(MemberDTO memberDTO) {
		MemberEntity entity = memberRepository.findById(memberDTO.getMemberId())
				.orElseThrow(() -> new EntityNotFoundException("아이디가 없습니다."));
		
		entity.setMemberName(memberDTO.getMemberName());
		entity.setEmail(memberDTO.getEmail());
		entity.setPhone(memberDTO.getPhone());
		entity.setAddress(memberDTO.getAddress());
		
		// 비밀번호는 전달된 값이 있을 때에만 암호화해서 수정
		if (memberDTO.getMemberPassword() != null && !memberDTO.getMemberPassword().isEmpty()) {
			entity.setMemberPassword(passwordEncoder.encode(memberDTO.getMemberPassword()));
		}
		
		// 리턴될 때 DB에 저장됨
		memberRepository.save(entity);
	}

}

 

 

(8) 결과 화면

첫 접속 화면

 

개인정보 수정 화면(비밀번호 수정한 경우 / 수정하지 않은 경우)

- 수정할 비밀번호를 새로 입력하지 않으면 기존 비밀번호로 로그인 가능하도록 처리함

- 비밀번호를 수정하기 위해 비밀번호 입력란에 새로 입력한 경우에는 수정한 비밀번호로 로그인 가능하도록 처리함

- 나머지 내용들(email, phone, address 등)은 모두 새로 수정할 입력값을 받아 Service로 넘겨줌

- 위의 수정사항이 모두 DB에 반영되도록 Service에서 처리함

 

 

 

 

 

 

 

1-2) 추가 정리사항(AuthenticatedUser, @Transactional)

AuthenticatedUserDetailsService.java의 역할 : 로그인 정보(비밀번호 일치, enabled 값이 true일 때)가 맞을 경우에만

현재 로그인한 사용자 정보를 담고 있는 객체(AuthenticatedUser 객체)를 생성해주고,

로그인 정보가 일치하지 않을 경우에는 AuthenticatedUser 객체를 생성하지 않는다!

 

${#authentication} : AuthenticatedUser 객체(Session에 저장되어 있는 값)
${#authentication.name} : AuthenticatedUser 객체(현재 로그인한 사용자 정보) 중 아이디

 

@Transactional : Commit, Rollback을 감지하는 역할을 하는 Annotation