Я пишу новое приложение Spring Boot с нуля, чтобы лучше понять, как работает его конфигурация.Сейчас я пытаюсь создать контроль доступа, который не работает должным образом.
Моя система UserDetailsService использует пользовательский объект UserDTO для проверки:
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return new UserDTO(
userRepository.findByEmailAddress(email)
.orElseThrow(() -> new UsernameNotFoundException("User '" + email + "' not found."))
);
}
Сущность пользователя:
@Entity
@Table(name = "USERS")
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
public class User extends BaseEntity {
private String firstName;
private String lastName;
private String emailAddress;
private String nickname;
private String password;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
@ManyToMany
@JoinTable(
name = "USER_ROLE",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"))
private Set<Role> roles;
public Set<Role> getRoles() {
return roles;
}
}
Вот так выглядит класс UserDTO:
@Getter
@Setter
@NoArgsConstructor
public class UserDTO implements UserDetails {
private Long id;
private String firstName;
private String lastName;
private String username;
private String nickname;
private String password;
private Set<RoleDTO> authorities;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
public UserDTO(User user){
this.id = user.getId();
this.firstName = user.getFirstName();
this.lastName = user.getLastName();
this.username = user.getEmailAddress();
this.nickname = user.getNickname();
this.password = user.getPassword();
this.authorities = user.getRoles().stream().map(RoleDTO::new).collect(Collectors.toSet());
this.accountNonExpired = user.isAccountNonExpired();
this.accountNonLocked = user.isAccountNonLocked();
this.credentialsNonExpired = user.isCredentialsNonExpired();
this.enabled = user.isEnabled();
}
}
Таблица ролей в H2:
INSERT INTO PUBLIC.ROLES(ID, VERSION, AUTHORITY) VALUES
(1, 0, 'ROLE_USER'),
(2, 0, 'ROLE_MODERATOR'),
(3, 0, 'ROLE_ADMIN');
И контроллер:
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserSimpleDTO createUser(@RequestBody UserDTO user){
return userService.createUser(user);
}
@GetMapping("/{id}")
@ResponseStatus(HttpStatus.OK)
public UserSimpleDTO getUserGeneralData(@PathVariable long id){
return userService.getUserGeneralData(id);
}
@GetMapping("/{id}/details")
@ResponseStatus(HttpStatus.OK)
@RolesAllowed("ROLE_MODERATOR")
public UserDTO getUserDetailedInfo(@PathVariable long id) {
return userService.getUserDetailedInfo(id);
}
Моя ролевая сущность и классы DTO выглядят так:
@Entity
@Table(name = "ROLES")
@Getter
@NoArgsConstructor
public class Role extends BaseEntity implements GrantedAuthority {
private String authority;
}
RoleDTO:
@Getter
@NoArgsConstructor
public class RoleDTO implements GrantedAuthority {
private String authority;
public RoleDTO(Role role){
this.authority = role.getAuthority();
}
}
Когда я запускаю тесты как обычный пользователь, getUserDetailedInfo () возвращает 200 вместо 401, но у смоделированного объекта User явно есть только ROLE_USER
:
Чего не хватает, поэтому аннотация @RolesAllowed не работает должным образом, или где-то в этом есть ошибкаcode?
РЕДАКТИРОВАТЬ:
Есть набор тестов, который я использую для тестирования данного метода, который я забыл добавить:
@Test
@WithAnonymousUser
public void getUserDetailedInfoDoesNotAllowAnonymous2() {
getUserDetailedInfoREST(1, 401);
}
@Test
@WithMockUser
public void getUserDetailedInfoDoesNotAllowUser2() {
getUserDetailedInfoREST(1, 403);
}
@Test
@WithMockModerator
public void getUserDetailedInfoAllowsModerator2() {
getUserDetailedInfoREST(1, 200);
}
@Test
@WithMockAdmin
public void getUserDetailedInfoDAllowsAdmin2() {
getUserDetailedInfoREST(1, 200);
}
Это реализация теста getUserDetailedInfoREST ()Метод:
private String getUserDetailedInfoREST(long userId, int expectedStatus) {
try {
String response = this.mockMvc.perform(get("/users/" + userId + "/details"))
.andDo(print())
.andExpect(status().is(expectedStatus))
.andDo(this::mapMvcResultToUserDTO)
.andReturn().getResponse().getContentAsString();
return response;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
Мои пользовательские аннотации:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="admin",roles= {"USER", "MODERATOR", "ADMIN"})
public @interface WithMockAdmin {
}
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(value="moderator",roles= {"USER", "MODERATOR"})
public @interface WithMockModerator {
}
Я имеютакже проверил его, используя мои собственные MockHttpServletRequestBuilder
методы, которые передают реальных существующих пользователей в базу данных вместо поддельных, также хотел бы знать, является ли это хорошей или плохой практикой, поскольку мне трудно принять решение.Это делает тесты немного менее читабельными, но использование реальных пользователей вместо издевательских выглядит очень соблазнительно:
private static final String ADMIN_USERNAME = "admin@mail.com";
private static final String ADMIN_PASSWORD = "admin123";
public static MockHttpServletRequestBuilder getAsAdmin(String urlTemplate, Object... uriVars) {
return MockMvcRequestBuilders.get(urlTemplate, uriVars).with(httpBasic(ADMIN_USERNAME, ADMIN_PASSWORD));
}
//... and the other HTTP methods+users implementations ...