SpringBoot学习笔记-配置MySQL与实现注册登录模块(中)

  1. 1. 配置JWT验证
  2. 2. 实现验证登录API
  3. 3. 实现返回信息API
  4. 4. 实现注册账号API

本节内容为实现用适合前后端分离的 JWT 验证替代传统的 Session 验证方式,并实现登录、获取信息以及注册三个后端 API。

1. 配置JWT验证

传统模式是使用 Session 进行验证,但是由于前后端分离后可能会存在跨域问题,因此我们用 JWT(JSON Web Tokens)验证会更加方便。JWT 验证不仅可以很容易地实现跨域,也无需在服务器端存储数据,这样就算我们有很多个服务器,那么只需要获得一次令牌后就可以登录多个服务器。

我们所有的页面大致可以分为两大类,第一类是无需登录验证就能访问(公开页面),另一类就是登录后才能访问(授权页面)。

JWT 验证的原理是用户登录后服务器会给用户返回一个 jwt token,且会将一些 ID 之类的用户信息加到 jwt token 里,之后客户端每次向服务器端发送请求的时候都会带上这个令牌,服务器端在访问授权页面时会先验证这个令牌是否合法,如果合法就会根据令牌中的用户信息从数据库中查找出这个用户并将其数据提取至上下文,接着再访问授权页面。

生成 jwt token 时服务器端会先构建一个字符串,第一段存储用户 ID,第二段存储一个只有服务器能看到的密钥,然后可以通过一些哈希算法将字符串加密(加密算法是固定的),接着会将第一段用户 ID 加上加密后的信息合在一起(即 jwt token)返回给用户,之后服务器端想要验证的话就根据接收到的用户 ID 配合自己的密钥重复一遍这个固定的加密算法,看看加密后的结果是否和接收到的 jwt token 中的加密信息一致。

现在可能会有几个问题,比如 jwt token 是存在于客户端的,那么如果用户去篡改里面的数据会怎样,比如把用户 ID 换成具有权限的另一个用户的 ID?

这种情况是不会发生的,因为假如用户修改了 ID,但是由于加密算法的特性是不可逆的,即无法通过加密信息反推回原始字符串的信息,因此用户不知道服务器加密的密钥是什么,就没办法得到修改 ID 后再经过密钥加密的信息。

首先我们先去 Maven 仓库查找并添加以下依赖:

  • JetBrains Java Annotations
  • jjwt-api
  • jjwt-impl
  • jjwt-jackson

然后我们要实现 utils.JwtUtil 类(utils 包创建在 com.kob.backend 包下,放在哪个包下其实都是看个人习惯,不一定都要按这样写),为 JWT 工具类,用来创建、解析 jwt token

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
package com.kob.backend.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

@Component
public class JwtUtil {
public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // token的有效期设置为14天
public static final String JWT_KEY = "IVK157AXCZSChcwW23AUvayrXYhgcXAHKBMDziw17dW"; // 密钥,自己随便打,但是长度要够长,否则会报错

public static String getUUID() { // 生成一个随机的UUID并去掉其中的"-"
return UUID.randomUUID().toString().replaceAll("-", "");
}

public static SecretKey generalKey() {
byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); // 使用Base64解码预设的JWT_KEY
return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256"); // 使用这个解码后的密钥生成一个HmacSHA256算法的SecretKey
}

private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SecretKey secretKey = generalKey();

long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis; // 计算出token的过期时间
Date expDate = new Date(expMillis);

return Jwts.builder()
.id(uuid)
.subject(subject)
.issuer("sg")
.issuedAt(now)
.signWith(secretKey)
.expiration(expDate);
}

public static String createJWT(String subject) { // 创建一个JWT。
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact(); // 将其转换为一个紧凑的URL安全的JWT字符串
}

public static Claims parseJWT(String jwt) throws Exception { // 解析一个JWT
SecretKey secretKey = generalKey();
return Jwts.parser()
.verifyWith(secretKey) // 使用生成的密钥来验证JWT的签名
.build()
.parseSignedClaims(jwt)
.getPayload(); // 解析JWT并返回其payload(载荷)部分
}
}

