- Springboot-security(鉴权模块)
1
2
3
4
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
|
默认情况下,springboot-security
是拦截所有请求的
因此我们需要配置让它放行我们需要的请求路径
1
2
3
4
5
6
7
8
9
|
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/", "/auth/**").permitAll(); // 对根路径和所有/auth路径都放行
}
}
|
- 在
@Configuration
中声明两个Bean
,这两个Bean
是鉴权的关键,一个用于鉴定权限,一个用于加密密码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.authorizeRequests().antMatchers("/", "/auth/**").permitAll();
}
/* 鉴权管理器 */
@Bean
public AuthenticationManager customAuthenticationManager() throws Exception {
return authenticationManager();
}
/* 加密器 */
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public boolean registerUser(String username, String password) {
return userMapper.registerUser(username, bCryptPasswordEncoder.encode(password)); // 加密存储密码
}
public User selectUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
/* 覆盖loadUserByUsername方法,查询对应用户是否存在,并返回用户的详细信息 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = selectUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username + " 不存在!");
}
return org.springframework.security.core.userdetails.User
.withUsername(username)
.password(user.getPassword())
.authorities(Collections.emptyList())
.build();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
@RestController
public class UserController {
@Autowired
private UserService userService;
@Autowired
private AuthenticationManager authenticationManager;
@PostMapping("/auth/login")
public Result login(@RequestBody Map<String, String> body) {
String username = body.get("username");
String password = body.get("password");
UserDetails userDetails;
try {
userDetails = userService.loadUserByUsername(username); /* 获取用户详细信息 */
} catch (UsernameNotFoundException e) {
return Result.fail("用户不存在");
}
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(
userDetails, password, userDetails.getAuthorities()); /* 根据用户详细信息创建token(令牌)对象,它保存了用户信息 */
try {
authenticationManager.authenticate(token); /* 验证令牌 */
SecurityContextHolder.getContext().setAuthentication(token); // 将信息保存到Session中
} catch (BadCredentialsException e) {
return Result.fail("密码错误");
}
return Result.ok("登录成功", userService.selectUserByUsername(username), true);
}
}
|
1
2
3
4
5
6
7
8
|
@GetMapping("/auth")
public Result auth() {
/* 我把这个 Authentication 就看作 Session 对象 */
Authentication session = SecurityContextHolder.getContext().getAuthentication();
User user = userService.selectUserByUsername(session == null ? null : session.getName());
if (user == null) return Result.fail("用户尚未登录!");
return Result.ok("用户已登录", user, true);
}
|
1
2
3
4
5
6
7
|
@GetMapping("/auth/logout")
/* 如果已经执行过clearContext,那么这里会是Null */
Authentication session = SecurityContextHolder.getContext().getAuthentication();
if (session == null) return Result.fail("用户尚未登录"); /* 因此这里需要避免一下空指针异常 */
SecurityContextHolder.clearContext();
return Result.ok("注销成功");
}
|
- 自动化
- 节省你的时间
- 避免犯错(是人都会犯错)
- 测试可以反映一个人的水平
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<!-- 依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.1</version>
<scope>test</scope>
</dependency>
<!-- 用于单元测试 -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
<!-- 用于集成测试 -->
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
</plugin>
|
程序创建好后的第一次测试,主要用于测试一下程序能否正常编译
1
2
3
4
5
|
public class SmokeTest {
@Test
public void test(){
}
}
|
我们的对象都会有一些依赖,但我们编写测试的时候不方便或无法获得那些依赖,因此就需要一些假的数据
Mock
就是假的依赖的对象,例如我们的UserService
依赖UserDao
,那么我们可以创建一个MockDao
来模拟UserDao
所谓Mock
就是创建一个假的服务,这就是Mock
的概念
1
2
3
4
5
6
7
|
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.1.0</version>
<scope>test</scope>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
public boolean registerUser(String username, String password) {
return userMapper.registerUser(username, bCryptPasswordEncoder.encode(password));
}
public User selectUserByUsername(String username) {
return userMapper.selectUserByUsername(username);
}
/* 覆盖loadUserByUsername方法,返回对应用户的详细信息 */
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = selectUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username + " 不存在!");
}
return org.springframework.security.core.userdetails.User
.withUsername(username)
.password(user.getPassword())
.authorities(Collections.emptyList())
.build();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
@ExtendWith(MockitoExtension.class) /* 为这个测试启用Mockito扩展 */
class UserServiceTest {
@Mock
BCryptPasswordEncoder mockBCryptPasswordEncoder; /* 假的BCryptPasswordEncoder */
@Mock
UserMapper mockUserMapper; /* 假的UserMapper */
@InjectMocks
UserService userService; /* 真实的UserService,并为他注入上面的Mock依赖 */
@Test
public void testRegisterUser() {
/* 当mockBCryptPasswordEncoder.encode("plaintext")的时候,让它返回"ciphertext",模拟加密过程 */
Mockito.when(mockBCryptPasswordEncoder.encode("plaintext")).thenReturn("ciphertext");
userService.registerUser("test", "plaintext"); /* 测试这个方法 */
/* 我希望调用registerUser的时候,传递给mockUserMapper的值是"test"和"ciphertext" */
Mockito.verify(mockUserMapper).registerUser("test", "ciphertext"); /* 验证一下mockUserMapper以什么方式被调用了 */
}
@Test
public void testSelectUserByUsername() {
userService.selectUserByUsername("test");
Mockito.verify(mockUserMapper).selectUserByUsername("test"); /* 验证一下mockUserMapper是否收到的值是"test" */
}
@Test
public void testLoadUserByUsernameThrowsUsernameNotFoundException() {
/* 当调用mockUserMapper.selectUserByUsername("test"),模拟让它返回null */
// Mockito.when(mockUserMapper.selectUserByUsername("test")).thenReturn(null);
/* 上面的实际是多余的,因为你不对它配置的时候,它默认就是返回null */
/* 断言一下,当执行userService.loadUserByUsername("test")的时候会抛出UsernameNotFoundException异常 */
Assertions.assertThrows(UsernameNotFoundException.class,
() -> userService.loadUserByUsername("test"));
}
@Test
public void testLoadUserByUsernameReturnUserDetail() {
/* 当调用mockUserMapper.selectUserByUsername("test")模拟让它返回一个User对象 */
Mockito.when(mockUserMapper.selectUserByUsername("test"))
.thenReturn(new User("test", "ciphertext"));
UserDetails userDetails = userService.loadUserByUsername("test"); /* 调用实际的逻辑 */
/* 验证返回的结果是否符合预期 */
Assertions.assertEquals("test", userDetails.getUsername());
Assertions.assertEquals("ciphertext", userDetails.getPassword());
}
}
|
- 为Springboot的Controller编写测试需要引入Springboot的依赖
1
2
3
4
5
|
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
@ExtendWith(MockitoExtension.class)
@ExtendWith(SpringExtension.class)
class UserControllerTest {
private MockMvc mockMvc; /* 测试Controller必须的,模拟各种请求 */
@Mock
private UserService userService;
@Mock
private AuthenticationManager authenticationManager;
private BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
/* 每次调用测试方法之前,都会执行 */
@BeforeEach
void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(new UserController(userService, authenticationManager)).build();
}
@Test
void testAuthByDefault() throws Exception {
/* 执行一个Get请求,请求auth接口,并预期的返回的状态码是 200,并且响应中包含"用户尚未登录" */
mockMvc.perform(MockMvcRequestBuilders.get("/auth"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(result -> Assertions.assertTrue(result.getResponse().getContentAsString(StandardCharsets.UTF_8).contains("用户尚未登录")));
}
@Test
void testLogin() throws Exception {
/* 发送一个Get请求,请求auth接口,并预期的返回的状态码是 200,并且响应中包含"用户尚未登录" */
mockMvc.perform(MockMvcRequestBuilders.get("/auth"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(result -> Assertions.assertTrue(result.getResponse().getContentAsString(StandardCharsets.UTF_8).contains("用户尚未登录")));
/* Mock一些依赖 */
Mockito.when(userService.loadUserByUsername("admin123"))
.thenReturn(new User("admin123", bCryptPasswordEncoder.encode("admin123"), Collections.emptyList()));
Mockito.when(userService.selectUserByUsername("admin123"))
.thenReturn(new com.github.wjinlei.springbootblog.entity.User("admin123", "admin123"));
/* 准备post参数 */
Map<String, String> params = new HashMap<>();
params.put("username", "admin123");
params.put("password", "admin123");
String jsonValue = new ObjectMapper().writeValueAsString(params); /* 将Map序列化成json字符串 */
/* 发送一个Get请求,请求/auth/login接口,并预期返回的状态码是 200,并且拿到它的返回值 */
MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonValue))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(result -> Assertions.assertTrue(result.getResponse().getContentAsString(StandardCharsets.UTF_8).contains("登录成功")))
.andReturn();
HttpSession session = mvcResult.getRequest().getSession(); /* 从请求中获取session */
/* 再次发起GET /auth 请求,并断言结果中包含"用户已登录" */
mockMvc.perform(MockMvcRequestBuilders.get("/auth").session((MockHttpSession) session))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(result -> Assertions.assertTrue(result.getResponse().getContentAsString(StandardCharsets.UTF_8).contains("用户已登录")));
}
}
|
所谓集成测试,就是把整个项目打包,对整个项目暴露出来的外部接口进行测试,而不是对某个类,某个方法,每个单元进行测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<!-- 用于单元测试 -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<!-- 从单元测试中排除 IntegrationTest名称的测试 -->
<excludes>
<exclude>**/*IntegrationTest</exclude>
</excludes>
</configuration>
</plugin>
<!-- 用于集成测试 -->
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<!-- 从集成测试中包含 IntegrationTest名称的测试 -->
<includes>
<include>**/*IntegrationTest</include>
</includes>
</configuration>
</plugin>
|
1
|
sudo docker run --name jenkins -p 8081:8080 -p 50000:50000 -v `pwd`/jenkins_data:/var/jenkins_home jenkins/jenkins
|
- Jenkins 创建一个工程构建项目

集成测试需要启动一个真正的环境,然后在进行整体测试
例如,如果项目依赖MySQL
,也就意味着,你在进行集成测试之前,应该先启动一个MySQL
要在Maven中启动MySQL,我们需要一个插件exec-maven-plugin
,它可以帮助我们执行一个外部命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
<plugins>
<!-- 用于单元测试 -->
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<!-- 从单元测试中排除 IntegrationTest名称的测试 -->
<excludes>
<exclude>**/*IntegrationTest</exclude>
</excludes>
</configuration>
</plugin>
<!-- 用于集成测试 -->
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<includes>
<include>**/*IntegrationTest</include>
</includes>
</configuration>
</plugin>
<!-- 用于执行一个外部命令 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<!-- 可以有多个execution, 每一个execution,需要一个id,用以区分 -->
<id>start-test-database</id>
<!-- 绑定到哪个阶段上,Maven在运行到这个阶段的时候,会自动帮我们运行整个goal -->
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<!-- 执行docker命令启动一个测试用MySQL-->
<executable>sudo</executable>
<arguments>
<argument>docker</argument>
<argument>run</argument>
<argument>--name</argument>
<argument>test-mysql</argument>
<argument>-p</argument>
<argument>3307:3306</argument>
<argument>-e</argument>
<argument>MYSQL_ROOT_PASSWORD=root</argument>
<argument>-e</argument>
<argument>MYSQL_DATABASE=blog</argument>
<argument>-d</argument>
<argument>mysql:8.0.27</argument>
</arguments>
</configuration>
</execution>
<!-- 测试数据库启动之后,先等待20秒左右,等待数据库初始化 -->
<execution>
<id>wait-test-database</id>
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>ping</executable>
<arguments>
<argument>-c</argument>
<argument>20</argument>
<argument>baidu.com</argument>
</arguments>
</configuration>
</execution>
<!-- 集成测试之后销毁测试数据库 -->
<execution>
<id>rm-test-database</id>
<phase>post-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>sudo</executable>
<arguments>
<argument>docker</argument>
<argument>rm</argument>
<argument>-f</argument>
<argument>test-mysql</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
<!-- flyway插件也绑定到pre-integration-test阶段,并且要在exec插件之后,因为要等数据启动起来后再migrate-->
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>8.0.4</version>
<configuration>
<url>jdbc:mysql://localhost:3306/blog?characterEncoding=utf-8</url>
<user>root</user>
<password>root</password>
</configuration>
<executions>
<execution>
<id>initialize</id>
<phase>pre-integration-test</phase>
<goals>
<goal>migrate</goal>
</goals>
<!-- 这个execution额外的配置,因为是对测试数据库执行,而不是真实的数据库,因此端口指向测试数据库的端口3307 -->
<configuration>
<url>jdbc:mysql://localhost:3307/blog?characterEncoding=utf-8</url>
<user>root</user>
<password>root</password>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@ExtendWith(SpringExtension.class)
/* 指定要测试SpringbootBlogApplication, 并指定一个随机端口,因为你不能占用正常的端口 */
@SpringBootTest(classes = SpringbootBlogApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
/* 指定一个测试的配置文件,因为真实的项目配置文件连接的数据库是3306,而我们测试数据库是3307 */
/* 我们可以在test包中新建一个resources/test.properties配置文件并修改端口为3307 */
@TestPropertySource(locations = "classpath:test.properties")
public class AppIntegrationTest {
@Inject
Environment environment;
/**
* 测试首页能够访问
*/
@Test
void testAuthByDefault() throws IOException, InterruptedException {
/* 获得随机启动的端口号 */
String port = environment.getProperty("local.server.port");
/* 使用Java 11 自带的httpclient库发送请求 */
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:" + port + "/auth"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals(200, response.statusCode());
Assertions.assertTrue(response.body().contains("用户尚未登录"));
}
}
|