회원 수정 페이지
UserController.java
@GetMapping("/user/updateForm")
public String updateForm() {
return "user/updateForm";
}
현재 로그인된 회원의 정보를 수정해야 하니까 그 유저의 id값을 받기 위해 updateForm에서 hidden값으로 id를 적어둔다.
user/updateForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ include file="../layout/header.jsp"%>
<div class="container">
<form>
<input type="hidden" id="id" value="${principal.user.id}"/>
<div class="form-group">
<label for="username">Username</label>
<input type="text" value="${principal.user.username}" class="form-control" placeholder="Enter username" id="username" readonly>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" class="form-control" placeholder="Enter password" id="password">
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" value="${principal.user.email}" class="form-control" placeholder="Enter email" id="email">
</div>
</form>
<button id="btn-update" class="btn btn-primary">회원수정완료</button>
</div>
<script src="/js/user.js"></script>
<%@ include file="../layout/footer.jsp"%>
user.js
let index = {
init: function() {
...
$("#btn-update").on("click", () => {
this.update();
});
},
...
update: function(){
let data = {
id: $("#id").val(),
password: $("#password").val(),
email: $("#email").val()
};
$.ajax({
type: "PUT",
url: "/user",
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json"
}).done(function(resp){
alert("회원수정이 완료되었습니다.");
location.href="/";
}).fail(function(error){
alert(JSON.stringify(error));
});
},
}
index.init();
UserApiController.java
@PutMapping("/user")
public ResponseDto<Integer> update(@RequestBody User user) {
userService.회원수정(user);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
UserService.java
@Transactional
public void 회원수정(User user) {
// 수정시에는 영속성 컨텍스트라는 곳에 User 오브젝트를 영속화 시키고, 영속화된 User 오브젝트를 수정해야 한다.
// select를 해서 User 오브젝트를 DB에서 가져오는 이유는 영속화하기 위해!
// 영속화된 오브젝트를 변경하면 자동으로 DB에 update문을 날려주기 때문이다!
User persistance = userRepository.findById(user.getId()).orElseThrow(()->{
return new IllegalArgumentException("회원 찾기 실패");
});
String rawPassword = user.getPassword();
String encPassword = encoder.encode(rawPassword); // 암호화
persistance.setPassword(encPassword);
persistance.setEmail(user.getEmail());
// 회원 수정 함수 종료시 = 서비스 종료시 = 트랜잭션 종료시 -> 자동 커밋!!
// 영속화된 persistance 객체의 변화가 감지되면 더티체킹이 되어 변화된 것들 자동으로 update!
}
이제 회원 수정을 시도하면 DB값은 바뀌지만 회원 정보 탭에서는 바뀌지 않았다. 다시 로그아웃한 후 회원 정보 탭을 확인해보면 바뀌어있다. 이유는 DB만 바뀌고 세션값은 안바뀌었기 때문이다. 세션의 값을 변경해줘야 한다.
세션값 변경하기
UserApiController에서 userService의 회원수정 함수가 끝난 직후에 트랜잭션이 종료되기 때문에 DB 값은 update가 되지만 세션값은 변경되지 않은 상태여서 직접 변경해줘야 한다.
이를 위해 스프링 시큐리티가 어떻게 로그인 되고, 어떻게 세션이 만들어지는 지에 대한 개념이 필요하다.
SecurityContextHolder 안에 SeecurityContext가 있고 그 안에 Authentication이라는 객체가 들어가는데 들어가는 순간부터 세션에 값이 저장이 되는 상태이다. 이때부터 필요한 곳에서 @AuthenticationPrincipal로 Authentication 객체를 가져올 수 있다.
사용자의 로그인 요청이 들어왔을 때 AuthenticationFilter가 먼저 작동하는데 Http Body에 있는 username과 password로 토큰을 생성한다. 이 토큰을 매니저에게 넘겨주면 매니저는 UserDetailsService에 토큰을 넘겨줘서 DB에 해당 유저 정보를 조회한다. 만약 있으면 Authentication 객체를 만들어서 세션에 저장하는 것이다. 없으면 안만든다.
우리가 만든 UserDetailService에서는 토큰이 아닌 username를 넘겨줬었다. username을 넘겨줘서 DB 조회 후 있으면 PrincipaDetail에 던져서 세션을 만드는 방식이다.
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User principal = userRepository.findByUsername(username)
.orElseThrow(()->{
...
});
return new PrincipalDetail(principal);
}
password는 따로 스프링이 가져가서 인코딩 작업을 한다. SecurityConfig를 보면 매니저가 들고있는 passwordEncoder로 인코딩을 해서 매니저는 그 값을 알고있게 된다.
@Bean
public BCryptPasswordEncoder encodePWD() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(principalDetailService).passwordEncoder(encodePWD());
}
매니저가 UserDetailsService에는 username만 날리고 password는 자기가 BCrypt인코더로 인코딩해서 password 비교를 하는 것이다.
흐름 정리
그렇다면 강제로 세션을 만드려고 할 때 어떻게 해야할까? 강제로 만들때도 마찬가지로 위의 로직을 따라야 한다.
UserApiController.java
@PutMapping("/user")
public ResponseDto<Integer> update(@RequestBody User user,
@AuthenticationPrincipal PrincipalDetail principal, HttpSession session) {
userService.회원수정(user);
// 여기서는 트랜잭션이 종료되기 때문에 DB에 값은 변경되지만
// 세션 값은 변경되지 않은 상태이기 때문에 직접 세션값을 변경해줘야 한다.
// 토큰을 만들어서 Authentication 객체 만들기
Authentication authentication =
new UsernamePasswordAuthenticationToken(principal, null);
// SecurityContextHolder 안의 SecurityContext 접근
SecurityContext securityContext = SecurityContextHolder.getContext();
// 강제로 세션값 바꾸기
securityContext.setAuthentication(authentication);
// 세션에 담기 - 정해진 규칙
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
이렇게 하면 될 줄 알았지만? 경고창이 뜨면서 바뀌지 않는다... 아무래도 직접 Authentication을 만들어서 넣어주는 건 시큐리티에서 막는 것 같다. 그러면 그냥 AuthenticationManager에 접근 후 강제 로그인 해서 Authentication을 만들면 자동으로 컨텍스트에 넣어줄텐데 이 방법으로 바꿔보자
SecurityConfig에서 authenticationManagerBean을 오버라이딩 해준 후 Bean에 등록해주자
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
// TODO Auto-generated method stub
return super.authenticationManagerBean();
}
UserService에서 @Autowired로 AuthenticationManager을 가져오고 세션 등록을 해준다.
@Service
public class UserService {
...
@Autowired
private AuthenticationManager authenticationManager;
...
@Transactional
public void 회원수정(User user) {
...
// 세션 등록
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
근데 이렇게 해도 안된다... 왜냐하면 user.getUsername()과 user.getPassword()가 아직 DB 변경이 안된 상태이기 때문이다. 회원수정() 메서드가 끝나야 즉, 서비스가 끝나야 DB가 update되니까 서비스에서 세션등록을 하면 안된다.
UserApiController.java에서 세션등록을 해보자
@RestController
public class UserApiController {
...
@Autowired
private AuthenticationManager authenticationManager;
....
@PutMapping("/user")
public ResponseDto<Integer> update(@RequestBody User user) {
...
// 세션 등록
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
...
}
}
또 안된다... 이유는 @RequestBody로 받은 user에는 username이 없기 때문이다! 수정시에 username은 수정 못하도록 설정하고 user.js에서 아예 username을 안받았어서 null 상태인 것이다.
user.js에서 username을 받아주자!
username: $("#username").val(),
잘 작동한다!!!
출처 : https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9
'Spring > Blog 만들기 with SpringBoot' 카테고리의 다른 글
댓글 시스템 (0) | 2022.09.06 |
---|---|
카카오 로그인 구현 (0) | 2022.08.21 |
게시판 (0) | 2022.07.25 |
비밀번호 해쉬 & 시큐리티 로그인 (0) | 2022.07.24 |
스프링부트 Spring Security (0) | 2022.07.14 |