Springboot+Vue博客项目总结,web开发平台

依赖包:

io.jsonwebtoken

jjwt

0.9.1

工具类:

public class JWTUtils {

//密钥

private static final String jwtToken = “123456Mszlu!@#$$”;

//生成token

public static String createToken(Long userId){

Map<String,Object> 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;

}

//检查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;

}

public static void main(String[] args) {

String token=JWTUtils.createToken(100L);

System.out.println(token);

Map<String, Object> map = JWTUtils.checkToken(token);

System.out.println(map.get(“userId”));

}

}

3.3 Controller

@RestController

@RequestMapping(“login”)

public class loginController {

@Autowired

private LoginService loginService;

@PostMapping

public Result login(@RequestBody LoginParam loginParam){

//登录->验证用户

return loginService.login(loginParam);

}

}

3.4 Service

关于这里StringUtils的用法:Java之StringUtils的常用方法

md5加密的依赖包:

commons-codec

commons-codec

@Service

public class LoginServiceImpl implements LoginService {

@Autowired

private SysUserService sysUserService;

@Autowired

private RedisTemplate<String, String> redisTemplate;

//加密盐

private static final String slat = “mszlu!@#”;

@Override

public Result login(LoginParam loginParam) {

//1.检查参数是否合法

String account = loginParam.getAccount();

String password = loginParam.getPassword();

if (StringUtils.isBlank(account) || StringUtils.isAllBlank(password)) {

return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());

}

//用md5加密

password = DigestUtils.md5Hex(password + slat);

//2.根据用户名何密码去user表中查询 是否存在

SysUser sysUser = sysUserService.findUser(account, password);

//3.如果不存在 登录失败

if (sysUser == null) {

return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(), ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());

}

//4.如果存在 使用jwt 生成token 返回给前端

String token = JWTUtils.createToken(sysUser.getId());

//5.toekn放入redis,设置过期时间。登录认证的时候先认证token字符串是否合法,在认证redsi认证是否合法

redisTemplate.opsForValue().set(“TOKEN_” + token, JSON.toJSONString(sysUser), 1, TimeUnit.DAYS);

return Result.success(token);

}

}

/**

  • 根据account和password查询用户表

  • @param account

  • @param password

  • @return

*/

@Override

public SysUser findUser(String account, String password) {

LambdaQueryWrapper queryWrapper=new LambdaQueryWrapper<>();

queryWrapper.eq(SysUser::getAccount,account);

queryWrapper.eq(SysUser::getPassword,password);

//需要id,account,头像avatar,naickname昵称

queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);

queryWrapper.last(“limit 1”);

SysUser sysUser = sysUserMapper.selectOne(queryWrapper);

return sysUser;

}

3.5 登录参数,redis配置

接受前端传来的登录参数:

@Data

public class LoginParam {

private String account;

private String password;

}

配置redis:

spring.redis.host=localhost

spring.redis.port=6379

5.获取用户信息


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SK3ukVrG-1638022304550)(../../../AppData/Roaming/Typora/typora-user-images/image-20211125224045403.png)]

5.1 接口说明

接口url:/users/currentUser

请求方式:GET

请求参数:

| 参数名称 | 参数类型 | 说明 |

| — | — | — |

| Authorization | string | 头部信息(TOKEN) |

返回数据:

{

“success”: true,

“code”: 200,

“msg”: “success”,

“data”: {

“id”:1,

“account”:“1”,

“nickaname”:“1”,

“avatar”:“ss”

}

}

5.2 Controller

@RestController

@RequestMapping(“users”)

public class UserController {

@Autowired

private SysUserService sysUserService;

@GetMapping(“currentUser”)

public Result currentUser(@RequestHeader(“Authorization”) String token){

return sysUserService.findUserByToken(token);

}

}

5.3 Service

/**

  • 根据token查询用户信息

  • @param token

  • @return

*/

@Override

public Result findUserByToken(String token) {

/**

  • 1.token合法性校验:是否为空,解析是否成功,redis是否存在

  • 2.如果校验失败,返回错误

  • 3.如果成功,返回对应的结果 LoginUserVo

*/

SysUser sysUser=loginService.checkToken(token);

if(sysUser==null){

return Result.fail(ErrorCode.TOKEN_ERROR.getCode() ,ErrorCode.TOKEN_ERROR.getMsg());

}

LoginUserVo loginUserVo = new LoginUserVo();

loginUserVo.setId(String.valueOf(sysUser.getId()));

loginUserVo.setNickname(sysUser.getNickname());

loginUserVo.setAccount(sysUser.getAccount());

loginUserVo.setAvatar(sysUser.getAvatar());

return Result.success(loginUserVo);

}

