Event Controller Test를 수정하여 Spring Security가 제대로 적용됐는 지 확인해야한다.
고려해야할 사항은 다음과 같다
public String getToken() throws Exception {
String clientId = "myapp";
String clientSecret = "pass";
Account account = createAccount();
accountService.create(account);
var response = mockMvc.perform(post("/oauth/token")
.with(httpBasic(clientId, clientSecret))
.param("username", account.getUsername())
.param("password", "password")
.param("grant_type", "password")
);
var responseString = response.andReturn().getResponse().getContentAsString();
Jackson2JsonParser paser = new Jackson2JsonParser();
return "Bearer " + paser.parseMap(responseString).get("access_token").toString();
}
public String getToken(Account account) throws Exception {
String clientId = "myapp";
String clientSecret = "pass";
var response = mockMvc.perform(post("/oauth/token")
.with(httpBasic(clientId, clientSecret))
.param("username", account.getUsername())
.param("password", "password")
.param("grant_type", "password")
);
var responseString = response.andReturn().getResponse().getContentAsString();
Jackson2JsonParser paser = new Jackson2JsonParser();
return "Bearer " + paser.parseMap(responseString).get("access_token").toString();
}
@Test
public void create_event_success() throws Exception {
//Given
EventDto eventDto = createEventDto();
//When
//Then
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, getToken())
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isCreated())
.andExpect(jsonPath("event").exists())
.andExpect(header().exists(HttpHeaders.LOCATION))
.andExpect(header().string(HttpHeaders.CONTENT_TYPE,MediaTypes.HAL_JSON_VALUE))
.andExpect(jsonPath("_links.self").exists())
.andExpect(jsonPath("_links.query-events").exists())
.andExpect(jsonPath("_links.profile").exists())
.andExpect(jsonPath("_links.update-event").exists());
}
@Test
public void create_event_unauthorized_without_token() throws Exception {
//Given
EventDto eventDto = createEventDto();
//When
//Then
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
public void get_event_success() throws Exception {
//Given
Event event = createEvent(0);
this.eventRepository.save(event);
//When
//Then
mockMvc.perform(get("/api/events/{id}", event.getId()))
.andDo(print())
.andExpect(jsonPath("event").exists())
.andExpect(jsonPath("_links").exists());
}
@Test
public void get_event_success_with_token() throws Exception {
//Given
Event event = createEvent(0);
this.eventRepository.save(event);
//When
//Then
mockMvc.perform(get("/api/events/{id}", event.getId())
.header(HttpHeaders.AUTHORIZATION, getToken())
)
.andDo(print())
.andExpect(jsonPath("event").exists())
.andExpect(jsonPath("_links").exists())
.andExpect(jsonPath("_links.create-events").exists());
}
@Test
public void update_event_success() throws Exception {
//Given
Account account = createAccount();
Event event = createEvent(11123, account);
accountService.create(account);
eventRepository.save(event);
String newDescription = "new description";
EventDto eventDto = EventDto.builder()
.name(event.getName())
.description(newDescription)
.build();
//When
//Then
mockMvc.perform(put("/api/events/{id}", event.getId())
.header(HttpHeaders.AUTHORIZATION, getToken(account))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(jsonPath("event.name").value(event.getName()))
.andExpect(jsonPath("event.description").value(newDescription))
.andExpect(jsonPath("_links").exists())
.andExpect(jsonPath("_links.profile").exists());
}
@Test
public void update_event_unauthorized_without_token() throws Exception {
//Given
Account account = createAccount();
Event event = createEvent(11123, account);
accountService.create(account);
eventRepository.save(event);
String newDescription = "new description";
EventDto eventDto = EventDto.builder()
.name(event.getName())
.description(newDescription)
.build();
//When
//Then
mockMvc.perform(put("/api/events/{id}", event.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isUnauthorized());
}
@Test
public void update_event_unauthorized_wrong_account() throws Exception {
//Given
Account account = createAccount();
Account wrongAccount = Account.builder()
.name("wrong")
.password("password")
.roles(Set.of(AccountRole.ADMIN, AccountRole.USER))
.build();
Event event = createEvent(11123, account);
accountService.create(account);
accountService.create(wrongAccount);
eventRepository.save(event);
String newDescription = "new description";
EventDto eventDto = EventDto.builder()
.name(event.getName())
.description(newDescription)
.build();
//When
//Then
mockMvc.perform(put("/api/events/{id}", event.getId())
.header(HttpHeaders.AUTHORIZATION, getToken(wrongAccount))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(eventDto)))
.andDo(print())
.andExpect(status().isUnauthorized());
}
기본적으로 Event에서 Update와 Create는 권한이 없으면 진행할 수 없다.
따라서 @PreAuthorize 어노테이션을 활용해서 USER나 ADMIN 권한이 있는 경우에만 create와 update를 사용할 수 있도록 설정해준다.
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public Event create(EventDto eventDto){
...
}
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public Event update(Integer id, EventDto eventDto){
...
}
강의를 내가 필요한 부분만 들었기 때문에(중간 중간 생략...) Create Link 부분을 누락했다. 따라서 지금 급하게 구현한다.
private void addCreateLink(RepresentationModel eventResource){
WebMvcLinkBuilder queryLink = linkTo(methodOn(EventController.class).create(new EventDto()));
eventResource.add(queryLink.withRel("create-events"));
}
그리고 이 CreateLink와 UpdateLink는 로그인이 되어 있는 경우에만 제공하도록 해야 한다.
이를 위해선 현재 로그인한 유저 정보를 가져와야 한다. Spring security에서는 Provider에서 발급하는 AuthenticationProvider에 저장한다. 그 중에서 유저 정보는 principal 객체로 관리한다. 따라서 이 principal 객체를 가져와야 한다.
AuthenticationProvider를 가져오기 위해선 SecurityContextHolder.getContext().getAuthentication() 메소드를 사용해야하며 principal 객체는 getPrincipal을 통해 가져올 수 있다.
이때 중요한 점은 이번 프로젝트에서는 annoymouse 사용자를 허용했다는 것이다. annoymouse 사용자는 미인증유저로 AuthenticationProvider내 principal 객체에 null 값을 가지고 있다.
따라서 getPrincipal()을 했을 때 가져오는 객체가 Account.class이면 create & update link를 제공해야 한다.
(추가 설명 : principal 객체는 UserDetailService의 loadUserByUsername 메소드에서 반환되는 객체랑 같다.)
addLink() 메소드를 다음과 같이 수정합니다.
private void addLinks(EventResource eventResource, String profileLink){
var principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principal.getClass().equals(Account.class)){
addCreateLink(eventResource);
addUpdateLink(eventResource);
}
addQueryLink(eventResource);
addSelfLink(eventResource);
addProfileLink(eventResource, profileLink);
}
Event 수정은 작성자만 할 수 있어야 한다.
이 부분은 Spring Security로 관리하기 어렵다.
따라서 별도의 Method로 관리하고자 한다.
우선 principal을 가져오기 와서 Event 객체의 Manager와 같은 지 비교하는 함수를 구현합니다.
private Account getPrincipal(){
return (Account)SecurityContextHolder.getContext().
getAuthentication().getPrincipal();
}
private void validEventManager(Event event){
Account currentAccount = getPrincipal();
if(!event.getManager().equals(currentAccount))
throw new CustomException(HttpStatus.UNAUTHORIZED, "이벤트 작성자가 아닙니다.");
}
그 후 Event Update 로직으로 가서 validEventManager 함수를 추가해줍니다.
@PreAuthorize("hasAnyRole('USER','ADMIN')")
public Event update(Integer id, EventDto eventDto){
Event event = this.read(id);
validEventManager(event);
event.update(eventDto);
return this.eventRepository.save(event);
}