这个类提供了创建和解析 JWT 的功能,可以用于实现基于 JWT 的身份验证和授权。但是需要注意的是,这个类中的 JWT_KEY 是硬编码的,这在实际的生产环境中是不推荐的,因为这样会导致所有的令牌都使用同一个密钥,一旦这个密钥泄露,所有的令牌都可能会被篡改或伪造。在实际的生产环境中,密钥应该被安全地存储在一个安全的地方,如环境变量或密钥管理服务中。

在 JWT 中,subject(或称为 sub)是一个预定义的声明,通常用来标识这个 JWT 所关联的主体。这个主体通常是一个用户的标识符,比如用户 ID 或用户名。这样,当你从一个 JWT 中解析出 subject 时,你就知道这个 JWT 是属于哪个用户的,从而可以进行相应的授权操作。例如,你可以根据 subject 来查找用户的角色,然后根据角色来决定用户是否有权限访问某个资源。这就是基于 JWT 的身份验证和授权的基本原理。

UUID(Universally Unique Identifier)是通用唯一识别码。它是通过特定的算法生成的一种128位的字符串,用于在全局范围内唯一地标识信息。UUID 的主要目的不是防止重复,而是减少重复的概率,使其小到可以忽略不计。

代码中 getUUID() 方法生成了一个随机的 UUID,这个 UUID 被用作 JWT 的 id,用于唯一地标识每一个 JWT。这样,即使有两个完全相同的 JWT(即具有相同的 subject 和过期时间),只要它们的 id 不同,就可以被认为是两个不同的 JWT。这对于跟踪和管理 JWT 非常有用。例如,如果一个 JWT 被盗,你可以通过它的 id 找到它,并将其加入到黑名单中,使其无法再被使用。

接下来还需要实现 config.filter.JwtAuthenticationTokenFilter 类(filter 包创建在之前的 config 包下),用来验证 jwt token,如果验证成功,则将 User 信息注入上下文中:

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
package com.kob.backend.config.filter;

import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private UserMapper userMapper;

@Override
protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("Authorization"); // 从请求头中获取名为"Authorization"的字段

if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) { // 这个字段应该包含一个以"Bearer "开头的JWT
filterChain.doFilter(request, response); // 将请求传递给下一个过滤器或处理器
return;
}

token = token.substring(7); // 跳过"Bearer "共7个字符

String userid;
try {
Claims claims = JwtUtil.parseJWT(token); // 解析JWT,获取JWT的载荷
userid = claims.getSubject(); // 从载荷中获取"subject",这个"subject"应该是用户ID
} catch (Exception e) {
throw new RuntimeException(e);
}

User user = userMapper.selectById(Integer.parseInt(userid)); // 查询数据库获取用户信息

if (user == null) {
throw new RuntimeException("用户未登录");
}

UserDetailsImpl loginUser = new UserDetailsImpl(user);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, null);

SecurityContextHolder.getContext().setAuthentication(authenticationToken); // 将其设置到Spring Security的上下文中

filterChain.doFilter(request, response); // 将请求传递给下一个过滤器或处理器
}
}

以上是一个基于 Spring Security 的 JWT 认证过滤器。它的主要功能是在每次 HTTP 请求到达时,从请求头中获取 JWT,解析出用户 ID,然后查询数据库获取用户信息,最后将用户信息保存到 Spring Security 的上下文中。这样,后续的处理模块就可以从 Spring Security 的上下文中获取到用户信息,进行相应的授权操作。

最后我们还需要配置一下之前实现过的 config.SecurityConfig 类,放行登录、注册等接口,因为用户未登录时需要能访问这些页面才能正常登录:

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
package com.kob.backend.config;

import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception { // AuthenticationManager用于处理身份验证
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity http) throws Exception { // 配置HttpSecurity
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/login/", "/user/account/register/").permitAll() // 需要公开的链接在这边写即可
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
}

