프로젝트/SNS 프로젝트
[회원정보 수정] @AuthenticationPrincipal / Ajax사용
영카이브
2024. 3. 28. 22:09
Authentication이란?
Spring Security에서 사용자를 나타내는 중요한 객체
Spring Security에서는 사용자의 인증 및 인가 정보를 Authentication객체에 담는다.
즉 이 객체는 현재 사용자를 나타내는 핵심 객체이며, 사용자가 인증되면 이 객체가 생성되어야 한다.
@AuthenticationPrincipal 이란?
@AuthenticationPrincipal 어노테이션을 사용하면 세션에 저장된 현재 사용자의 정보를 쉽게 찾을 수 있다.
예를 들어 로그인 후 세션에 저장된 사용자 정보를 Authentication 객체를 통해 가져오려면 일련의 과정을 거쳐야한다.
그러나 @AuthenticationPrincipal을 사용하면 이 과정을 간소화할 수 있다.
@GetMapping("/user/{id}/update")
public String update(@PathVariable int id,
// 해당 세션 접근
@AuthenticationPrincipal PrincipalDetails principalDetails ) {
// 추천
System.out.println("세션정보: " + principalDetails.getUser());
// 극혐
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
PrincipalDetails mPrincipalDetails = (PrincipalDetails)auth.getPrincipal();
System.out.println("직접 찾은 세션정보: "+mPrincipalDetails.getUser());
return "user/update";
}
로그인 후 세션에 저장된 사용자 정보를 Authentication 객체를 통해 가져오려면 일련의 과정
- /auth/signin으로 POST 요청이 발생하면 Spring Security는 POST 요청을 인식하고, 해당 요청을 낚아채서 자체적인 인증 프로세스를 진행한다.
- 이때, 낚아챈 요청은 PrincipalDetailsService로 전달된다. PrincipalDetailsService는 Spring Security에서 제공하는 UserDetailsService 인터페이스를 구현한 클래스로, 사용자 정보를 조회하고 인증을 처리한다.
- PrincipalDetailsService 내부에서는 전달받은 username을 사용하여 해당 사용자 정보를 조회(인증)한다. 만약 해당 사용자가 존재하지 않으면 인증에 실패하고 예외를 발생시킨다.
- 사용자가 존재한다면, 해당 사용자 정보를 단순히 담은 PrincipalDetails 객체를 생성한다.
- 이 PrincipalDetails 객체를 Spring Security가 인식하고 관리하게 하려면 Authentication 객체에 포함시켜야한다. 인증된 사용자라는 뜻이다. 이 Authentication 객체는 세션 영역( 현재 사용자의 상태를 유지하는 데 사용되는 메모리 공간 ) 안의 SecurityContextHolder라는 객체에 저장된다. 즉 SecurityContextHolder 객체 안에 Authentication이 있다.
- 이렇게 인증된 사용자 정보를 찾기 위해서는 @AuthenticationPrincipal 어노테이션을 사용하면 세션 영역에 저장된 Authentication 객체를 쉽게 가져올 수 있으며, 그 안에 포함된 PrincipalDetails를 찾을 수 있다.
따라서 @AuthenticationPrincipal을 사용하면 별도의 코드 없이도 세션에 저장된 사용자 정보에 접근할 수 있어서 개발 과정이 간소화된다는 장점이 있다.
뷰에 현재 사용자의 인증 정보 띄우는 방법
pom.xml에 시큐리티 태그 라이브러리 추가
<!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
</dependency>
<sec:authorize> 태그 안에서 <sec:authentication> 태그를 사용하여 현재 사용자의 인증 정보를 가져올 수 있다. property 속성을 사용하여 principal 값을 가져와 var 속성을 사용하여 변수에 저장한다.
장점: 인증정보를 가져올 때 Model 객체를 사용하는 방법은 추가적인 Java 코드를 작성하지않아도 된다.
header는 공통으로 들어가므로 header.jsp에 인증정보를 가져와 공통으로 여러 페이지에 적용시킬 수 있도록 하였다.
header.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<sec:authorize access="isAuthenticated()">
<sec:authentication property="principal" var="principal"/>
</sec:authorize>
update.jsp
<div class="item__input">
<input type="text" name="name" placeholder="이름"
value="${principal.user.name}" />
</div>
Ajax 사용하기
회원정보 수정을 위해서는 put.method를 사용해야 한다.
update.jsp의 form태그 안에 method는 put설정이 불가하다. 따라서 자바스크립트를 사용해야 한다.
// (1) 회원정보 수정
function update(userId, event) {
let data = $("#profileUpdate").serialize();
console.log(data);
// 데이터를 응답해줘야한다.
$.ajax({
type: "put",
url: "/api/user/" + userId,
data: data,
contentType: "application/x-www-form-urlencoded; charset=utf-8", // contentType의 오타 수정
dataType: "json"
}).done(res => {
console.log("update 성공");
location.href ="/user/"+ userId;
}).fail(error => {
console.log("update 실패");
});
}
참고!
직렬화(Serialize) : 데이터를 일련의 바이트 스트림으로 변환하는 과정
자바스크립트에서는 주로 jQuery의 serialize() 함수를 사용하여 HTML 폼 요소의 입력 값을 URL 인코딩된 문자열로 반환한다.
직렬화 장점 : 직렬화된 데이터를 사용하면 자바스크립트에서 폼 데이터를 쉽게 추출하고 전송한다.
ex) name=%EC%8C%80&username=ssar&password=&website=&bio=123&email=ssar%40nate.com&tel=&gender=23
회원정보 수정 과정을 담은 자바 코드
UserApiController
@RequiredArgsConstructor
@RestController
public class UserApiController {
private final UserService userService;
// 데이터 트랜스젝션 오브젝트
@PutMapping("/api/user/{id}")
public CMRespDto<?> update(@PathVariable int id,UserUpdateDto userUpdateDto
,@AuthenticationPrincipal PrincipalDetails principalDetails){
User userEntity = userService.회원수정(id, userUpdateDto.toEntity());
principalDetails.setUser(userEntity);// 세션정보 변경
return new CMRespDto<>(1,"회원수정완료", userEntity);
}
}
UserUpdateDto
@Data
public class UserUpdateDto {
private String name; //필수값
private String password; //필수값
private String website;
private String bio;
private String phone;
private String gender;
// 필수값이 아닌값까지 고려해서 Entity를 만드는 것은 위험함
// 사용자가 이름,패스워드 기재 안하면 DB에 공백이 들어가므로 validation체크해야함
public User toEntity() {
return User.builder()
.name(name) // Validation 체크
.password(password) // Validation 체크
.website(website)
.bio(bio)
.phone(phone)
.gender(gender)
.build();
}
}
별도의 ApiController로 만들어주어야 하는데 왜냐하면 ajax통신을 하면 파일(페이지)를 응답하는게 아닌 data를 응답하기 때문이다. data를 응답하는 것을 api라고 부른다. 따라서 ApiController에는 @RestController라고 어노테이션을 붙인다. 클라이언트에서 서버로 데이터를 전송하는 경우 DTO를 사용한다. 그리고 필수데이터와 아닌 데이터는 분기시켜야한다. 왜냐하면 클라이언트가 만약 password같은 필수값을 기재하지 않고 수정을 한다면 DB에 공백인 상태 그대로 UPDATE되기 때문이다. 이것은 유효성검사를 할 때 진행하겠다.
서비스단에서 회원정보수정 로직을 진행하고 data를 응답하기위해 user 오브젝트를 받아주어야한다. toEntity 메서드는 DTO에서 엔티티로 변환하는 역할을 담당한다. 그리고 String타입의 리턴이 아니라 앞서 만들었던 공통응답 DTO를 이용한다.
참고! 공통응답 DTO
@AllArgsConstructor
@NoArgsConstructor
@Data
public class CMRespDto<T> {
private int code; // 1(성공),-1(실패)
private String message;
private T data;
}
UserService - 자세한 내용은 다음 페이지에서..
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Transactional
public User 회원수정(int id,User user) {
// 1. 영속화
// (1) get(): 무조건 찾았다.걱정마
// (2) orElseThrow(): 못찾았으니 exception 발동
User userEntity = userRepository.findById(id).get();
// 2. 영속화된 오브젝트 수정
userEntity.setName(user.getName());
String rawPassword = user.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
userEntity.setPassword(encPassword);
userEntity.setBio(user.getBio());
userEntity.setWebsite(user.getWebsite());
userEntity.setPhone(user.getPhone());
userEntity.setGender(user.getGender());
return userEntity;
// 수정이 끝나면 더티체킹이 일어나서 업데이트 완료
}
}