前言 :注意这不是一篇类似什么手把手带你完成一个前后端分离项目的博客,这个项目基本上是我从零一个人开始开发的,所以仅以此来记录一下我的开发流程。所以说是类似一个开发日志的文章,并不是一个教学文章,但是如果里面的一些操作能给你带来一点点灵感那就再好不过了。
项目介绍 :这是一个基于 SpringBoot + Vue 十分普通的一个前后端分离项目,所以登录注册等核心功能的安全验证使用了 JWT,即时聊天的核心功能实现使用了 WebSocket 的第三方组件: Socket.io 数据层使用了 MyBatis 进行数据操作。Redis 进行 token 缓存等操作。
后端技术栈 · SpringBoot · Mybatis · lombok · redis · JWT · MySQL
一. 准备工作和项目搭建 1.1 新建 SpringBoot 项目 这个就不用说了吧,这个如果你都不会,我估计也没必要看了
1.2 确定项目结构
1.3 依赖导入及配置文件编写 pom.xml
<dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.2.0</version > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-generator</artifactId > <version > 3.2.0</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-jdbc</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-devtools</artifactId > <scope > runtime</scope > <optional > true</optional > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <scope > runtime</scope > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <optional > true</optional > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency > <dependency > <groupId > commons-codec</groupId > <artifactId > commons-codec</artifactId > <version > 1.10</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.13.1</version > <scope > test</scope > </dependency > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency > <dependency > <groupId > log4j</groupId > <artifactId > log4j</artifactId > <version > 1.2.16</version > <scope > compile</scope > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > fastjson</artifactId > <version > 1.2.75</version > </dependency >
application.yml (SpringBoot 配置文件编写)
server: port: 后端运行端口号 spring: # MySql 配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your database name?user=root&password=root&useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT username: your username password: your password # redis 配置 redis: host: localhost port: your port name
1.4 数据库和表的设计 这里随便建个表就好(有用户名密码还有邮箱这几个字段就 ok 了),因为只是测试登录和注册功能,完整项目的表后续更新即时聊天核心功能实现的时候会展示的。
二. 项目骨架搭建 2.1 开发项目逻辑结构 完整的项目结构
图接上图
三. 基础代码编写 3.1 返回类和 API 状态码封装 以下是定义通用返回对象,这个网上有很多封装很好的可以拿来用的,这个一般都是统一规范定义的,建议自己有能力整合一套,因为这玩意是配套的基本上,要用就要用一套。(我这个是定义在 api 包里的)
CommonResult 通用返回类
public class CommonResult <T> { private long code; private String message; private T token; protected CommonResult () { } protected CommonResult (long code, String message, T token) { this .code = code; this .message = message; this .token = token; } public static <T> CommonResult<T> success (T data) { return new CommonResult <T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data); } public static <T> CommonResult<T> success (T token, String message) { return new CommonResult <T>(ResultCode.SUCCESS.getCode(), message, token); } public static <T> CommonResult<T> failed (IErrorCode errorCode) { return new CommonResult <T>(errorCode.getCode(), errorCode.getMessage(), null ); } public static <T> CommonResult<T> failed (String message) { return new CommonResult <T>(ResultCode.FAILED.getCode(), message, null ); } public static <T> CommonResult<T> failed () { return failed(ResultCode.FAILED); } public static <T> CommonResult<T> usernameorpassworderroe (IErrorCode errorCode) { return new CommonResult <T>(errorCode.getCode(), errorCode.getMessage(), null ); } public static <T> CommonResult<T> usernameorpassworderroe (String message) { return new CommonResult <T>(ResultCode.USERNAME_OR_PASSWORD_ERROR.getCode(), message, null ); } public static <T> CommonResult<T> usernameorpassworderroe () { return usernameorpassworderroe(ResultCode.USERNAME_OR_PASSWORD_ERROR); } public static <T> CommonResult<T> accountpwdnotexist (IErrorCode errorCode) { return new CommonResult <T>(errorCode.getCode(), errorCode.getMessage(), null ); } public static <T> CommonResult<T> accountpwdnotexist (String message) { return new CommonResult <T>(ResultCode.ACCOUNT_PWD_NOT_EXISTS.getCode(), message, null ); } public static <T> CommonResult<T> accountpwdnotexist () { return accountpwdnotexist(ResultCode.ACCOUNT_PWD_NOT_EXISTS); } public static <T> CommonResult<T> validateFailed () { return failed(ResultCode.VALIDATE_FAILED); } public static <T> CommonResult<T> validateFailed (String message) { return new CommonResult <T>(ResultCode.VALIDATE_FAILED.getCode(), message, null ); } public static <T> CommonResult<T> unauthorized (T data) { return new CommonResult <T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data); } public static <T> CommonResult<T> forbidden (T data) { return new CommonResult <T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data); } public static <T> CommonResult<T> tokenerror (IErrorCode errorCode) { return new CommonResult <T>(errorCode.getCode(), errorCode.getMessage(), null ); } public static <T> CommonResult<T> tokenerror (String message) { return new CommonResult <T>(ResultCode.TOKEN_ERROR.getCode(), message, null ); } public static <T> CommonResult<T> tokenerror () { return tokenerror(ResultCode.TOKEN_ERROR); } public long getCode () { return code; } public void setCode (long code) { this .code = code; } public String getMessage () { return message; } public void setMessage (String message) { this .message = message; } public T getToken () { return token; } public void setData (T token) { this .token = token; } }
IErrorCode(接口)
public interface IErrorCode { long getCode () ; String getMessage () ; }
ResultCode(类)
使用枚举的好处是把状态码和状态信息结合到了一起,当然不使用枚举类用静态成员变量也行,状态码信息需要单独封装。
public enum ResultCode implements IErrorCode { SUCCESS(200 , "操作成功" ), FAILED(500 , "操作失败" ), VALIDATE_FAILED(1001 , "参数检验失败" ), UNAUTHORIZED(401 , "暂未登录或token已经过期" ), TOKEN_ERROR(1003 , "Token 错误" ), FORBIDDEN(403 , "没有相关权限" ), USERNAME_OR_PASSWORD_ERROR(405 , "用户名或密码错误" ), DATA_Not_Exist_ERROR(603 , "数据不存在" ), DATA_ADD_ERROR(604 , "数据添加异常" ), ACCOUNT_PWD_NOT_EXISTS(1002 , "用户名或者密码不存在" ); private long code; private String message; private ResultCode (long code, String message) { this .code = code; this .message = message; } @Override public long getCode () { return code; } @Override public String getMessage () { return message; } }
3.2 跨域请求的解决方式 产生跨域请求的原因还有同源策略建议自行百度
CorsConfig(配置类)
@Configuration public class CorsConfig { private CorsConfiguration buildConfig () { System.out.println("跨域工具类测试" ); CorsConfiguration corsConfiguration = new CorsConfiguration (); corsConfiguration.addAllowedOrigin("*" ); corsConfiguration.addAllowedHeader("*" ); corsConfiguration.addAllowedMethod("*" ); return corsConfiguration; } @Bean public CorsFilter corsFilter () { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource (); source.registerCorsConfiguration("/**" , buildConfig()); return new CorsFilter (source); } }
3.3 JWT (重点) 这里简单说一下 JWT 的验证流程:
首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 token head.payload.singurater
后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
JwtUtils 工具类
这个基本上也是统一规范定义的,可以根据项目的大小来自己定义所需的方法,但是基本的几个流程是不会变的。
public class JwtUtils { public static final String jwtToken = "你自己定义的密钥" ; public static String createToken (int userId) { Map<String, Object> claims = new Hashtable <>(); claims.put("userId" , userId); JwtBuilder jwtBuilder = Jwts.builder() .signWith(SignatureAlgorithm.HS256, jwtToken) .setClaims(claims) .setIssuedAt(new Date ()) .setExpiration(new Date (System.currentTimeMillis() + 24 * 60 * 60 * 1000 )); String token = jwtBuilder.compact(); return token; } public static Map<String, Object> checkToken (String token) { try { Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token); return (Map<String, Object>) parse.getBody(); } catch (Exception e) { e.printStackTrace(); } return null ; } @Test public void test () { String token = JwtUtils.createToken(100 ); System.out.println(token); Map<String, Object> map = checkToken(token); System.out.println(map.get("userId" )); } }
四. 核心代码编写 4.1 entity 实体类编写 这一层的编写一般是按照数据库中定义的字段去编写相应的实体类,我前面说过,由于这个是登录注册业务的测试所以数据库中只有几个字段,所以想对应的实体类也会比较简单
User (类)
@Data public class User { private int id; private String username; private String password; private String email; }
4.2 mapper 层和 service 层 4.2.1 mapper 层 userMapper.xml 这里是所有关于用户的操作
<mapper namespace ="com.alexjoker.mapper.UserMapper" > <select id ="findUser" resultType ="com.alexjoker.entity.User" > select * from user where username = #{username} and password = #{password} </select > <select id ="findUserById" resultType ="com.alexjoker.entity.User" > select * from user where id = #{id} </select > <select id ="findUserByName" resultType ="com.alexjoker.entity.User" > select * from user where username = #{username} </select > <select id ="findUserByEmail" resultType ="com.alexjoker.entity.User" > select * from user where email = #{email} </select > </mapper >
RegisterMapper.xml 这里是用户注册的操作
<mapper namespace ="com.alexjoker.mapper.RegisterMapper" > <insert id ="Register" parameterType ="com.alexjoker.entity.User" > insert into user(username, password, email) values (#{username}, #{password}, #{email}) </insert > </mapper >
4.2.2 mapper 层的接口 UserMapper
@Mapper public interface UserMapper { User findUser (String username, String password) ; User findUserById (int id) ; User findUserByName (String username) ; User findUserByEmail (String email) ; }
RegisterMapper
这里需要注意一下:mapper文件中的update,delete,insert语句是不需要设置返回类型的,它们都是默认返回一个int ,所以把返回值类型改成 int。
@Mapper public interface RegisterMapper { int Register (String username, String password, String email) ; }
4.2.3 service 层 整体项目结构(注意在设计时尽量降低其业务耦合度)
LoginService 接口
public interface LoginService { CommonResult login (User user) ; CommonResult logout (String token) ; }
LoginServiceImpl 实现类
@Service("LoginService") public class LoginServiceImpl implements LoginService { @Autowired private UserService userService; @Autowired private RedisTemplate<String, String> redisTemplate; @Override public CommonResult login (User user) { String username = user.getUsername(); String password = user.getPassword(); if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { return CommonResult.validateFailed("参数不合法" ); } User sysUser = userService.findUser(username, password); if (sysUser == null ) { return CommonResult.accountpwdnotexist("用户名或密码不存在" ); } String token = JwtUtils.createToken(sysUser.getId()); redisTemplate.opsForValue().set("TOKEN_" + token, JSON.toJSONString(sysUser), 1 , TimeUnit.DAYS); return CommonResult.success(token); } @Override public CommonResult logout (String token) { redisTemplate.delete("TOKEN_" + token); return CommonResult.success("退出登录成功" ); } }
RegisterService 接口
public interface RegisterService { CommonResult register (User user) ; }
RegisterService 实现类
@Service("RegisterService") public class RegisterServiceImpl implements RegisterService { @Autowired private RegisterMapper registerMapper; @Autowired private UserService userService; @Override public CommonResult register (User user) { String username = user.getUsername(); String email = user.getEmail(); String password = user.getPassword(); if (StringUtils.isBlank(username) || StringUtils.isBlank(email) || StringUtils.isBlank(password)) { return CommonResult.validateFailed("参数不合法" ); } User sysUsername = userService.findUserByName(username); if (sysUsername != null ) { return CommonResult.validateFailed("用户名已存在" ); } User sysEmail = userService.findUserByEmail(email); if (sysEmail != null ) { return CommonResult.validateFailed("该邮箱已存在" ); } registerMapper.Register(username, password, email); return CommonResult.success("注册成功" ); } }
UserService 接口
public interface UserService { User findUserById (int id) ; User findUser (String username, String password) ; User findUserByName (String username) ; User findUserByEmail (String email) ; }
UserServiceImpl 实现类
@Service("UserService") public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Autowired TokenService tokenService; @Override public User findUserById (int id) { return userMapper.findUserById(id); } @Override public User findUserByName (String username) { return userMapper.findUserByName(username); } @Override public User findUserByEmail (String email) { return userMapper.findUserByEmail(email); } @Override public User findUser (String username, String password) { return userMapper.findUser(username, password); }
TokenServcie 接口
public interface TokenService { User chekToken (String token) ; }
TokenServiceImpl 实现类
@Service("TokenService") public class TokenServiceImpl implements TokenService { @Autowired private RedisTemplate<String, String> redisTemplate; @Override public User chekToken (String token) { if (StringUtils.isBlank(token)) { return null ; } Map<String, Object> objectMap = JwtUtils.checkToken(token); if (objectMap == null ) { return null ; } String userJson = redisTemplate.opsForValue().get("Token_" + token); if (StringUtils.isBlank(userJson)) { return null ; } User user = JSON.parseObject(userJson, User.class); return null ; } }
4.3 Controller 核心业务逻辑开发 LoginController 登录业务类
@ResponseBody @RestController public class RegisterController { @Autowired RegisterService registerService; @RequestMapping(value = "/register") public CommonResult register (@RequestBody User user) { return registerService.register(user); } }
LogoutController 退出登录业务类
@RestController public class LogoutController { @Autowired LoginService loginService; @GetMapping(value = "/logout") public CommonResult logout (@RequestHeader("Authorization") String token) { return loginService.logout(token); } }
RegisterController 注册业务类
@ResponseBody @RestController public class RegisterController { @Autowired RegisterService registerService; @RequestMapping(value = "/register") public CommonResult register (@RequestBody User user) { return registerService.register(user); } }
五. 测试 ApiPost 进行后端测试
5.1 注册测试
这里有个小问题,我 success 返回的应该是 data 但这里是 token 貌似是我返回类有个地方写串行了,不过影响不大。
5.2 登录测试
5.3 Redis 测试
六. 总结: OK! 到这里后端基本上就写完了,里面有一点还可以再进行优化的地方,后面在说前端的时候会详细说的,其实总的来说这个项目难到我的不是后端,因为 SpringBoot 这一套还是比较对我而言的还是比较熟练的,前端才是最头痛的一件事,下一篇会详细说明我是在 Vue 里面坐牢的。
写在最后:
其实最最开始的时候,也就是我在考虑这个项目怎么做的时候,我有想过要不要以此来写一篇类似手把手的教学博客,但是后来放弃了,主要原因是真的没有什么时间去弄(主要是懒)其次就是这个项目我有给过行内的朋友们看过,真的是做的非常一般,所以我觉得真的没什么必要,而且这个世界上的小白太多了,我没有义务去手把手的教会他们,所以接下来关于这个项目的几篇文章,真的只是记录一下我的思路罢了,另外国内的抄袭风气实在是太严重了,从某园抄到某N。
说一下我做这个项目的理由吧:首先就是对自己技术栈的一个锻炼,其次就是我真的很想开发一个比较有意思的项目,外加现在市面上的这种前后端分离项目太成熟了,成熟到什么地步呢,可以这么说,项目和框架体量大的,可以大到初学者一眼看不出相互逻辑的,并且网上很少有将一个业务从大项目中分离开去给你解释的,所以对于初学者是非常的不友好。也算是对于自己开发的一种记录吧
最后吐槽一下:别看我就是简简单单贴了几行代码上去说说了思路,由于这个是真正能给用户用到的项目,所以很多写法不能想你在学校里面写大作业一样随便糊弄一下就过去了,看着简单,但这背后我付出的努力和遇到的困难是没有经历过的人是没有办法想到的。所以不要轻易的就去嘲笑一个人的成果不管是大是小。
分享 关于 Shotalk 即时聊天网站的开发(注册与登录-后端篇) 复制链接 微信二维码 系统分享
微信扫码打开,再通过右上角菜单分享。
交流与讨论
评论由 Valine / LeanCloud 提供,点击后连接第三方服务。