个人博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。
blog.mszlu.com
项目讲解说明:
- 提供前端工程,只需要实现后端接口即可
- 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
- 开发完成后,开始优化项目,提升编程思维能力
- 比如页面静态化,缓存,云存储,日志等
- docker部署上线
- 云服务器购买,域名购买,域名备案等
项目使用技术 :
springboot + mybatisplus+redis+mysql
目录
目录
一. 工程搭建
二. 首页展示
1.文章列表
2. 首页---最热标签
3 首页--最热文章
4. 最新文章显示
5. 文章归档显示
三. 登录
1. 登录功能实现
2. 获取用户信息
3. 退出登录
四. 用户注册
五. 登录拦截器
一. 工程搭建
1.1 前端用vscode打开并运行
npm run dev
运行结果
1.2 创建maven工程
导入依赖
org.springframework.boot spring-boot-starterorg.springframework.boot spring-boot-starter-loggingcom.qiniu qiniu-java-sdk[7.7.0, 7.7.99] org.springframework.boot spring-boot-starter-log4j2org.springframework.boot spring-boot-starter-aoporg.springframework.boot spring-boot-starter-mailorg.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-testtest org.springframework.boot spring-boot-starter-data-rediscom.alibaba fastjson1.2.76 mysql mysql-connector-javaorg.springframework.boot spring-boot-configuration-processortrue org.apache.commons commons-lang3commons-collections commons-collections3.2.2 commons-codec commons-codecorg.apache.commons commons-lang33.12.0 com.baomidou mybatis-plus-boot-starter3.4.3 org.projectlombok lombokjoda-time joda-time2.10.10 org.springframework.boot spring-boot-starter-jdbcio.jsonwebtoken jjwt0.9.1 org.springframework.boot spring-boot-maven-plugin2.6.7 src/main/resources ***.properties
1.3 application配置文件配置
#server
server.port= 8888
#应用名称
spring.application.name=hnjm_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/hnjm_blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#标识表名均为ms_前缀
mybatis-plus.global-config.db-config.table-prefix=ms_
mybatis-plus.type-aliases-package=com.hnjm.pojo
mybatis-plus.mapper-locations=classpath:com/hnjm/mapper
private String createDate;
private String author;
// private ArticleBodyVo body;
private List tags;
private CategoryVo category;
}
说明:java中long数据能表示的范围比js中number大,在跟前端交互时,这样也就意味着部分数值在js中存不下(变成不准确的值)。
解决办法可以这样:
使用fastjson的ToStringSerializer注解,让系统序列化时,保留相关精度。
(7)TagVo.java
@Data
public class TagVo {
private Long id;
private String tagName;
private String avatar;
}
(8)CategoryVo.java
@Data
public class CategoryVo {
private Long id;
private String avatar;
private String categoryName;
private String description;
}
(9)在param文件中创建PageParams.java
根据接口文档中,前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。
@Data
public class PageParams {
private int page = 1;
private int pageSize = 5;
private Long categoryId;
private Long tagId;
private String year;
private String month;
public String getMonth(){
if (this.month != null && this.month.length() == 1){
return "0"+this.month;
}
return this.month;
}
}
1.2.3 Mapper接口
此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。
public interface ArticleMapper extends BaseMapper {
}
public interface TagMapper extends BaseMapper{ List findTagsByArticleId(Long articleId); }
public interface SysUserMapper extends BaseMapper{ }
说明:
因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。
TagMapper.xml
select id,avatar,tag_name as tagName from ms_tag id in (select tag_id from ms_article_tag where article_id = #{articleId})
注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.hnjm.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。
1.2.4 Service
该阶段定义了三个service接口类。
public interface ArticleService {
//分页查询文章列表
Result listArticle(PageParams pageParams);
}
public interface SysUserService {
//根据文章id获取用户信息
SysUser findUserById(Long id);
}
public interface TagService {
//通过文章id查询ui赢得标签,有一张表专门映射文章id和标签id
List findTagsByArticleId(Long articleId);
}
serviceImpl类
(1)ArticleServiceImpl
该实现类目前实现了文章分页查询。
知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。
@Service
public class ArticleServiceImpl implements ArticleService{
@Autowired
ArticleMapper articleMapper;
@Autowired
TagService tagService;
@Autowired
SysUserService sysUserService;
//分页查询
@Override
public Result listArticle(PageParams pageParams) {
//分页
Page page = new Page<>(pageParams.getPage(),pageParams.getPageSize());
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
//先置顶排序由属性weight决定,后按照时间排序
wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
Page articlePage = articleMapper.selectPage(page, wrapper);
//文章列表
List records = articlePage.getRecords();
//因为页面展示出来的数据不一定和数据库数据一致,所以我们要做一个拷贝
//将查出的数据复制到articleVo中实现解耦,vo和页面数据交换
List articleVo = copyList(records,true,true);
return Result.success(articleVo);
}
//copyList实现
private List copyList(List records,boolean isTag,boolean isAuthor) {
ArrayList articleVos = new ArrayList<>();
for(Article article:records){
articleVos.add(copy(article,isTag,isAuthor));
}
return articleVos;
}
//这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,Article和articlevo相同属性的拷贝过来返回
private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
ArticleVo articleVo = new ArticleVo();
BeanUtils.copyProperties(article,articleVo);
//joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
//是否显示标签和作者
if(isTag){
articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
}
if(isAuthor){
articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
}
return articleVo;
}
}
(2) TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。
@Service
public class TagServiceImpl implements TagService{
@Autowired
TagMapper tagMapper;
@Override
public List findTagsByArticleId(Long articleId) {
List tags = tagMapper.findTagsByArticleId(articleId);
return copyList(tags);
}
//copyList实现
private List copyList(List tags) {
ArrayList tagVos = new ArrayList<>();
for(Tag tag : tags){
tagVos.add(copy(tag));
}
return tagVos;
}
private TagVo copy(Tag tag){
TagVo tagVo = new TagVo();
//BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
BeanUtils.copyProperties(tag,tagVo);
return tagVo;
}
}
(3)SysUserServiceImpl.java:根据id查找用户信息
@Service
public class SysUserServiceImpl implements SysUserService{
@Autowired
SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id) {
return sysUserMapper.selectById(id);
}
}
运行结果
也可以在postman 中测试
2. 首页---最热标签
2.1 接口文档
接口url:/tags/hot
请求方式:GET
请求参数:无
返回数据:
思路分析:
思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。
2.2 编码实现
2.2.1 controller
@RestController
@RequestMapping("/tags")
public class TagController {
@Autowired
TagService tagService;
//tags/hot相应最热标签tag对象
@RequestMapping("/hot")
public Result hot() {
int limit = 6;//最热六个
return tagService.hots(limit);
}
}
2.2.2 TagMapper.java
public interface TagMapper extends BaseMapper{ //根据id 查找tag列表 List findTagsByArticleId(Long articleId); //查询最热标签前limit条 List findHotsTagId(int limit); //通过最热标签tagid查询最热tags List findTagsByIds(List tagIds); }
TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签TagMapper.xml
2.2.3 TagService.java
Result hots(int limit);
TagServiceImp.java
//查询若干个最热标签功能
public Result hots(int limit){
List hotsTagId = tagMapper.findHotsTagId(limit);
//判断hotsTagId是否为空
if(CollectionUtils.isEmpty(hotsTagId)){
return Result.success(Collections.emptyList());
}
List tagsList = tagMapper.findTagsByIds(hotsTagId);
return Result.success(tagsList);
}
2.3 统一异常处理
定义Handler包,设置统一异常处理类AllExceptionHandler ,不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。
//对加了@Controller的方法进行拦截处理,AOP实现
@ControllerAdvice
public class AllExceptionHandler {
//进行异常处理,处理Exception.class异常
@ExceptionHandler(Exception.class)
//返回json数据
@ResponseBody
public Result doExceptionHandler(Exception e) {
return Result.fail(-999,"系统异常,抱歉!");
}
}
说明:
1)@ResponseBody :返回jsojn数据
2)@ControllerAdvice:对加了@Controller的方法进行拦截处理,AOP实现
3)@ExceptionHandler(Exception.class)://进行异常处理,处理Exception.class异常
运行结果
3 首页--最热文章
3.1 接口文档
接口url:/articles/hot
请求方式:POST
返回数据:
3.2 编码实现
思路:原理同最热标签查询。根据view_count排序,选择出最热文章。
(1)Articlecontroller
//显示最热文章
@PostMapping("/hot")
public Result hotArticle(){
int limit = 3;
return articleService.hotArticle(limit);
}
(2)ArticleService
Result hotArticle(int limit);
(3)ArticleServiceImpl
//根据view_counts字段查询最热文章
@Override
public Result hotArticle(int limit) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(Article::getViewCounts)
.select(Article::getId,Article::getTitle)
.last("limit "+limit);
List articles = articleMapper.selectList(wrapper);
return Result.success(copyList(articles,false,false));
}
运行结果
4. 最新文章显示
4.1 接口说明
接口url:/articles/new
请求方式:POST
返回数据
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"id": 1,
"title": "springboot介绍以及入门案例",
},
{
"id": 9,
"title": "Vue.js 是什么",
},
{
"id": 10,
"title": "Element相关",
}
]
}
4.2 编程实现
思路:原理和最热文章完全相同,只是通过create_date字段排序,选择出最新
(1)ArticleController
//显示最新文章
@PostMapping("/new")
public Result newArticle(){
int limit = 3;
return articleService.newArticle(limit);
}
(2)ArticleService
Result newArticle(int limit);
(3)ArticleServiceImpl
/最新文章
@Override
public Result newArticle(int limit) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.orderByDesc(Article::getCreateDate)
.select(Article::getId,Article::getTitle)
.last("limit "+limit);
List articles = articleMapper.selectList(wrapper);
return Result.success(copyList(articles,false,false));
}
运行结果
5. 文章归档显示
5.1 接口说明
接口url:/articles/listArchives
请求方式:POST
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": [
{
"year": "2021",
"month": "6",
"count": 2
}
]
}
5.2 编程实现
思路:
根据接口文档分析,归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。
文章归档返回的对象为Archives(档案)类,将这个类放入vo包中。
(1) Archives.java
@Data
public class Archives {
private Integer year;
private Integer month;
private Long count;
}
(2) ArticleController中添加方法
//文章归档
@PostMapping("/listArchives")
public Result listArchives(){
return articleService.listArchives();
}
(3)ArticleMapper.java 中添加方法
List listArchives();
对应的ArticleMapper.xml文件中添加方法
select YEAR(FROM_UNIXTIME(create_date/1000)) as year, MONTH(FROM_UNIXTIME(create_date/1000)) as month, count(*) as count from ms_article group by year,month
说明:数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。ROM_UNIXTIME 格式化MYSQL时间戳函数
将bigint类型转换为时间数据类型 from_unixtime(1488470400000/1000)
(4) ArticleService与ArticleServiceImpl
Result listArchives();
//文章归档
@Override
public Result listArchives() {
List archivesList = articleMapper.listArchives();
return Result.success(archivesList);
}
运行结果:
三. 登录
1. 登录功能实现
1.1 接口说明
接口url:/login
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
1.2 编程实现
(1)工具类:在util中添加JWT工具类
JWT
登录使用JWT技术。
jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。
请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。
jwt 有三部分组成:A.B.C
A:Header,{"type":"JWT","alg":"HS256"} 固定
B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息
C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。
jwt 验证,主要就是验证C部分 是否合法。
public class JWTUtils {
private static final String jwtToken = "123456Mszlu!@#$$";
public static String createToken(Long userId){
Map claims = new HashMap<>();
claims.put("userId",userId);
JwtBuilder jwtBuilder = Jwts.builder()
.signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
.setClaims(claims) // body数据,要唯一,自行设置
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
String token = jwtBuilder.compact();
return token;
}
public static Map checkToken(String token){
try {
Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
return (Map) parse.getBody();
}catch (Exception e){
e.printStackTrace();
}
return null;
}
}
(2)LoginParam (根据接口文档中参数定义)
import lombok.Data;
@Data
public class LoginParam {
private String account;
private String password;
private String nickname;
}
(3)LoginController
@RestController
@RequestMapping("/login")
public class LoginController {
@Autowired
private LoginService loginService;
//登录
@PostMapping
public Result login(@RequestBody LoginParam loginParam){
return loginService.login(loginParam);
}
}
(3)LoginService(登录由login去处理)
Result login(LoginParam loginParam);
(4)需要开启redis服务器,并在配置文件中加载
在application.properties配置文件中加上
#redis配置 spring.redis.host=localhost spring.redis.port=6379
(5)ErrorCode.class
因为每次登录都有错误验证,所以在vo包中定义了一个ErrorCode类
ErrorCode.class
public enum ErrorCode {
PARAMS_ERROR(10001,"参数有误"),
ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),
TOKEN_ERROR(10003,"Token不合法"),
ACCOUNT_EXIST(10004,"账号已存在"),
NO_PERMISSION(70001,"无访问权限"),
SESSION_TIME_OUT(90001,"会话超时"),
NO_LOGIN(90002,"未登录"),;
private int code;
private String msg;
ErrorCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
private SysUserService sysUserService;
@Autowired
private RedisTemplate redisTemplate;
//md5加密使用到的盐
private static final String salt="mszlu!@#";
@Override
public Result login(LoginParam loginParam) {
String account = loginParam.getAccount();
String password = loginParam.getPassword();
//用户名或者密码为空
if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){
//提前写好的错误编码类,方便使用
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
}
//这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖
//密码加盐,因为数据库中的密码是经过盐加密的
String pwd = DigestUtils.md5Hex(password+salt);
System.out.println(pwd);
//验证时需要通过sysUserService查询到对应用户并返回部分用户信息
SysUser user = sysUserService.findUser(account, pwd);
//用户不存在
if(user == null){//错误信息调用错误码
return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
}
//用户密码正确,给用户返回一个JWT (token)
String token = JWTUtils.createToken(user.getId());
//存入redis,要确保已开启redis
redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
return Result.success(token);
}
}
在SysUserService中添加方法
//根据账号、密码查找
SysUser findUser(String account, String password);
SysUserServiceImpl
@Override
public SysUser findUser(String account, String password) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(SysUser::getAccount,account);
queryWrapper.eq(SysUser::getPassword,password);
queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);
queryWrapper.last("limit 1");//加快速度
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
return sysUser;
}
最后在postman中测试,切记不要在前端测试,因为显示用户名信息没有写完,会死掉的
2. 获取用户信息
2.1 接口说明
接口url:/users/currentUser
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据
{
"success": true,
"code": 200,
"msg": "success",
"data": {
"id":1,
"account":"1",
"nickaname":"1",
"avatar":"ss"
}
}
2.2 编码实现
思路:登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。
(1)UserController
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
SysUserService sysUserService;
//用户信息展示请求
//token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解
@GetMapping("/currentUser")
public Result currentUser(@RequestHeader("Authorization") String token){
return sysUserService.findUserByToken(token);
}
}
(2)SysUserService
Result findUserByToken(String token); SysUser checkToken(String token);//从reddis中获取用户信息
(3)LoginUserVo(根据接口文档中定义类)
该类用于返回用户信息,由于页面展示用户部分信息
import lombok.Data;
@Data
public class LoginUserVo {
private Long id;
private String account;
private String nickname;
private String avatar; //头像
}
(4) SysUserServiceImpl
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public SysUser findUserById(Long id) {
return sysUserMapper.selectById(id);
}
@Override
public SysUser findUser(String account, String password) {
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(SysUser::getAccount, account);
queryWrapper.eq(SysUser::getPassword, password);
queryWrapper.select(SysUser::getId, SysUser::getAccount, SysUser::getAvatar, SysUser::getNickname);
queryWrapper.last("limit 1");//加快速度
SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
return sysUser;
}
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result findUserByToken(String token) {
SysUser user=checkToken(token);
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setAccount(user.getAccount());
loginUserVo.setId(user.getId());
loginUserVo.setAvatar(user.getAvatar());
loginUserVo.setNickname(user.getNickname());
return Result.success(loginUserVo);
}
public SysUser checkToken(String token) {
if(StringUtils.isBlank(token)){//如果对象为空
return null;
}
Map map = JWTUtils.checkToken(token);//解析token
if(map == null){
return null;
}
String userJson= redisTemplate.opsForValue().get("TOKEN_"+token);
if(StringUtils.isBlank(userJson)){
return null;
}
//token解析成功,并且redis存在
//JSON.parseObject将json对象转为SysUser对象
SysUser sysUser=JSON.parseObject(userJson,SysUser.class);//将json对象转换为use
return sysUser;
}
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getAccount,account)
.last("limit 1");
return this.sysUserMapper.selectOne(wrapper);
}
@Override
public void save(SysUser sysUser) {
//保存用户id会自动生成
//默认生成分布式id,采用雪花算法
//mybatis-plus
sysUserMapper.insert(sysUser);
}
}
运行结果
3. 退出登录
3.1 接口文档
接口url:/logout
请求方式:GET
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| Authorization | string | 头部信息(TOKEN) |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": null
}
3.2 编码实现
思路:退出登录就是删除redis中的toke
(1)LoginOutController
@RestController
@RequestMapping("/logout")
public class LoginOutController {
@Autowired
private LoginService loginService;
@GetMapping
public Result logout(@RequestHeader("Authorization") String token){
return loginService.logout(token);
}
}
(2)LoginService和LoginServiceImpl
Result logout(String token);
//退出登录
@Override
public Result logout(String token) {
redisTemplate.delete("TOKEN_"+token);
return Result.success(null);
}
运行结果:
四. 用户注册
4.1. 接口说明
接口url:/register
请求方式:POST
请求参数:
| 参数名称 | 参数类型 | 说明 |
|---|---|---|
| account | string | 账号 |
| password | string | 密码 |
| nickname | string | 昵称 |
返回数据:
{
"success": true,
"code": 200,
"msg": "success",
"data": "token"
}
4.2 编码实现
(1)RegisterController
@RestController
@RequestMapping("/register")
public class RegisterController {
@Autowired
LoginService loginService;
//注册功能,返回数据为token
@PostMapping
public Result register(@RequestBody LoginParam loginParam){
return loginService.register(loginParam);
}
}
(2)注册功能在LoginService实现
LoginService与LoginServiceImpl
Result register(LoginParam loginParam);
思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.
LoginServiceImpl
@Override
public Result register(LoginParam loginParam) {
String account = loginParam.getAccount();
String password = loginParam.getPassword();
String nickname = loginParam.getNickname();
//用户参数为空
if(StringUtils.isBlank(account)||StringUtils.isBlank(password)||StringUtils.isBlank(nickname)){
return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
}
SysUser sysUser = sysUserService.findUserByAccount(account);
//用户已经存在
if(sysUser!=null){
return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), "账号已经被注册");
}
//创建用户,ID默认为自增
sysUser =new SysUser();
sysUser.setAccount(account); //账户名
sysUser.setNickname(nickname); //昵称
sysUser.setPassword(DigestUtils.md5Hex(password+salt)); //密码加盐md5
sysUser.setCreateDate(System.currentTimeMillis()); //创建时间
sysUser.setLastLogin(System.currentTimeMillis()); //最后登录时间
sysUser.setAvatar("/static/img/logo.b3a48c0.png"); //头像
sysUser.setAdmin(1); //管理员权限
sysUser.setDeleted(0); //假删除
sysUser.setSalt(""); //盐
sysUser.setStatus(""); //状态
sysUser.setEmail(""); //邮箱
this.sysUserService.save(sysUser);
//生成token
String token = JWTUtils.createToken(sysUser.getId());
//注册成功 使用Jwt生成Token ,返回Token 并 存到redis
redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);
return Result.success(token);
}
上面代码中涉及到两个方法
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户
@Override
public SysUser findUserByAccount(String account) {
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SysUser::getAccount,account)
.last("limit 1");
return this.sysUserMapper.selectOne(wrapper);
}
@Override
public void save(SysUser sysUser) {
//保存用户id会自动生成
//默认生成分布式id,采用雪花算法
//mybatis-plus
sysUserMapper.insert(sysUser);
}
运行结果:
五. 登录拦截器
每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。
那么可不可以统一进行登录判断呢?
可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。
5.1 定义拦截器LoginInterceptor
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Autowired
public SysUserService sysUserService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if(!(handler instanceof HandlerMethod)){
//拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler
//handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法
//解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethod
return true;
}
//获取token
String token = request.getHeader("Authorization");
//日志问题,需要导入lombok下的@slf4
log.info("=============request start=================");
String requestURI = request.getRequestURI();
log.info("request uri:{}",requestURI);
log.info("request method:{}",request.getMethod());
log.info("token:{}",token);
log.info("=============request end===================");
//token为空,不放行
if(StringUtils.isBlank(token)){
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
//设置返回消息格式
response.setContentType("application/json;charset=utf8");
//返回json信息
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//token不为空,去做认证
SysUser sysUser =sysUserService.checkToken(token) ;
//用户不存在,即认证失败
if(sysUser == null){
System.out.println("没有该用户");
Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
response.setContentType("application/json;charset=utf8");
response.getWriter().print(JSON.toJSONString(result));
return false;
}
//登陆验证成功,放行
return true;
}
}
使拦截器生效,必须要在WebMVCConfig添加配置
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置拦截接口,此处配置为test,用于测试
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/test");
}
创建测试类
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping
public Result test(){
return Result.success(null);
}
}



