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
'SpringBoot' 카테고리의 다른 글
SpringBoot(26) - 복습 예제(게시판 글 목록 보기) (0) | 2024.07.31 |
---|---|
SpringBoot(25) - 복습 예제(글쓰기) (0) | 2024.07.30 |
SpringBoot(23) - 복습 예제(비밀번호 암호화 / 로그인 / 로그아웃), 추가 정리사항(경로) (0) | 2024.07.26 |
SpringBoot(22) - 복습 예제(회원가입 시 ID 중복 체크) (0) | 2024.07.25 |
SpringBoot(21) - 복습 예제(회원가입 기능 구현) (0) | 2024.07.24 |