Spring Boot 测试和访问控制:在 SecurityContext

问题描述

我想使用访问控制来测试我的 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);
    }
}

添加到 WebSecurityConfig

@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();

而不是只使用@AutoConfiguremockmvc

@Autowire
private mockmvc mockmvc;

所以第三个问题是:测试我的 API 的最佳方法是什么?我想编写测试,包括首先使用 JWT 进行身份验证,然后调用端点(“api/groups/create”),最后测试是否经过身份验证的用户有权创建该资源)。需要注意的是,用户只有在组的 userSet 中包含他的 id 才能创建组。

如果我必须分享更多代码以便更好地理解,请告诉我 提前致谢

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)