From a2b22aca88282077b25b01d2857645629e7eaaad Mon Sep 17 00:00:00 2001 From: fengbaichao Date: Sun, 18 Apr 2021 10:41:20 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BD=BF=E7=94=A8PowerMockRunner=E5=92=8CM?= =?UTF-8?q?ockito=E7=BC=96=E5=86=99=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E8=AF=A6=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: fengbaichao --- docs/basis/PowerMockRunnerAndMockito.md | 177 ++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 docs/basis/PowerMockRunnerAndMockito.md diff --git a/docs/basis/PowerMockRunnerAndMockito.md b/docs/basis/PowerMockRunnerAndMockito.md new file mode 100644 index 0000000..dbc2b0a --- /dev/null +++ b/docs/basis/PowerMockRunnerAndMockito.md @@ -0,0 +1,177 @@ +单元测试可以提高测试开发的效率,减少代码错误率,提高代码健壮性,提高代码质量。在Spring框架中常用的两种测试框架:PowerMockRunner和SpringRunner两个单元测试,鉴于SpringRunner启动的一系列依赖和数据连接的问题,推荐使用PowerMockRunner,这样能有效的提高测试的效率,并且其提供的API能覆盖的场景广泛,使用方便,可谓是Java单元测试之模拟利器。 + +#### 1. PowerMock是什么? + +PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无法测试的测试问题。使用PowerMock,可以模拟静态方法,删除静态初始化程序,允许模拟而不依赖于注入,等等。PowerMock通过在执行测试时在运行时修改字节码来完成这些技巧。PowerMock还包含一些实用程序,可让您更轻松地访问对象的内部状态。 + + +举个例子,你在使用Junit进行单元测试时,并不想让测试数据进入数据库,怎么办?这个时候就可以使用PowerMock,拦截数据库操作,并模拟返回参数。 + +#### 2. PowerMock包引入 + +```xml + + + org.powermock + powermock-core + 2.0.2 + test + + +org.mockito +mockito-core +2.23.0 + + +org.powermock +powermock-module-junit4 +2.0.4 +test + + +org.powermock +powermock-api-mockito2 +2.0.2 +test + + +com.github.jsonzou +jmockdata +4.3.0 + + +``` + +#### 3. 重要注解说明 + +```java +@RunWith(PowerMockRunner.class) // 告诉JUnit使用PowerMockRunner进行测试 +@PrepareForTest({RandomUtil.class}) // 所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类 +@PowerMockIgnore("javax.management.*") //为了解决使用powermock后,提示classloader错误 +``` + + + +#### 4. 使用示例 + +##### 4.1 模拟接口返回 + +首先对接口进行mock,然后录制相关行为 + +```java +InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) + + Powermockito.when(mock.method(Params…)).thenReturn(value) + + Powermockito.when(mock.method(Params..)).thenThrow(Exception) +``` + +##### 4.2 设置对象的private属性 + +需要使用whitebox向class或者对象中赋值。 + +如我们已经对接口尽心了mock,现在需要将此mock加入到对象中,可以采用如下方法: + +```java +Whitebox.setInternalState(Object object, String fieldname, Object… value); +``` + +其中object为需要设置属性的静态类或对象。 + +##### 4.3 模拟构造函数 + +对于模拟构造函数,也即当出现new InstanceClass()时可以将此构造函数拦截并替换结果为我们需要的mock对象。 + +注意:使用时需要加入标记: + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ InstanceClass.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.whenNew(InstanceClass.class).thenReturn(Object value) +``` + +##### 4.4 模拟静态方法 + +模拟静态方法类似于模拟构造函数,也需要加入注释标记。 + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ StaticClassToMock.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(StaticClassToMock.class); + + Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +##### 4.5 模拟final方法 + +Final方法的模拟类似于模拟静态方法。 + +```java +@RunWith(PowerMockRunner.class) + +@PrepareForTest({ FinalClassToMock.class }) + +@PowerMockIgnore("javax.management.\*") + +Powermockito.mockStatic(FinalClassToMock.class); + + Powermockito.when(StaticClassToMock.method(Object.. params)).thenReturn(Object value) +``` + +##### 4.6 模拟静态类 + +模拟静态类类似于模拟静态方法。 + +##### 4.7 使用spy方法避免执行被测类中的成员函数 + +如被测试类为:TargetClass,想要屏蔽的方法为targetMethod. + +```java +1) PowerMockito.spy(TargetClass.class); + + 2) Powemockito.when(TargetClass.targetMethod()).doReturn() + + 3) 注意加入 + +@RunWith(PowerMockRunner.class) + +@PrepareForTest(DisplayMoRelationBuilder.class) + +@PowerMockIgnore("javax.management.*") +``` + +##### 4.8 参数匹配器 + +有时我们在处理doMethod(Param param)时,不想进行精确匹配,这时可以使用Mockito提供的模糊匹配方式。 + +如:Mockito.anyInt(),Mockito.anyString() + +##### 4.9 处理public void型的静态方法 + +```java +Powermockito.doNothing.when(T class2mock, String method, … params> +``` + +#### 5. 单元测试用例可选清单 + +输入数据验证:这些检查通常可以对输入到应用程序系统中的数据采用。 + +- 必传项测试 +- 唯一字段值测试 +- 空值测试 +- 字段只接受允许的字符 +- 负值测试 +- 字段限于字段长度规范 +- 不可能的值 +- 垃圾值测试 +- 检查字段之间的依赖性 +- 等效类划分和边界条件测试 +- 错误和异常处理测试 \ No newline at end of file From 0dffa1aca96c9c8148f59049968960ca963d99ae Mon Sep 17 00:00:00 2001 From: fengbaichao Date: Mon, 19 Apr 2021 23:56:50 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=BA=90=E7=A0=81?= =?UTF-8?q?=E5=88=86=E6=9E=90Spring=20Security=E7=94=A8=E6=88=B7=E8=AE=A4?= =?UTF-8?q?=E8=AF=81=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...g-security-login-authentication-process.md | 196 ++++++++++++++++++ docs/basis/PowerMockRunnerAndMockito.md | 8 +- 2 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 docs/advanced/spring-security-login-authentication-process.md diff --git a/docs/advanced/spring-security-login-authentication-process.md b/docs/advanced/spring-security-login-authentication-process.md new file mode 100644 index 0000000..89d0328 --- /dev/null +++ b/docs/advanced/spring-security-login-authentication-process.md @@ -0,0 +1,196 @@ +Spring Security的登录主要是由一系列的过滤器组成,我们如果需要修改登录的校验逻辑,只需要在过滤器链路上添加修改相关的逻辑即可。这里主要通过Spring Security的源码来了解相关的认证登录的逻辑。 + +#### 1.Spring Security的认证流程 + +主要分析: + +1. 认证用户的流程 +2. 如何进行认证校验 +3. 认证成功后怎么获取用户信息 + +具体的过滤器链路如下所示: + +[![cT2G4g.png](https://z3.ax1x.com/2021/04/19/cT2G4g.png)](https://imgtu.com/i/cT2G4g) + +Spring Security的认证流程图如下,认证的主要过程有: + +1. 用户提交用户名和密码,然后通过UsernamePasswordAuthenticationFilter对其进行封装成为UsernamePasswordAuthenticationToken对象,这个是AbstractAuthenticationToken的子类,而AbstractAuthenticationToken又是Authentication的一个实现,所以可以看到后续获取的都是Authentication类型的对象实例; +2. 将第一步的UsernamePasswordAuthenticationToken对象传递给AuthenticationManager; +3. 通过AbstractUserDetailsAuthenticationProvider的默认实现类DaoAuthenticationProvider的retrieveUser方法,这个方法会调用UserDetailsService的loadUserByUsername方法来进行用户名和密码的判断,使用的默认的逻辑进行处理; +4. 将成功认证后的用户信息放入到SecurityContextHolder中,之后可以通过SecurityContext获取用户的相关信息。 + +[![coGpvR.png](https://z3.ax1x.com/2021/04/19/coGpvR.png)](https://imgtu.com/i/coGpvR) + +spring-security源码下载地址: + +```java +https://github.com/spring-projects/spring-security +``` + +#### 2.Spring Security的认证源码分析 + +##### 2.1 搭建项目并访问 + +首先我们搭建一个Spring Security的项目,使用Spring Boot可以很方便的进行集成开发,主要引入如下的依赖即可(当然也可以查看官网,选择合适的版本): + +```java + + org.springframework.boot + spring-boot-starter-security + +``` + +启动项目后会随机生成一个密码串,这里需要复制保存以便登录的时候使用: + +[![coJ0ld.png](https://z3.ax1x.com/2021/04/19/coJ0ld.png)](https://imgtu.com/i/coJ0ld) + +访问登录地址: + +```java +http://localhost:8080/login +``` + +[![coJfpQ.png](https://z3.ax1x.com/2021/04/19/coJfpQ.png)](https://imgtu.com/i/coJfpQ) + +默认的账户名和密码: + +```java +账户名: user +密码: 项目启动时生成的密码串 +``` + +##### 2.2 进行源码分析 + +1. 进行断点后会发现首先进入的是UsernamePasswordAuthenticationFilter的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法,会对用户名和密码进行封装成UsernamePasswordAuthenticationToken对象,然后调用this.getAuthenticationManager().authenticate(authRequest)方法进入到AuthenticationManager中。 + + attemptAuthentication方法源码如下所示: + +```java +@Override + public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) + throws AuthenticationException { + if (this.postOnly && !request.getMethod().equals("POST")) { + throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); + } + String username = obtainUsername(request); + username = (username != null) ? username : ""; + username = username.trim(); + String password = obtainPassword(request); + password = (password != null) ? password : ""; + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + // Allow subclasses to set the "details" property + setDetails(request, authRequest); + return this.getAuthenticationManager().authenticate(authRequest); + } +``` + +2. 随后请求进入到WebSecurityConfigurerAdapter的AuthenticationManagerDelegator中,AuthenticationManagerDelegator是AuthenticationManager的一个子类,最后封装成为UsernamePasswordAuthenticationToken对象,供DaoAuthenticationProvider使用。 + + AuthenticationManagerDelegator的源码如下: + + ```java + static final class AuthenticationManagerDelegator implements AuthenticationManager { + private AuthenticationManagerBuilder delegateBuilder; + private AuthenticationManager delegate; + private final Object delegateMonitor = new Object(); + private Set beanNames; + + AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder, ApplicationContext context) { + Assert.notNull(delegateBuilder, "delegateBuilder cannot be null"); + Field parentAuthMgrField = ReflectionUtils.findField(AuthenticationManagerBuilder.class, "parentAuthenticationManager"); + ReflectionUtils.makeAccessible(parentAuthMgrField); + this.beanNames = getAuthenticationManagerBeanNames(context); + validateBeanCycle(ReflectionUtils.getField(parentAuthMgrField, delegateBuilder), this.beanNames); + this.delegateBuilder = delegateBuilder; + } + + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (this.delegate != null) { + return this.delegate.authenticate(authentication); + } else { + synchronized(this.delegateMonitor) { + if (this.delegate == null) { + this.delegate = (AuthenticationManager)this.delegateBuilder.getObject(); + this.delegateBuilder = null; + } + } + + return this.delegate.authenticate(authentication); + } + } + + private static Set getAuthenticationManagerBeanNames(ApplicationContext applicationContext) { + String[] beanNamesForType = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, AuthenticationManager.class); + return new HashSet(Arrays.asList(beanNamesForType)); + } + + private static void validateBeanCycle(Object auth, Set beanNames) { + if (auth != null && !beanNames.isEmpty() && auth instanceof Advised) { + TargetSource targetSource = ((Advised)auth).getTargetSource(); + if (targetSource instanceof LazyInitTargetSource) { + LazyInitTargetSource lits = (LazyInitTargetSource)targetSource; + if (beanNames.contains(lits.getTargetBeanName())) { + throw new FatalBeanException("A dependency cycle was detected when trying to resolve the AuthenticationManager. Please ensure you have configured authentication."); + } + } + } + } + } + ``` + + org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration.AuthenticationManagerDelegator#authenticate + + ```java + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (this.delegate != null) { + return this.delegate.authenticate(authentication); + } + synchronized (this.delegateMonitor) { + if (this.delegate == null) { + this.delegate = this.delegateBuilder.getObject(); + this.delegateBuilder = null; + } + } + return this.delegate.authenticate(authentication); + } + ``` + +3. 进入到DaoAuthenticationProvider的retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)方法进行用户的认证,这里的认证主要会调用默认的UserDetailsService对用户名和密码进行校验,如果是使用的类似于Mysql的数据源,其默认的实现是JdbcDaoImpl。 + + org.springframework.security.authentication.dao.DaoAuthenticationProvider#retrieveUser + + ```java + @Override + protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) + throws AuthenticationException { + prepareTimingAttackProtection(); + try { + UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); + if (loadedUser == null) { + throw new InternalAuthenticationServiceException( + "UserDetailsService returned null, which is an interface contract violation"); + } + return loadedUser; + } + catch (UsernameNotFoundException ex) { + mitigateAgainstTimingAttack(authentication); + throw ex; + } + catch (InternalAuthenticationServiceException ex) { + throw ex; + } + catch (Exception ex) { + throw new InternalAuthenticationServiceException(ex.getMessage(), ex); + } + } + + ``` + +4. 将上一步认证后的用户实例放入SecurityContextHolder中,至此我们可以很方便的从SecurityContextHolder中获取用户信息,方法如下: + + ```java + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + ``` + + diff --git a/docs/basis/PowerMockRunnerAndMockito.md b/docs/basis/PowerMockRunnerAndMockito.md index dbc2b0a..d5a7b12 100644 --- a/docs/basis/PowerMockRunnerAndMockito.md +++ b/docs/basis/PowerMockRunnerAndMockito.md @@ -61,9 +61,9 @@ PowerMock是一个Java模拟框架,可用于解决通常认为很难甚至无 ```java InterfaceToMock mock = Powermockito.mock(InterfaceToMock.class) - Powermockito.when(mock.method(Params…)).thenReturn(value) +Powermockito.when(mock.method(Params…)).thenReturn(value) - Powermockito.when(mock.method(Params..)).thenThrow(Exception) +Powermockito.when(mock.method(Params..)).thenThrow(Exception) ``` ##### 4.2 设置对象的private属性 @@ -137,9 +137,9 @@ Powermockito.mockStatic(FinalClassToMock.class); ```java 1) PowerMockito.spy(TargetClass.class); - 2) Powemockito.when(TargetClass.targetMethod()).doReturn() +2) Powemockito.when(TargetClass.targetMethod()).doReturn() - 3) 注意加入 +3) 注意加入 @RunWith(PowerMockRunner.class)