这段代码的重点部分在于 configure 方法,这个方法用于配置 HttpSecurity,它是 Spring Security 的核心配置点。在这个方法中,首先禁用了 CSRF(跨站请求伪造)保护,然后设置了会话管理策略为无状态,这意味着 Spring Security 不会创建或使用 HTTP 会话。接着,它配置了 URL 的访问权限,/user/account/login//user/account/register/ 这两个 URL 对所有用户开放,OPTIONS 请求对所有用户开放,此外其他的所有请求都需要身份验证。最后,它在 UsernamePasswordAuthenticationFilter 之前添加了 JwtAuthenticationTokenFilter,这意味着在每次 HTTP 请求到达时,JwtAuthenticationTokenFilter 都会先于 UsernamePasswordAuthenticationFilter 执行。

较新版本的 Spring Security 5.7 会看到提示说 WebSecurityConfigurerAdapter 已经废除,不过目前对本项目没什么影响,如果一定要改可以参考以下代码:

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
package com.kob.backend.config;

import com.kob.backend.config.filter.JwtAuthenticationTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

@Bean
public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception {
return authenticationConfiguration.getAuthenticationManager();
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/user/account/login/", "/user/account/register/").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.anyRequest().authenticated();

http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}
}

2. 实现验证登录API

配置完成后接下来我们就可以创建后端的 API 了。在这之前我们给数据库的 user 表添加一列 photo 用来存储用户的头像链接(数据库中存储图像都是存的链接),类型为 varchar(1000),然后还得去 pojo.User 类中添加一个字段:

1
private String photo;

现在我们编写第一个 API:/user/account/login/,功能是验证用户名和密码,验证成功后返回 jwt token

SpringBoot 中写一个 API 一共要实现三个部分:controller 用来调用 serviceservice 里面实现一个接口,还需要在 service.impl 中写一个具体的接口的实现。

service 包下创建 user.account 包,表示用户账号相关的 API,然后创建 LoginService 接口(注意不是创建类):

1
2
3
4
5
6
7
package com.kob.backend.service.user.account;

import java.util.Map;

public interface LoginService {
Map<String, String> login(String username, String password);
}

接着在 service.impl 包下创建 user.account 包,然后创建 LoginServiceImpl 类用来实现我们之前定义的接口:

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
package com.kob.backend.service.impl.user.account;

import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.service.user.account.LoginService;
import com.kob.backend.utils.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service // 注入到Spring中,未来可以用@Autowired注解将该类注入到某个其他类中
public class LoginServiceImpl implements LoginService {
@Autowired
private AuthenticationManager authenticationManager;

@Override
public Map<String, String> login(String username, String password) {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password); // 需要先封装一下,因为数据库中存的不是明文

Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 验证是否能登录,如果失败会自动处理

UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal();
User user = loginUser.getUser(); // 将用户取出来
String jwt_token = JwtUtil.createJWT(user.getId().toString()); // 将用户的ID转换成jwt_token

Map<String, String> res = new HashMap<>();
res.put("result", "success");
res.put("jwt_token", jwt_token);

return res;
}
}

最后就可以实现 controller 模块了,我们可以先把 controller.user 包下的 UserController 类删了,这是之前学习数据库操作与调试用的,然后创建一个 account 包,在包中创建 LoginController 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.kob.backend.controller.user.account;

import com.kob.backend.service.user.account.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class LoginController {
@Autowired // 将接口注入进来,这就是Spring的IoC依赖注入特性
private LoginService loginService;

@PostMapping("/user/account/login/") // 登录采用POST请求,不是明文传输,较为安全
public Map<String, String> login(@RequestParam Map<String, String> data) { // 将POST参数放在一个Map中
String username = data.get("username");
String password = data.get("password");
return loginService.login(username, password);
}
}

实现好后我们可以自己调试一下,如果直接从浏览器访问 URL 的话是 GET 请求,尝试访问 http://localhost:3000/user/account/login/ 会看到报错状态码为405,表示方法不被允许,可以用 Postman 软件调试也可以自己打开前端调试,我们采用第二种方法。

