问题描述
我想使用访问控制来测试我的 Spring Boot 应用程序:用户不必访问其他人的资源。
@Entity
@Table(name = "USERS")
@Data
public class User implements Serializable {
@Serial
private static final long serialVersionUID = -6521572023157125222L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true)
@NotNull
@Size(min = 4,max = 15)
private String username;
@Column
@NotNull
@JsonIgnore
private String password;
@OnetoMany(fetch = FetchType.LAZY,cascade = CascadeType.ALL,orphanRemoval = true,mappedBy = "user")
@JsonManagedReference(value = "user")
private Set<Purchase> purchaseSet;
@ManyToOne
@JoinColumn(name = "role_id",nullable = false)
@EqualsAndHashCode.Exclude
@JsonBackReference
private Role role;
@ManyToMany(mappedBy = "userSet")
@JsonManagedReference(value = "userSet")
@EqualsAndHashCode.Exclude
private Set<Group> sharedGroupsSet;
}
让我们关注 sharedGroupsSet:可以添加用户(或添加他自己)的组列表。组实体如下(我不能将表命名为“组”......我不知道为什么,但如果表名与“组”不同,则方案是正确的,否则不是(关系不好和主键)。所以我称这个表为“teams”。第一个问题:有人知道为什么这个名字(使用 MysqL)不起作用?):
@Entity
@Table(name = "TEAMS")
@Data
public class Group implements Serializable {
@Serial
private static final long serialVersionUID = 1172517945693877297L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
private String name;
@NotNull
private String description;
@OnetoMany(fetch = FetchType.LAZY,mappedBy = "group")
@JsonManagedReference(value = "purchases_of_group")
private Set<Purchase> purchaseSet;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable
@JsonManagedReference(value = "userSet")
private Set<User> userSet;
@ManyToMany(cascade = CascadeType.ALL)
@JoinTable
@JsonManagedReference(value = "categorySet")
private Set<Category> categorySet;
}
所以我决定为 db 上的查询添加一个授权级别(以授予访问控制):我写了一个 GroupAuthorizationService,如下所示:
@Service
@Log
@Data
public class GroupAuthorizationService {
@Autowired
AuthorizationService authorizationService;
public boolean hasAccess(Optional<Group> group) {
if (authorizationService.isAuthorizationdisabled()){
return true;
}
log.info("Controllo autorizzazioni per Optional<Group>");
AuthenticatedUser authenticatedUser = (AuthenticatedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (group.isEmpty()) {
return true;
} else {
return group.get().getUserSet().stream().anyMatch(u -> u.getId().equals(authenticatedUser.getId()));
}
}
public boolean hasAccess(Group group) {
if (authorizationService.isAuthorizationdisabled()){
return true;
}
if(group == null){
return true;
}
log.info("Controllo autorizzazioni per gruppo " + group.getName());
AuthenticatedUser authenticatedUser = (AuthenticatedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return group.getUserSet().stream().anyMatch(u -> u.getId().equals(authenticatedUser.getId()));
}
public boolean hasAccess(Iterable<Group> groups) {
if (authorizationService.isAuthorizationdisabled()){
return true;
}
log.info("Controllo autorizzazioni per la lista di utenti");
AuthenticatedUser authenticatedUser = (AuthenticatedUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
boolean isCorrect = true;
for (Group team : groups) {
if (team.getUserSet().stream().noneMatch(u -> u.getId().equals(authenticatedUser.getId()))) {
isCorrect = false;
break;
}
}
return isCorrect;
}
}
我在 GroupRepository 中使用它:
@Repository
@PostAuthorize("@groupAuthorizationService.hasAccess(returnObject)")
public interface GroupRepository extends JpaRepository<Group,Long> {
Group findGroupByName(String name);
}
此服务允许我在我的 API 中添加访问控制:用户不能访问所有组的数据,而只能访问他们自己的数据。也可以使用@Autowired AuthorizationService 禁用访问控制:
@Service
public class AuthorizationService {
private boolean disableAuthorization;
public void enableAuthorization(){
this.disableAuthorization = false;
}
public void disableAuthorization(){
this.disableAuthorization = true;
}
public boolean isAuthorizationdisabled(){
return this.disableAuthorization;
}
}
第二个问题:这是一种有效的方法吗?将 @PostAuthorize 用于存储库级别,我确保应用程序中的任何地方都无法访问无效资源。我还没有找到在所有 api 方法中使用 @PostAuthorize 的更好解决方案,所以我创建了我的!
最后,我使用 JWT 过滤器实现了身份验证:
@Component
@Log
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserService userService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Qualifier("handlerExceptionResolver")
@Autowired
private HandlerExceptionResolver resolver;
@SneakyThrows
@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain chain)
throws ExpiredJwtException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get
// only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (CustomJwtException e) {
log.warning("Errore nel filtro " + e.getMessage());
resolver.resolveException(request,response,null,e);
return;
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
AuthenticatedUser authenticatedUser = this.userService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set
// authentication
if (jwtTokenUtil.validatetoken(jwtToken,authenticatedUser)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
authenticatedUser,authenticatedUser.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context,we specify
// that the current user is authenticated. So it passes the
// Spring Security Configurations successfully.
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request,response);
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
prePostEnabled = true,securedEnabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private UserService userService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it kNows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
// We don't need CSRF for this example
httpSecurity.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/api/authenticate","/api/register","/","/favicon.ico").permitAll()
// all other requests need to be authenticated
.anyRequest().authenticated().and()
// make sure we use stateless session; session won't be used to store user's state.
.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
httpSecurity.addFilterBefore(jwtRequestFilter,UsernamePasswordAuthenticationFilter.class);
}
}
API 工作正常!但如果我想测试它,我有一些问题:
@ActiveProfiles("test")
@ContextConfiguration(classes = SpeseApiApplication.class)
@SpringBoottest
@Log
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class GroupControllerTest {
@Autowired
EntityManager entityManager;
@Autowired
private WebApplicationContext wac;
@Autowired
private GroupRepository groupRepository;
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private PasswordEncoder bcryptEncoder;
@Autowired
private AuthorizationService authorizationService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
private mockmvc mockmvc;
private User user1;
private User user2;
private User user3;
private String token;
private final String username1 = "Riccardo 1";
private final String password1 = "passw0rd1";
private final String jsonUser1;
private final String username2 = "Riccardo 2";
private final String password2 = "passw0rd2";
private final String jsonUser2;
private final String username3 = "Riccardo 3";
private final String password3 = "passw0rd3";
private final String groupName1 = "Gruppo Riccardo 1";
private final String groupDescription1 = "Descrizione Gruppo Riccardo 1";
private final String groupName2 = "Gruppo Riccardo 2";
private final String groupDescription2 = "Descrizione Gruppo Riccardo 2";
private final String jsonGroup2;
private final String groupName3 = "Gruppo Riccardo 3";
private final String groupDescription3 = "Descrizione Gruppo Riccardo 3";
private final String jsonGroup3;
GroupControllertest() throws JsonProcessingException {
jsonUser1 = generateJsonUserDTO(username1,password1);
jsonUser2 = generateJsonUserDTO(username2,password2);
jsonGroup2 = generateJsonGroupDTO(
groupName2,groupDescription2,new HashSet<>(Arrays.asList()),new HashSet<>(Arrays.asList(2))
);
jsonGroup3 = generateJsonGroupDTO(
groupName3,groupDescription3,new HashSet<>(Arrays.asList(2,3))
);
}
@BeforeEach
public void setup() {
//
//SecurityContextHolder.getContext().setAuthentication(
// new AnonymousAuthenticationToken(
// "GUEST",// "USERNAME",// AuthorityUtils.createAuthorityList("GUEST")
// )
//);
authorizationService.disableAuthorization();
user1 = new User();
user1.setUsername(username1);
user1.setPassword(bcryptEncoder.encode(password1));
user1.setRole(roleRepository.findByName(RoleEnum.ROLE_USER));
user2 = new User();
user2.setUsername(username2);
user2.setPassword(bcryptEncoder.encode(password2));
user2.setRole(roleRepository.findByName(RoleEnum.ROLE_USER));
user3 = new User();
user3.setUsername(username3);
user3.setPassword(bcryptEncoder.encode(password3));
user3.setRole(roleRepository.findByName(RoleEnum.ROLE_USER));
Group group = new Group();
group.setName(groupName1);
group.setDescription(groupDescription1);
group.setUserSet(new HashSet<>(Arrays.asList(user1)));
group.setCategorySet(new HashSet<>());
group.setPurchaseSet(new HashSet<>());
groupRepository.save(group);
userRepository.save(user2);
userRepository.save(user3);
authorizationService.enableAuthorization();
mockmvc = mockmvcBuilders
.webAppContextSetup(wac)
.addFilters(jwtRequestFilter)
.build();
// SecurityContextHolder.getContext().setAuthentication(null);
}
public void authenticateUser(String jsonUser) throws Exception {
ResultActions resultActions = mockmvc.perform(mockmvcRequestBuilders.post("/api/authenticate")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonUser)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.token").exists())
.andDo(print());
String resultString = resultActions.andReturn().getResponse().getContentAsstring();
JacksonjsonParser jsonParser = new JacksonjsonParser();
token = jsonParser.parseMap(resultString).get("token").toString();
}
@Test
public void createGroupTwoTimes() throws Exception {
authenticateUser(jsonUser2);
mockmvc.perform(mockmvcRequestBuilders.post("/api/groups/create")
.header("authorization","Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonGroup2)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andDo(print());
mockmvc.perform(mockmvcRequestBuilders.post("/api/groups/create")
.header("authorization","Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonGroup2)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isNotAcceptable())
.andDo(print());
Group groupFromDb = groupRepository.findGroupByName(groupName2);
assertthat(groupFromDb).extracting(Group::getName).isEqualTo(groupName2);
assertthat(groupFromDb).extracting(Group::getDescription).isEqualTo(groupDescription2);
assertEquals(2,groupFromDb.getId());
assertEquals(0,groupFromDb.getPurchaseSet().size());
assertEquals(0,groupFromDb.getCategorySet().size());
assertEquals(1,groupFromDb.getUserSet().size());
assertTrue(groupFromDb.getUserSet().stream().anyMatch(u ->
u.getId().equals(user2.getId())
));
assertTrue(groupFromDb.getUserSet().stream().noneMatch(u ->
u.getId().equals(user1.getId())
));
assertTrue(groupFromDb.getUserSet().stream().noneMatch(u ->
u.getId().equals(user3.getId())
));
mockmvc.perform(mockmvcRequestBuilders.post("/api/groups/create")
.header("authorization","Bearer " + token)
.contentType(MediaType.APPLICATION_JSON)
.content(jsonGroup3)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andDo(print());
groupFromDb = groupRepository.findGroupByName(groupName3);
assertthat(groupFromDb).extracting(Group::getName).isEqualTo(groupName3);
assertthat(groupFromDb).extracting(Group::getDescription).isEqualTo(groupDescription3);
assertEquals(3,groupFromDb.getCategorySet().size());
assertEquals(2,groupFromDb.getUserSet().size());
assertTrue(groupFromDb.getUserSet().stream().anyMatch(u ->
u.getId().equals(user2.getId())
));
assertTrue(groupFromDb.getUserSet().stream().anyMatch(u ->
u.getId().equals(user3.getId())
));
assertTrue(groupFromDb.getUserSet().stream().noneMatch(u ->
u.getId().equals(user1.getId())
));
}
private String generateJsonGroupDTO(String groupName,String groupDescription,Set<Integer> purchaseSet,Set<Integer> useRSSet) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
GroupDTO groupDTO = new GroupDTO();
groupDTO.setName(groupName);
groupDTO.setDescription(groupDescription);
groupDTO.setPurchaseSet(purchaseSet);
groupDTO.setUserSet(useRSSet);
return objectMapper.writeValueAsstring(groupDTO);
}
private String generateJsonUserDTO(String username,String password) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
UserDTO userDTOOk = new UserDTO();
userDTOOk.setUsername(username);
userDTOOk.setPassword(password);
return objectMapper.writeValueAsstring(userDTOOk);
}
}
如果我运行它,我会得到这个错误:
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:333)
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:200)
at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:58)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at com.sun.proxy.$Proxy139.save(UnkNown Source)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:208)
at com.sun.proxy.$Proxy108.save(UnkNown Source)
at gabellini.company.speseapi.GroupControllerTest.setup(GroupControllerTest.java:165)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptLifecycleMethod(TimeoutExtension.java:126)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptBeforeEachMethod(TimeoutExtension.java:76)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeMethodInExtensionContext(ClassBasedTestDescriptor.java:490)
at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$synthesizeBeforeEachMethodAdapter$19(ClassBasedTestDescriptor.java:475)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeEachMethods$2(TestMethodTestDescriptor.java:167)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeBeforeMethodsOrCallbacksUntilExceptionOccurs$5(TestMethodTestDescriptor.java:195)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeMethodsOrCallbacksUntilExceptionOccurs(TestMethodTestDescriptor.java:195)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeBeforeEachMethods(TestMethodTestDescriptor.java:164)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:127)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
所以我找到了一个 hack(但我认为这不是一个好的解决方案):
我取消注释这行
//SecurityContextHolder.getContext().setAuthentication(
// new AnonymousAuthenticationToken(
// "GUEST",// AuthorityUtils.createAuthorityList("GUEST")
// )
//);
还有:
// SecurityContextHolder.getContext().setAuthentication(null);
设置经过身份验证的用户(匿名)。但我不认为这是一个有效的方法。 而且我不明白为什么我必须使用:
mockmvc = mockmvcBuilders
.webAppContextSetup(wac)
.addFilters(jwtRequestFilter)
.build();
@Autowire
private mockmvc mockmvc;
所以第三个问题是:测试我的 API 的最佳方法是什么?我想编写测试,包括首先使用 JWT 进行身份验证,然后调用端点(“api/groups/create”),最后测试是否经过身份验证的用户有权创建该资源)。需要注意的是,用户只有在组的 userSet 中包含他的 id 才能创建组。
解决方法
暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!
如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。
小编邮箱:dio#foxmail.com (将#修改为@)