我编写了一个自动化的JUnit测试,该测试使用MockMvc击中了Spring Boot项目中所有公开的http端点,以查看是否可以使用无效角色访问它们(断言它们都是禁止的)。
但是,当最后的断言失败时,Spring Boot会使用有关每个嘲笑调用的信息来污染日志。我希望运行的开发人员看到不安全端点的简单列表,我在JUnit测试结束时记录了这些列表。最后将assert更改为assert(true)会驱动我想要的行为(仅打印不安全的方法)。
这是我的JUnit测试类:
package com.example.microservice.person.tests.controller;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.logging.LoggingSystem;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import com.example.microservice.person.TestAppConfiguration;
@RunWith(SpringRunner.class)
@Import(value= {TestAppConfiguration.class})
@WebMvcTest
public class UnauthorizedAccessTest {
//This class tries to make sure that every exposed HTTP endpoint is not accessible by a bad ROLE
Logger logger = LoggerFactory.getLogger(UnauthorizedAccessTest.class);
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired
private MockMvc mvc;
@BeforeClass
public static void setErrorLogging() {
//My attempt at turning off automatic logging
LoggingSystem.get(ClassLoader.getSystemClassLoader())
.setLogLevel(Logger.ROOT_LOGGER_NAME, LogLevel.OFF);
}
@Test
@WithMockUser(username="attacker", roles = {"DENYME"})
public void TestAllEndpointsUnauthorized() throws Exception {
List<String> unsecureMethods = new ArrayList<String>();
List<String> secureMethods = new ArrayList<String>();
for (RequestMappingInfo restMethod :
requestMappingHandlerMapping.getHandlerMethods().keySet()) {
for (String path: restMethod.getPatternsCondition().getPatterns()) {
Set<RequestMethod> methodTypes = restMethod.getMethodsCondition().getMethods();
if (methodTypes.contains(RequestMethod.GET)) {
try {
mvc.perform(get(path)).andExpect(status().isForbidden());
secureMethods.add("GET: " + path);
}
catch(AssertionError e) {
unsecureMethods.add("GET: " + path);
}
}
if (methodTypes.contains(RequestMethod.POST)) {
try {
mvc.perform(post(path)).andExpect(status().isForbidden());
secureMethods.add("POST: " + path);
}
catch(AssertionError e) {
unsecureMethods.add("POST: " + path);
}
}
if (methodTypes.contains(RequestMethod.PUT)) {
try {
mvc.perform(put(path)).andExpect(status().isForbidden());
secureMethods.add("PUT: " + path);
}
catch(AssertionError e) {
unsecureMethods.add("PUT: " + path);
}
}
if (methodTypes.contains(RequestMethod.DELETE)) {
try {
mvc.perform(delete(path)).andExpect(status().isForbidden());
secureMethods.add("DELETE" + path);
}
catch(AssertionError e) {
unsecureMethods.add("DELETE: " + path);
}
}
}
}
//Log report and error if needed
if (unsecureMethods.size() > 0) {
logger.error("Invalid roles were able to access the following rest methods");
for (String s : unsecureMethods) {
logger.error("\t" + s);
}
}
else {
logger.info("All rest methods denied access to the invalid role");
for (String s: secureMethods) {
logger.info("\t" + s);
}
}
assert(unsecureMethods.size() == 0);
}
}
注意:如果有一种更简洁(或更好的实践)方法对此进行测试,我绝对愿意看到这一点。我只是想一种方法来确保其他开发人员正在保护其端点如果没有,构建就会失败。
这是我希望日志显示的样子:
2019-02-21 08:31:40.959 ERROR 23628 --- [ main] c.e.m.p.t.c.UnauthorizedAccessTest : Invalid roles were able to access the following rest methods
2019-02-21 08:31:40.959 ERROR 23628 --- [ main] c.e.m.p.t.c.UnauthorizedAccessTest : GET: /cache/clearPersonCache
但是它会打印我想要的内容,然后在断言失败时,日志中会填充一堆无关的信息。每种REST方法都会填充它,而不仅仅是那些失败的方法。
2019-02-21 08:33:36.035 ERROR 21736 --- [ main] c.e.m.p.t.c.UnauthorizedAccessTest : Invalid roles were able to access the following rest methods
2019-02-21 08:33:36.035 ERROR 21736 --- [ main] c.e.m.p.t.c.UnauthorizedAccessTest : GET: /cache/clearPersonCache
MockHttpServletRequest:
HTTP Method = GET
Request URI = /cache/clearPersonCache
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = com.example.microservice.person.controller.CacheController
Method = private java.lang.Boolean com.example.microservice.person.controller.CacheController.clearPersonCache()
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"application/json;charset=UTF-8", X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = application/json;charset=UTF-8
Body = true
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = POST
Request URI = /person/updatePerson
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = POST
Request URI = /person/addPerson
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = GET
Request URI = /person/fixAgeOfPerson
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = GET
Request URI = /person/getPersonList
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = GET
Request URI = /person/getPersonById
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
MockHttpServletRequest:
HTTP Method = GET
Request URI = /person/averageAgeInBirthMonth
Parameters = {}
Headers = []
Body = <no character encoding set>
Session Attrs = {SPRING_SECURITY_CONTEXT=org.springframework.security.core.context.SecurityContextImpl@a63d2d27: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@a63d2d27: Principal: org.springframework.security.core.userdetails.User@201c88f5: Username: attacker; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_DENYME; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_DENYME}
Handler:
Type = null
Async:
Async started = false
Async result = null
Resolved Exception:
Type = null
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 403
Error message = Forbidden
Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"1; mode=block", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
因此,如果开发人员因该单元测试而导致构建失败,我认为发现不安全方法的工作将耗费大量精力。如何禁用在失败的断言上记录的JUnit / Spring并实现所需的记录?