直接在前端项目的 App.vue 文件中编写临时调试代码,使用 Ajax 发出 POST 请求:

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
<template>
<NavBar />
<router-view />
</template>

<script>
import NavBar from "@/components/NavBar.vue";
import $ from "jquery";

export default {
components: {
NavBar,
},
setup() {
$.ajax({
url: "http://localhost:3000/user/account/login/",
type: "POST",
data: {
username: "AsanoSaki",
password: "123456",
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
},
});
},
};
</script>

<style>
body {
background-image: url("@/assets/images/background.png");
background-size: cover;
}
</style>

然后我们在前端页面中打开控制台,一刷新页面即可看到输出结果。我们可以双击并复制下来控制台中的 jwt_token 内容,然后去 JWT IO 中解析一下,能够得到以下结果,其中的 sub 存储的即为用户 ID:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Header部分,表示加密算法
{
"alg": "HS256"
}

// Payload部分,表示数据
{
"jti": "98d002b0b919404ea0571d815cecf5ba",
"sub": "1",
"iss": "sg",
"iat": 1700100065,
"exp": 1701309665
}

3. 实现返回信息API

现在我们来编写 API:/user/account/info/,功能是根据客户端传来的 jwt_token 获取用户信息。

首先在 service.user.account 包下创建 InfoService 接口:

1
2
3
4
5
6
7
package com.kob.backend.service.user.account;

import java.util.Map;

public interface InfoService {
Map<String, String> getInfo();
}

然后在 service.impl.user.account 包下创建 InfoServiceImpl 类,来实现 InfoService 接口:

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
package com.kob.backend.service.impl.user.account;