/**

  • 校验token是否合法

  • @param token

  • @return

*/

@Override

public SysUser checkToken(String token) {

if (StringUtils.isAllBlank(token)) {

return null;

}

Map<String, Object> stringObjectMap = JWTUtils.checkToken(token);

if (stringObjectMap == null) {

return null;

}

String userJson = redisTemplate.opsForValue().get(“TOKEN_” + token);

if (StringUtils.isBlank(userJson)) {

return null;

}

SysUser sysUser = JSON.parseObject(userJson, SysUser.class);

return sysUser;

}

6.登录拦截器


每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。

6.1 拦截器实现

@Slf4j

@Component

public class LoginInterceptor implements HandlerInterceptor {

@Autowired

private LoginService loginService;

/**

  • 在执行controlle方法之前执行

  • @param request

  • @param response

  • @param handler

  • @return

  • @throws Exception

*/

@Override

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

/**

  • 1、需要判断请求的接口和路径是否为 HandlerMethod(controller方法)

  • 2、如果token是否为空,如果为空,为登录

  • 3、如果token不为空,登录验证 loginService->checkToken

  • 4、如果认证成功,放行

*/

if (!(handler instanceof HandlerMethod)) {

//handler可能是RequestResourceHandler 放行

return true;

}

String token = request.getHeader(“Authorization”);

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===========”);

if (StringUtils.isBlank(token)) {

//未登录

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), “未登录”);

response.setContentType(“application/json;charset=utf-8”);

response.getWriter().print(JSON.toJSONString(result));

return false;

}

SysUser sysUser = loginService.checkToken(token);

if (sysUser == null) {

//未登录

Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), “未登录”);

response.setContentType(“application/json;charset=utf-8”);

response.getWriter().print(JSON.toJSONString(result));

return false;

}

//登录验证成功

//用ThreadLocal保存用户信息

UserThreadLocal.put(sysUser);

return true;

}

@Override

public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

//如果不删除,ThreaLocal中用完的信息会有内存泄漏的风险

UserThreadLocal.remove();

}

}

6.2 使拦截器生效

@Configuration