import com.kob.backend.pojo.User;
import com.kob.backend.service.impl.utils.UserDetailsImpl;
import com.kob.backend.service.user.account.InfoService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class InfoServiceImpl implements InfoService {
@Override
public Map<String, String> getInfo() {
// 将用户信息从上下文中提取出来
UsernamePasswordAuthenticationToken authenticationToken =
(UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();

UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal();
User user = loginUser.getUser();

Map<String, String> res = new HashMap<>();
res.put("result", "success");
res.put("id", user.getId().toString());
res.put("username", user.getUsername());
res.put("photo", user.getPhoto());
return res;
}
}

最后在 controller.user.account 包下创建 InfoController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.kob.backend.controller.user.account;

import com.kob.backend.service.user.account.InfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class InfoController {
@Autowired
private InfoService infoService;

@GetMapping("/user/account/info/") // 此处是获取信息的请求,使用GET
public Map<String, String> getInfo() {
return infoService.getInfo();
}
}

重启一下后端,然后我们直接访问 http://localhost:3000/user/account/info/ 会看到报错代码为403,表示没有权限访问,因为我们还没登录。

还是和之前一样,我们在前端中测试,将之前登录接收到的 jwt_token 用于之后访问授权链接,此次请求不用传数据,但是需要传一个 headers 表示表头,其中有一个 Authorization 属性,由 Bearer (注意有个空格)加上 jwt_token 组成,我们先直接把前面浏览器控制台输出的 jwt_token 复制过来:

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
...

<script>
import NavBar from "@/components/NavBar.vue";
import $ from "jquery";

export default {
components: {
NavBar,
},
setup() {
...

$.ajax({
url: "http://localhost:3000/user/account/info/",
type: "GET",
headers: {
// 不是固定的,是官方推荐的写法,Authorization是在我们的后端JwtAuthenticationTokenFilter类中设置的
Authorization: "Bearer " + "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI0OGNjYjZiY2E0ZTk0YjliODI3ZmM3M2Y0OTg5YjNjOSIsInN1YiI6IjEiLCJpc3MiOiJzZyIsImlhdCI6MTcwMDEwODIyOCwiZXhwIjoxNzAxMzE3ODI4fQ.B_eKTBIxfoiXy4b0tp1sPqy7ZH5GqRFfvYOCk2sx6IY",
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
},
});
},
};
</script>

...

在浏览器控制台可以看到以下输出:

1
2
3
4
5
6
{
"result": "success",
"photo": "https://cdn.acwing.com/media/user/profile/photo/82581_lg_e9bdbcb8aa.jpg",
"id": "1",
"username": "AsanoSaki"
}

4. 实现注册账号API

注册功能就有一些业务逻辑需要判断,代码量会稍微多一些。

首先在 service.user.account 包下创建 RegisterService 接口:

1
2
3
4
5
6
7
package com.kob.backend.service.user.account;

import java.util.Map;

public interface RegisterService {
Map<String, String> register(String username, String password, String confirmedPassword);
}

然后在 service.impl.user.account 包下创建 RegisterServiceImpl 类,来实现 InfoService 接口:

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
package com.kob.backend.service.impl.user.account;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.kob.backend.mapper.UserMapper;
import com.kob.backend.pojo.User;
import com.kob.backend.service.user.account.RegisterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class RegisterServiceImpl implements RegisterService {
@Autowired
private UserMapper userMapper;

@Autowired
private PasswordEncoder passwordEncoder;

@Override
public Map<String, String> register(String username, String password, String confirmedPassword) {
Map<String, String> res = new HashMap<>();

if (username == null) { // 判断是否存在用户名参数
res.put("result", "The username can't be empty!");
return res;
}
if (password == null || confirmedPassword == null) { // 判断是否存在密码参数
res.put("result", "The password can't be empty!");
return res;
}

username = username.trim(); // 删掉用户名首尾的空白字符
if (username.isEmpty()) { // 判断删去空格后用户名是否为空
res.put("result", "The username can't be empty!");
return res;
}
if (password.isEmpty() || confirmedPassword.isEmpty()) { // 判断密码是否为空
res.put("result", "The password can't be empty!");
return res;
}

if (username.length() > 100 || password.length() > 100) { // 判断用户名或密码长度是否超过数据库字段的范围
res.put("result", "The username or password can't be longer than 100!");
return res;
}
if (!password.equals(confirmedPassword)) { // 判断两次输入的密码是否一致
res.put("result", "The inputs of two passwords are different!");
return res;
}

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("username", username); // 在数据库中查看是否存在用户名相同的用户
List<User> users = userMapper.selectList(queryWrapper);
if (!users.isEmpty()) {
res.put("result", "The username already exists!");
return res;
}

// 执行数据库插入操作
String encodedPassword = passwordEncoder.encode(password);
String photo = "https://cdn.acwing.com/media/user/profile/photo/82581_lg_e9bdbcb8aa.jpg"; // 默认头像
User user = new User(null, username, encodedPassword, photo);
userMapper.insert(user);

res.put("result", "success");
return res;
}
}

最后在 controller.user.account 包下创建 RegisterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.kob.backend.controller.user.account;

import com.kob.backend.service.user.account.RegisterService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
public class RegisterController {
@Autowired
private RegisterService registerService;

@PostMapping("/user/account/register/")
public Map<String, String> register(@RequestParam Map<String, String> data) {
String username = data.get("username");
String password = data.get("password");
String confirmedPassword = data.get("confirmedPassword");
return registerService.register(username, password, confirmedPassword);
}
}

同样还是在前端编写调试代码,可以自行尝试数据为空,或者两次密码不一致等不合法操作,然后在浏览器控制台查看结果:

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
...

<script>
import NavBar from "@/components/NavBar.vue";
import $ from "jquery";

export default {
components: {
NavBar,
},
setup() {
...

$.ajax({
url: "http://localhost:3000/user/account/register/",
type: "POST",
data: {
username: "user7",
password: "123456",
confirmedPassword: "123456",
},
success(resp) {
console.log(resp);
},
error(resp) {
console.log(resp);
},
});
},
};
</script>

...

至此我们的后端部分实现完成了,后面就可以开始实现前端部分了。