public class WebConfig implements WebMvcConfigurer {

@Autowired

private LoginInterceptor loginInterceptor;

//跨域配置,前端和后端端口不一样

@Override

public void addCorsMappings(CorsRegistry registry) {

registry.addMapping(“/**”).allowedOrigins(“http://localhost:8080”);

}

//使拦截器生效

@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);

}

}

7.ThreadLocal保存用户信息


public class UserThreadLocal {

private UserThreadLocal(){

}

private static final ThreadLocal LOCAL=new ThreadLocal<>();

//存入

public static void put(SysUser sysUser){

LOCAL.set(sysUser);

}

//取出

public static SysUser get(){

return LOCAL.get();

}

//删除

public static void remove(){

LOCAL.remove();

}

}

8. 使用线程池更新阅读次数


可以参考:在SpringBoot中实现异步事件驱动

8.1 线程池配置

@ControllerAdvice

@EnableAsync //开启多线程

public class ThreadPoolConfig {

@Bean(“taskExecutor”)

public Executor asyncServiceExecutor(){

ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();

//设置核心线程数

executor.setCorePoolSize(5);

//设置最大线程数

executor.setMaxPoolSize(20);

//设置队列大小

executor.setQueueCapacity(Integer.MAX_VALUE);

//设置线程活跃时间(秒)

executor.setKeepAliveSeconds(60);

//设置默认线程名称

executor.setThreadNamePrefix(“小皮皮博客项目”);

//等待所有任务结束后再关闭线程池

executor.setWaitForTasksToCompleteOnShutdown(true);

//执行初始化

executor.initialize();

return executor;

}

}

8.2 使用

@Autowired

private ThreadService threadService;

/**

  • 查看文章详情

  • @param articleId

  • @return

*/

@Override

public Result findArticleById(Long articleId) {

Article article = articleMapper.selectById(articleId);

ArticleVo articleVo = copy(article, true, true, true, true);

//查看完文章了,新增阅读数,有没有问题呢?

//查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低(没办法解决,增加阅读数必然要加锁)

//更新增加了此次接口的耗时(考虑减少耗时)如果一旦更新出问题,不能影响查看操作

//线程池解决,可以吧更新操作更新到主线程中执行,和主线程就不相关了

threadService.updateArticleViewCount(articleMapper, article);

return Result.success(articleVo);

}

@Component

public class ThreadService {

//如果我们想在调用一个方法的时候开启一个新的线程开始异步操作,我们只需要在这个方法上加上@Async注解,当然前提是,这个方法所在的类必须在Spring环境中。

@Async(“taskExecutor”)

//期望此操作在线程池执行。不会影响原有的主线程

public void updateArticleViewCount(ArticleMapper articleMapper, Article article) {

Article articleUpdate = new Article();

int viewCounts = article.getViewCounts();

articleUpdate.setViewCounts(viewCounts + 1);

LambdaQueryWrapper

queryWrapper = new LambdaQueryWrapper<>();

queryWrapper.eq(Article::getId, article.getId());

//为了在多线程的环境下,线程安全 CAS思想,防止此时修改的时候已经被修改了(乐观锁)

queryWrapper.eq(Article::getViewCounts, viewCounts);

//第一个参数用于生成set条件,第二个生成where语句

//update article set view_count =100 where view_count==99 and id =xxx

articleMapper.update(articleUpdate, queryWrapper);

try {

Thread.sleep(5000);

System.out.println(“更新完成了!”);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

这里的update用法:

// 根据 whereWrapper 条件,更新记录

int update(@Param(Constants.ENTITY) T updateEntity, @Param(Constants.WRAPPER) Wrapper whereWrapper);

参数说明:

| 类型 | 参数名 | 描述 |

| :-: | :-: | :-: |

| T | entity | 实体对象 (set 条件值,可为 null) |

| Wrapper | updateWrapper | 实体对象封装操作类(可以为 null,里面的 entity 用于生成 where 语句) |

@Async注解:如果我们想在调用一个方法的时候开启一个新的线程开始异步操作,我们只需要在这个方法上加上@Async注解,当然前提是,这个方法所在的类必须在Spring环境中。

9.评论


9.1 接口说明

接口url:/comments/create/change

请求方式:POST

请求参数:

| 参数名称 | 参数类型 | 说明 |

| — | — | — |

| articleId | long | 文章id |

| content | string | 评论内容 |

| parent | long | 父评论id |

| toUserId | long | 被评论的用户id |

返回数据:

{

“success”: true,

“code”: 200,

“msg”: “success”,

“data”: null

}

9.2 加入到登录拦截器中

@Override

public void addInterceptors(InterceptorRegistry registry) {

//拦截test接口,后续实际遇到需要拦截的接口时,在配置为真正的拦截接口

registry.addInterceptor(loginInterceptor)

.addPathPatterns(“/test”).addPathPatterns(“/comments/create/change”);

}

9.3 Controller

构建评论参数对象:

package com.mszlu.blog.vo.params;

import lombok.Data;

@Data

public class CommentParam {

private Long articleId;

private String content;

private Long parent;

private Long toUserId;

}

@PostMapping(“create/change”)

public Result comment(@RequestBody CommentParam commentParam){

return commentsService.comment(commentParam);

}

9.4 Service

@Override

public Result comment(CommentParam commentParam) {

SysUser sysUser = UserThreadLocal.get();

Comment comment = new Comment();

comment.setArticleId(commentParam.getArticleId());

comment.setAuthorId(sysUser.getId());

comment.setContent(commentParam.getContent());

comment.setCreateDate(System.currentTimeMillis());

Long parent = commentParam.getParent();

if(parentnull||parent0){

comment.setLevel(1);

}else{

comment.setLevel(2);

}

comment.setParentId(parent==null?0:parent);

Long toUserId = commentParam.getToUserId();

comment.setToUid(toUserId==null?0:toUserId);

commentMapper.insert(comment);

return Result.success(null);

}

//防止前端 精度损失 把id转为string

//分布式id 比较长,传到前端 会有精度损失,必须转为string类型 进行传输,就不会有问题了

@JsonSerialize(using = ToStringSerializer.class)

private Long id;

10.AOP统一记录日志


关于AOP的文章可以参考:

自己实现一个日志注解

//Type代表可以放在类上面,METHOD代表可以放在方法上

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface LogAnnotation {

String module() default “”;

String operator() default “”;

}

统一日志处理切面

@Component

@Aspect //切面 定义通知和切点的关系

@Slf4j

public class LogAspect {

@Pointcut(“@annotation(com.xpp.blog.common.aop.LogAnnotation)”)

public void pt(){

}

//环绕通知

@Around(“pt()”)

public Object log(ProceedingJoinPoint point) throws Throwable {

long beginTime = System.currentTimeMillis();

//执行方法

Object result = point.proceed();

//执行时长

long time=System.currentTimeMillis()-beginTime;

//保存日志

recordLog(point,time);

return result;

}

private void recordLog(ProceedingJoinPoint joinPoint, long time) {

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

Method method = signature.getMethod();

LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);

log.info(“=log start============”);

log.info(“module:{}”,logAnnotation.module());

log.info(“operation:{}”,logAnnotation.operator());

//请求的方法名

String className = joinPoint.getTarget().getClass().getName();

String methodName = signature.getName();

log.info(“request method:{}”,className + “.” + methodName + “()”);

// //请求的参数

Object[] args = joinPoint.getArgs();

String params = JSON.toJSONString(args[0]);

log.info(“params:{}”,params);

//获取request 设置IP地址

HttpServletRequest request = HttpContextUtils.getHttpServletRequest();

log.info(“ip:{}”, IpUtils.getIpAddr(request));

log.info(“excute time : {} ms”,time);

log.info(“=log end============”);

}

}

使用

@PostMapping(“”)

//加上该注解代表要对此接口记录日志

@LogAnnotation(module = “文章”, operator = “获取文章列表”)

public Result listArticles(@RequestBody PageParams params) {

return articleService.listArticle(params);

}

11.文章图片上传


11.1 接口说明

接口url:/upload

请求方式:POST

请求参数:

| 参数名称 | 参数类型 | 说明 |

| — | — | — |

| image | file | 上传的文件名称 |

返回数据:

{

“success”:true,

“code”:200,

“msg”:“success”,

“data”:“https://static.mszlu.com/aa.png”

}

导入七牛云依赖:

com.qiniu

qiniu-java-sdk

[7.7.0, 7.7.99]

11.2 Controller

@RestController

@RequestMapping(“upload”)

public class UploadController {

@Autowired

private QiniuUtils qiniuUtils;

@PostMapping

public Result upload(@RequestParam(“image”)MultipartFile file){

//原始文件名称 比如xpp.png

String originalFilename = file.getOriginalFilename();

//得到一个唯一的文件名称

String fileName=UUID.randomUUID().toString()+“.”+ StringUtils.substringAfterLast(originalFilename,“.”);

//上传文件

boolean upload = qiniuUtils.upload(file, fileName);

if(upload){

return Result.success(QiniuUtils.url+fileName);

}

return Result.fail(20001,“上传失败”);

}

}

11.3 使用七牛云

配置上传文件的大小:

上传文件总的最大值

spring.servlet.multipart.max-request-size=20MB

单个文件的最大值

spring.servlet.multipart.max-file-size=2MB

@Component

public class QiniuUtils {

//填自己七牛云绑定的域名

public static final String url = “xxxxxxxxxxxx”;

//从配置文件读取

@Value(“${qiniu.accessKey}”)

private String accessKey;

@Value(“${qiniu.accessSecretKey}”)

private String accessSecretKey;

public boolean upload(MultipartFile file,String fileName){

//构造一个带指定 Region 对象的配置类

Configuration cfg = new Configuration(Region.huanan());

//…其他参数参考类注释

UploadManager uploadManager = new UploadManager(cfg);

//…生成上传凭证,然后准备上传

String bucket = “xppll”;

//默认不指定key的情况下,以文件内容的hash值作为文件名

try {

byte[] uploadBytes = file.getBytes();

Auth auth = Auth.create(accessKey, accessSecretKey);

String upToken = auth.uploadToken(bucket);

Response response = uploadManager.put(uploadBytes, fileName, upToken);

//解析上传成功的结果

DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class);

return true;

} catch (Exception ex) {

ex.printStackTrace();

}

return false;

}

}

12.AOP实习统一缓存处理(优化)


内存的访问速度 远远大于 磁盘的访问速度 (1000倍起)

自定义注解:

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

public @interface Cache {

//过期时间

long expire() default 1601000;

//缓存标识 key

String name() default “”;

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
img

打开全栈工匠技能包-1小时轻松掌握SSR

两小时精通jq+bs插件开发

生产环境下如歌部署Node.js

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

网易内部VUE自定义插件库NPM集成

谁说前端不用懂安全,XSS跨站脚本的危害

webpack的loader到底是什么样的?两小时带你写一个自己loader

3年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-9AA3LTbX-1711992493295)]
[外链图片转存中…(img-3CIM9a8G-1711992493296)]
[外链图片转存中…(img-M3aqTgtW-1711992493296)]
[外链图片转存中…(img-Ys3Ka66C-1711992493296)]
[外链图片转存中…(img-o2bOAL8q-1711992493296)]
[外链图片转存中…(img-zYbgOqvo-1711992493297)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-01O4uYpk-1711992493297)]

打开全栈工匠技能包-1小时轻松掌握SSR

两小时精通jq+bs插件开发

生产环境下如歌部署Node.js

CodeChina开源项目:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

网易内部VUE自定义插件库NPM集成

谁说前端不用懂安全,XSS跨站脚本的危害

webpack的loader到底是什么样的?两小时带你写一个自己loader

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值