spring boot+vue个人博客二:权限整合shiro+jwt,完成后端接口

Springboot + Vue + shiro 实现前后端分离、权限控制 本文总结自实习中对项目的重构。原先项目采用Springboot+freemarker模版,开发过程中觉得前端逻辑写的实在恶心,后端Controller层还必须返回Freemarker模版的ModelAndView,逐渐有了前后端分离的想法,由于之前,没有接触过,主要参考的还是网上的一些博客教程等,初步完成了前后端分离,在此记录以备查阅。 一、前后端分离思想 前端从后端剥离,形成一个前端工程,前... 阅读详情
系列大纲
GitHub(还没开发完成)
在线演示www.yzpnb.top
第一章:(后端)搭建spring boot脚手架:https://blog.youkuaiyun.com/grd_java/article/details/107452826
第二章:权限整合shiro+jwt,完成后端接口:https://blog.youkuaiyun.com/grd_java/article/details/107481570
第三章:搭建前端vue整合element ui脚手架:https://blog.youkuaiyun.com/grd_java/article/details/107498140
第四章:首页和公共头部组件:https://blog.youkuaiyun.com/grd_java/article/details/107507213
第五章:完成登陆注册页面,nginx网关对接后端:https://blog.youkuaiyun.com/grd_java/article/details/107515437
第六章:博客页面,观看博客,博客编辑功能:https://blog.youkuaiyun.com/grd_java/article/details/107525128
第七章:打包部署,多节点部署方案:https://blog.youkuaiyun.com/grd_java/article/details/107533253
在b站发现了一个不错的前后端分离博客项目
原本我是想整合spring security来做权限,手写一套响应式前端。但是最近学校要求我去整合项目
所以我前往b站找了找现成的,但是教学视频大多数为了让观众容易理解,将大部分分布式和扩展性内容极简化
我决定找一个相对简单的,但是所有的都是用的shiro做权限,所以我这里就先把shiro整合到分布式项目
这个项目我整合到了脚手架,有了充分的扩展性,大家可以跟着把这个做完部署到服务器
之后我将学校的任务完成后,会将shiro换成spring security,然后将页面和功能重写
到时候,我会用sass做css的模块化,并自己封装一套vue组件库来搭建前端
以下是我找的开源博客项目,我仅仅使用了shiro代码,因为原视频中没有用到分布式的模块化
我在b站找的视频链接:https://www.bilibili.com/video/BV1PQ4y1P7hZ
相应博客链接:https://juejin.im/post/5ecfca676fb9a04793456fb8#heading-6

一、整合shiro+jwt

如果你不想整合shiro做权限控制,直接跳到登陆接口开发就可以了,但是仅仅是拷贝代码就可以完成shiro的整合

1、创建通用模块,引入shiro和jwt依赖

在这里插入图片描述

 <dependencies>
        <!--shiro redis spring boot 整合包,这个包整合了3个-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.2.1</version>
        </dependency>
        <!-- hutool工具类-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.3</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>
    </dependencies>

2、配置shiro

依赖管理
因为需要common中其他内容

在这里插入图片描述
在这里插入图片描述

1、JwtToken

在这里插入图片描述

package com.yzpnb.shiro.handler;

import org.apache.shiro.authc.AuthenticationToken;

public class JwtToken implements AuthenticationToken {
    /**
     * shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法
     */
    private String token;
    public JwtToken(String token) {
        this.token = token;
    }
    @Override
    public Object getPrincipal() {
        return token;
    }
    @Override
    public Object getCredentials() {
        return token;
    }
}

2、JwtUtils

配置文件

在这里插入图片描述

shiro-redis:
  enabled: true
  redis-manager:
    host: 127.0.0.1:6379
yzpnb:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效时长,7天,单位秒
    expire: 604800
    header: token

在这里插入图片描述

package com.yzpnb.shiro.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
@ConfigurationProperties(prefix = "yzpnb.jwt")
public class JwtUtils {
    /**
     * JwtUtils是个生成和校验jwt的工具类,其中有些jwt相关的密钥信息是从项目配置文件中配置的
     */
    private String secret;
    private long expire;
    private String header;
    /**
     * 生成jwt token
     */
    public String generateToken(String userId) {
        Date nowDate = new Date();
        //过期时间
        Date expireDate = new Date(nowDate.getTime()+expire * 1000);
        return Jwts.builder()
                .setHeaderParam("typ","JWT")
                .setSubject(userId)
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS512,secret)
                .compact();
    }

    // 获取jwt的信息
    public Claims getClaimByToken(String token) {
        try{
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch(Exception e){
            return null;
        }
    }

    /**
     * token是否过期
     * @return  true:过期
     */
    public boolean isTokenExpired(Date expiration) {
        return expiration.before(new Date());
    }
}

3、AccountProfile

在这里插入图片描述

package com.yzpnb.shiro.handler;

import lombok.Data;

import java.io.Serializable;

@Data
public class AccountProfile implements Serializable {
    /**
     * 这是为了登录成功之后返回的一个用户信息的载体,
     */
    private Long id;
    private String username;
    private String avatar;
}

4、AccountRealm

在这里插入图片描述

package com.yzpnb.shiro.handler;

import com.yzpnb.shiro.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class AccountRealm extends AuthorizingRealm {
    /**
     * AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是
     *
     * supports:为了让realm支持jwt的凭证校验
     * doGetAuthorizationInfo:权限校验
     * doGetAuthenticationInfo:登录认证校验
     *
     * 作者:MarkerHub
     * 链接:https://juejin.im/post/5ecfca676fb9a04793456fb8
     * 来源:掘金
     * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     */
    @Autowired
    JwtUtils jwtUtils;
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        return null;
    }

    /**
     * 注意这个方法我们先不重写,因为这是一个通用的,而这个涉及到微服务,我们在微服务单独重写
     * @param token
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}
在微服务中完善剩下的逻辑

在这里插入图片描述

package com.yzpnb.service_blog.shiro;

import cn.hutool.core.bean.BeanUtil;
import com.yzpnb.service_blog.entity.MUser;
import com.yzpnb.service_blog.service.MUserService;
import com.yzpnb.shiro.handler.AccountProfile;
import com.yzpnb.shiro.handler.AccountRealm;
import com.yzpnb.shiro.handler.JwtToken;
import com.yzpnb.shiro.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ServiceAccountRealm extends AccountRealm {
    /**
     * 其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到我们通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。
     * 接下来我们逐步分析里面出现的新类
     *
     * 作者:MarkerHub
     * 链接:https://juejin.im/post/5ecfca676fb9a04793456fb8
     * 来源:掘金
     * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     */
    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    MUserService mUserService;
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        JwtToken jwt = (JwtToken) token;
        log.info("jwt----------------->{}", jwt);
        String userId = jwtUtils.getClaimByToken((String) jwt.getPrincipal()).getSubject();
        MUser mUser = mUserService.getById(Long.parseLong(userId));
        if(mUser == null) {
            throw new UnknownAccountException("账户不存在!");
        }
        if(mUser.getStatus() == -1) {
            throw new LockedAccountException("账户已被锁定!");
        }
        AccountProfile profile = new AccountProfile();
        BeanUtil.copyProperties(mUser, profile);
        log.info("profile----------------->{}", profile.toString());
        return new SimpleAuthenticationInfo(profile, jwt.getCredentials(), getName());
    }
}

5、jwt过滤器

在这里插入图片描述

package com.yzpnb.shiro.filter;

import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.yzpnb.common_utils.Result;
import com.yzpnb.shiro.handler.JwtToken;
import com.yzpnb.shiro.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.ExpiredCredentialsException;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtFilter extends AuthenticatingFilter {

    /**
     * 定义jwt的过滤器JwtFilter。
     * 这个过滤器是我们的重点,这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。
     * 我们需要重写几个方法:
     *
     * createToken:实现登录,我们需要生成我们自定义支持的JwtToken
     * onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
     * onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
     * preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。
     *
     * 作者:MarkerHub
     * 链接:https://juejin.im/post/5ecfca676fb9a04793456fb8
     * 来源:掘金
     * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     */
    @Autowired
    JwtUtils jwtUtils;
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        // 获取 token
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return null;
        }
        return new JwtToken(jwt);
    }
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String token = request.getHeader("Authorization");
        if(StringUtils.isEmpty(token)) {
            return true;
        } else {
            // 判断是否已过期
            Claims claim = jwtUtils.getClaimByToken(token);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
                throw new ExpiredCredentialsException("token已失效,请重新登录!");
            }
        }
        // 执行自动登录
        return executeLogin(servletRequest, servletResponse);
    }
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        try {
            //处理登录失败的异常
            Throwable throwable = e.getCause() == null ? e : e.getCause();
            Result r = Result.error().message(throwable.getMessage());
            String json = JSONUtil.toJsonStr(r);
            httpResponse.getWriter().print(json);
        } catch (IOException e1) {
        }
        return false;
    }
    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
        HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个OPTIONS请求,这里我们给OPTIONS请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(org.springframework.http.HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }
}

6、最终配置ShiroConfig

在这里插入图片描述

package com.yzpnb.shiro.config;

import com.yzpnb.shiro.filter.JwtFilter;
import com.yzpnb.shiro.handler.AccountRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * 配置类
 */
@Configuration//告诉spring 这个是配置类
public class ShiroConfigura {
    /**
     * 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
     * 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
     * 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。
     *
     * 作者:MarkerHub
     * 链接:https://juejin.im/post/5ecfca676fb9a04793456fb8
     * 来源:掘金
     * 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
     */
    @Autowired
    private JwtFilter jwtFilter;
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {//这里报错不用管,idea的原因
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }
    @Bean
    public DefaultWebSecurityManager securityManager(AccountRealm accountRealm,
                                                     SessionManager sessionManager,
                                                     RedisCacheManager redisCacheManager) {//这里报错不用管,idea的原因
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setCacheManager(redisCacheManager);
        /*
         * 关闭shiro自带的session,详情见文档
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    /**
     * 定义shiro过滤
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "jwt"); // 主要通过注解方式校验权限
        chainDefinition.addPathDefinitions(filterMap);
        return chainDefinition;
    }
    @Bean("shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        Map<String, Filter> filters = new HashMap<>();
        filters.put("jwt", jwtFilter);
        shiroFilter.setFilters(filters);
        Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilter.setFilterChainDefinitionMap(filterMap);
        return shiroFilter;
    }

    // 开启注解代理(默认好像已经开启,可以不要)
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        return creator;
    }
}

7、异常处理

在这里插入图片描述

package com.yzpnb.shiro.handler;

import com.yzpnb.common_utils.Result;
import com.yzpnb.service_base.handler.MyExceptionHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;

import java.io.IOException;

@Slf4j
@Component
public class ShiroExceptionHandler extends MyExceptionHandler {
    // 捕捉shiro的异常
    @ResponseBody
    @ExceptionHandler(ShiroException.class)
    public Result handle401(org.apache.shiro.ShiroException e) {
        return Result.error().code(401).message("你好"+e.getMessage());
    }
    /**
     * 处理Assert的异常
     */
    @ResponseBody
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result handler(IllegalArgumentException e) throws IOException {
        return Result.error().message(e.getMessage());
    }
    /**
     * @Validated 校验错误异常处理
     */
    @ResponseBody
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result handler(MethodArgumentNotValidException e) throws IOException {
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.error().message(objectError.getDefaultMessage());
    }

    @ResponseBody
    @ExceptionHandler(value = RuntimeException.class)
    public Result handler(RuntimeException e) throws IOException {
        return Result.error().message(e.getMessage());
    }
}

8、测试

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

到这里,shiro就整合的差不多了,这里就充分体现了分布式模块化的优势
首先,按照我这样的配置,你在以后添加微服务时,只需要在相应的微服务中写一个继承AccountRealm的实现类即可实现shiro的复用
就是我们上面配置的第4步AccountRealm,当然,如果不需要登录,那这步都可以省略
和单体应用的配置量相同,仅仅包的结构复杂一点不好理解,但是大大增加项目的弹性

在这里插入图片描述

二、登陆接口

1、实体校验

登陆的时候肯定需要用到表单
这时我们需要进行一些逻辑判断,比如用户名不能为空

在这里插入图片描述

package com.yzpnb.service_blog.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import java.util.Date;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;

/**
 * <p>
 * 
 * </p>
 *
 * @author testjava
 * @since 2020-07-20
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@ApiModel(value="MUser对象", description="")
public class MUser implements Serializable {

    private static final long serialVersionUID=1L;

    @ApiModelProperty(value = "用户id")
    @TableId(value = "id", type = IdType.ID_WORKER_STR)
    private String id;

    @NotBlank(message = "用户名不能为空")
    @ApiModelProperty(value = "用户名")
    private String username;

    @ApiModelProperty(value = "头像")
    private String avatar;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @ApiModelProperty(value = "邮箱")
    private String email;

    @NotBlank(message = "密码不能为空")
    @ApiModelProperty(value = "密码")
    private String password;

    @ApiModelProperty(value = "状态")
    private Integer status;

    @ApiModelProperty(value = "创建时间")
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreated;

    @ApiModelProperty(value = "更新时间")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;

    @ApiModelProperty(value = "逻辑删除1删除,0未删除")
    private Boolean isDelete;


}
controller接口
我们通过@Validated注解监听
如果不符合就报MethodArgumentNotValidException
@RequestBody注解用来将前端传来的json封装到实体类

在这里插入图片描述

@PostMapping("login")
    public Result login(@Validated @RequestBody MUser user){
        return Result.ok().data("user",user);
    }
处理相应异常
刚刚的shiro异常处理中我就把这个异常放进去了,放在service_base中,感觉不符合,所以和shiro异常放一起吧

在这里插入图片描述
在这里插入图片描述

2、登录实体类

对于这些只需要一两个属性的,就定义一个专属的实体类就好了

在这里插入图片描述

package com.yzpnb.service_blog.entity.md;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
public class LoginMD implements Serializable {
    @NotBlank(message = "用户名不能为空")
    @ApiModelProperty(value = "用户名")
    private String username;

    @NotBlank(message = "密码不能为空")
    @ApiModelProperty(value = "密码")
    private String password;
}

3、添加数据库测试数据

因为只有登录,还没有注册,所以先存一条数据
因为我们使用md5加密,所以对比时,要保证数据库中存储的都是加密后的密码
96e79218965eb72c92a549dd5a330112
上面这一串表示111111的md5加密格式

在这里插入图片描述

4、登录Controller

在这里插入图片描述

package com.yzpnb.service_blog.controller;

import cn.hutool.core.map.MapUtil;
import cn.hutool.crypto.SecureUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.yzpnb.common_utils.Result;
import com.yzpnb.service_blog.entity.MUser;
import com.yzpnb.service_blog.entity.md.LoginMD;
import com.yzpnb.service_blog.service.MUserService;
import com.yzpnb.shiro.utils.JwtUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.util.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletResponse;

@RestController
@RequestMapping("/service_blog/account")
public class AccountController {

    @Autowired
    JwtUtils jwtUtils;
    @Autowired
    private MUserService userService;
    /**
     *
     * @param loginMD   登陆实体对象
     * @param response 我们做登陆,需要将jwt放在header中,这就需要HttpServletResponse
     * @return
     */
    @CrossOrigin
    @PostMapping("/login")
    public Result login(@Validated @RequestBody LoginMD loginMD, HttpServletResponse response) {
        MUser user = userService.getOne(new QueryWrapper<MUser>().eq("username", loginMD.getUsername()));
        Assert.notNull(user, "用户不存在");
        if(!user.getPassword().equals(SecureUtil.md5(loginMD.getPassword()))) {
            return Result.error().message("密码错误!");
        }
        String jwt = jwtUtils.generateToken(user.getId());
        System.out.println("11");
        response.setHeader("Authorization", jwt);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
        // 用户可以另一个接口
        return Result.ok().data("loginMD",MapUtil.builder()
                .put("id", user.getId())
                .put("username", user.getUsername())
                .put("avatar", user.getAvatar())
                .put("email", user.getEmail())
                .map()
        );
    }

    // 退出
    @GetMapping("/logout")
    //@RequiresAuthentication//必须认证通过才能退出
    public Result logout() {
        SecurityUtils.getSubject().logout();
        return Result.ok();
    }
}

5、测试

在这里插入图片描述

在这里插入图片描述

解决办法,网上有很多,但是这里有一种最直接的方式

在这里插入图片描述在这里插入图片描述在这里插入图片描述

三、功能接口开发

1、注册用户

实体类

在这里插入图片描述

package com.yzpnb.service_blog.entity.md;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
public class SingInMD implements Serializable {
    @NotBlank(message = "用户名不能为空")
    @ApiModelProperty(value = "用户名")
    private String username;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    @ApiModelProperty(value = "邮箱")
    private String email;

    @NotBlank(message = "密码不能为空")
    @ApiModelProperty(value = "密码")
    private String password;
}

controller,我这里为了节省篇幅,就不把逻辑代码放在service中了,全写在controller中,维护会增加难度,所以你应该将逻辑放在service中

在这里插入图片描述

@PostMapping("signIn")
    public Result sigIn(@Validated @RequestBody SingInMD singInMD){
        /**
         * 判断
         */
        MUser username = userService.getOne(new QueryWrapper<MUser>().eq("username", singInMD.getUsername()));
        if(!StringUtils.isEmpty(username)){
            throw new CustomExceptionHandler(20001,"用户名已注册");
        }else{
            username = userService.getOne(new QueryWrapper<MUser>().eq("email", singInMD.getEmail()));
            if(!StringUtils.isEmpty(username)){
                throw new CustomExceptionHandler(20001,"该邮箱已注册");
            }
        }

        MUser user=new MUser();

        BeanUtils.copyProperties(singInMD,user);//将singInMD中的所有相符属性,全部拷贝到user对象
        user.setPassword(SecureUtil.md5(singInMD.getPassword()));//加密密码
        user.setStatus(1);//初始状态为1,表示正常
        user.setIsDelete(false);//初始逻辑删除为false就是0表示没有删除
        boolean save = userService.save(user);
        if(save){
            return Result.ok();
        }
        throw new CustomExceptionHandler(20001,"注册失败");
    }
测试

在这里插入图片描述
在这里插入图片描述

可以看到id由mybatis plus自动生成了,自动填充了创建时间和修改时间
对应用户的先开发到这,日后在完善

2、博客的增删改查

添加博客实体类

在这里插入图片描述

package com.yzpnb.service_blog.entity.md;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import javax.validation.constraints.NotBlank;
import java.io.Serializable;

@Data
public class InsertBlog implements Serializable {

    @ApiModelProperty(value = "用户id")
    private String userId;

    @NotBlank(message = "标题不能为空")
    @ApiModelProperty(value = "博客标题")
    private String title;

    @ApiModelProperty(value = "简介内容")
    private String description;

    @NotBlank(message = "内容不能为空")
    @ApiModelProperty(value = "内容")
    private String content;

    @ApiModelProperty(value = "发布状态")
    private Integer status;
}
controller接口
package com.yzpnb.service_blog.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yzpnb.common_utils.Result;
import com.yzpnb.service_base.handler.CustomExceptionHandler;
import com.yzpnb.service_blog.entity.MBlog;
import com.yzpnb.service_blog.entity.md.InsertBlog;
import com.yzpnb.service_blog.service.MBlogService;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.ibatis.annotations.Delete;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.apache.shiro.util.Assert;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author testjava
 * @since 2020-07-20
 */
@RestController
@RequestMapping("/service_blog/m-blog")
public class MBlogController {
    @Autowired
    private MBlogService blogService;

    //2、分页查询讲师,其中所有@Api开头的注解都是swagger提供的,解释注解
    @ApiOperation(value="分页查询所有博客")
    @GetMapping("limitSelect/{current}/{size}")
    public Result limitSelect(@ApiParam(name="current",value ="当前页",required=true)
                              @PathVariable("current") Long current,
                              @ApiParam(name="size",value="每页记录数")
                              @PathVariable("size") Long size){
        //1、创建page对象
        Page<MBlog> page=new Page<>(current,size);
        //2、通过接口中方法实现分页
        return Result.ok().data("limitBlog",blogService.page(page));
    }
    @ApiOperation("根据id获取博客")
    @GetMapping("blogById/{id}")
    public Result selectById(@ApiParam(name="id",value = "博客id")
                             @PathVariable(name = "id") Long id) {
        MBlog blog = blogService.getById(id);
        Assert.notNull(blog, "该博客已删除!");
        return Result.ok().data("blog",blog);
    }

//    @RequiresAuthentication//必须认证通过
    @ApiOperation(value = "根据id修改博客")
    @PutMapping("blogById")
    public Result updataById(   @ApiParam(name = "blog",value = "博客信息")
                                @Validated @RequestBody MBlog blog) {
        boolean b = blogService.updateById(blog);
        if(b){
            return Result.ok().message("修改成功");
        }
        throw new CustomExceptionHandler(20001,"修改失败");
    }
//    @RequiresAuthentication//必须认证通过
    @ApiOperation("根据id删除博客")
    @DeleteMapping("blogById/{id}")
    public Result deleteById(   @ApiParam(name = "id",value = "博客id")
                                @PathVariable String id) {
        boolean b = blogService.removeById(id);
        if(b){
            return Result.ok().message("删除成功");
        }
        throw new CustomExceptionHandler(20001,"删除失败");
    }

//    @RequiresAuthentication//必须认证通过
    @ApiOperation("添加博客")
    @PostMapping("insertBlog")
    public Result insertBlog(   @ApiParam(name = "blog",value = "博客信息")
                                @Validated @RequestBody InsertBlog blog) {
        MBlog mBlog=new MBlog();
        BeanUtils.copyProperties(blog,mBlog);//把blog的属性给mBlog
        mBlog.setStatus(0);//0表示未发布状态
        mBlog.setIsDelete(false);//表示未删除
        boolean b = blogService.save(mBlog);
        if(b){
            return Result.ok().message("保存成功");
        }
        throw new CustomExceptionHandler(20001,"保存失败");
    }
}

3、测试

保存博客

在这里插入图片描述在这里插入图片描述

查询博客

在这里插入图片描述在这里插入图片描述

修改博客

在这里插入图片描述在这里插入图片描述

删除博客

在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

到此后端的基本功能模块就开发完成了,接下来就可以进入到前端的开发了
因为这里分布式模块化做的比较完善,之后添加功能也十分方便

4、补充知识

数据库中存储的时间,没有办法直接使用,添加一个注解即可
@JsonFormat(pattern = “yyyy-MM-dd HH:mm”)//将获取的时间格式化

在这里插入图片描述

基于Spring Boot + Vue 前后端分离个人博客网站设计 个人博客网站摘要一、绪论1.1 课题背景、目的、意义1.2 国内外研究现状1.3 技术介绍1.3.1 Spring Boot1.3.2 MyBatisPlus1.3.3 Shiro1.3.4 Redis1.3.5 Jwt1.3.6 Vue1.3.7 Element-ui1.3.8 Axios1.3.9 前后端分离二、可行性分析2.1 技术可行性分析2.2 时间和资源可行性三、需求分析3.1 用户分析3.2 功能需求分析3.3 流程分析四、结构设计4.1 实体e-r图4.2 数据库设计五、详细设计六、系统测试 阅读详情

相关推荐

springboot+shiro前后端分离实现!!

本文章参考两位大佬写的博客完成的 https://blog.youkuaiyun.com/weixin_42236404/article/details/89319359 https://blog.youkuaiyun.com/qq_42109746/article/details/97102231 1、前言 1.1、唠嗑部分 学习shiro之前,建议先学习springsecurity,将两个框架进行对比的方式学习,会有更加深的印象。我之前写过一篇关于springsecurity的博客,你可以参考参考,个人感觉挺详细.

weixin_42375707的博客 8175

Springboot + shiro + jwt + vue 实现基本的认证,鉴权

一,前言 最近在思考如何实现一个权限管理系统。 在查阅一些资料和开源项目后,自己模仿着写了个demo 二,概念 1,RBAC模型 RBAC(Role-Based Access Control)——基于角色的访问控制 RBAC是一套成熟的权限模型。在传统权限模型中,我们直接把权限赋予用户。而在RBAC中,增加了“角色”的概念,我们首先把权限赋予角色,再把角色赋予用户。这样,由于增加了角色,授权会更加灵活方便。在RBAC中,根据权限的复杂程度,又可分为RBAC0、RBAC1、RBAC2、RBAC3。其中,R

saienenen的博客 2475

基于 Spring Boot 和 Vue 开发前后端分离快速开发平台源码分享

一个前后端分离快速开发平台,是基于 Spring Boot 和 Vue 开发,整合 Flowable工作流、Shiro、Redis等,来帮助中小型企业及个人实现敏捷化的应用交付和运营管理,并提供代码生成器、通用前端等业务组件,来帮助开发者聚焦于业务,加速中小型企业数字化转型。 最新源码获取方式:关注头条转发文章之后私信回复【源码】即可免费获取到! 开发平台 采用前后端分离的模式,前端开源框架:quickd-ui (基于 Vue、Element-UI) 后端采用Springboot+Myba...

代码界的扛把子 6775

vue+springboot+shiro+jwt实现登录

公司开发的系统原先的用户信息是基于shiro session 进行管理,但是session不适用于app端,并且服务器重启后需要重新登录。需要改造将shiro和jwt进行整合,实现通过token登录。 1.导入依赖 <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId>

m0_54861649的博客 7794

Spring Boot + Vue + Shiro 整合实战:构建企业级前后端分离应用

前后端分离架构是现代Web应用开发的主流模式,其核心在于通过RESTful API实现前后端职责解耦,后端专注于数据与业务逻辑,前端负责交互与展示。这种模式基于无状态通信原理,能有效支持并行开发与技术栈独立演进,其技术价值在于提升开发效率、增强系统可维护性与可扩展性。在实际工程实践中,该架构广泛应用于中后台管理系统、SaaS平台等需要清晰权限控制与高效协作的场景。本文聚焦于整合Spring Boot、Vue.js与Apache Shiro这一经典技术栈,深入探讨如何实现基于JWT的无状态认证与授权,并解决跨

weixin_33912453的博客 496

Java大牛带你4小时开发一个SpringBoot+vue前后端分离博客项目

第五步:定义jwt的过滤器JwtFilter。这个过滤器是我们的重点,这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。我们需要重写几个方法:createToken:实现登录,我们需要生成我们自定义支持的JwtTokenonAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;

m0_60721695的博客 827

二十三、SpringBoot + Jwt + Vue 权限管理系统 (3)

一步一步来,不要急躁哦!! 当时显示: 这个路由问题我提供两个解决方案:这里我们使用第二种解决方案,这类简单点,后续我们再开发页面的时候就不需要去改动路由,可以动态绑定。综上,我们把加载菜单数据这个动作放在router.js中。Router有个前缀拦截,就是在路由到页面之前我们可以做一些判断或者加载数据。src/router/index.js 这样我们才能绑定添加到路由。所以我会修改mock中的nav的数据成这样: 注意这两个的引入哦! 最后效果如下: 上面做完之后,总还觉得少

Daniel的博客 1464

后台管理系统Springboot+Shiro+JWT+Vue

项目开发中,后台系统尤为重要,最近就想搞一套能拿来直接使用的后台管理系统。 框架: Shiro,Shiro作为安全框架,实现登录、登出、身份验证、授权、会话管理。 JWT,Json Web Token负责访问接口时的验证。 Vue,前端框架,因为本身不是前端开发,所以Clone了某位高人在github上的模版。写的非常好,简单易用。https://github.com/PanJiaChen/vue-admin-template 前端代码就是照葫芦画瓢,不过很多地方也googl

libin372767477的专栏 1490

Spring Boot多模块+ Shiro + Vue:前后端分离登陆整合,权限认证

前言 本文主要使用spring boot + shiro + vue来实现前后端分离的认证登陆和权限管理,适合和我一样刚开始接触前后端完全分离项目的同学,但是你必须自己搭建过前端项目和后端项目,本文主要是介绍他们之间的互通,如果不知道这么搭建前端项目的同学可以先找别的blog看一下。 自己摸索了一下,可能会有一些问题,也有可能有更好的实现方式,但这个demo主要是用来记录自己搭建系统,独立完成前后端分离项目的过程,并且作为自己的毕业设计框架。所以有问题的话欢迎提出,共同交流。源码在github上,有需要的.

小小陈的博客 8599

Spring Boot整合Shiro+JWT+Redis实现token自动刷新与无感续期

无状态认证是现代分布式系统的基础能力,JWT作为轻量级身份凭证被广泛使用,但token过期导致的频繁登录和安全隐患始终是工程实践中的痛点。Shiro负责请求拦截与权限过滤,Redis承担在线状态与会话续期管理,三者结合可构建一套完整的自动刷新机制。本文从认证原理出发,解析如何通过短有效期JWT、滑动续期阈值与Redis原子操作,在用户活跃时静默换发新token,同时解决登出黑名单、并发刷新覆盖和旧token失效等关键问题。该方案适用于后端管理系统、微服务网关及高安全要求业务场景,既能避免用户被无故踢下线,又

weixin_34235457的博客 408

从数据手册到代码:一步步解读SC7A20寄存器配置与加速度值换算原理

本文详细解析了SC7A20三轴加速度传感器的寄存器配置与加速度值换算原理。从数据手册出发,逐步拆解关键寄存器设置,并推导原始ADC值到物理加速度的完整换算过程,帮助开发者深入理解传感器底层工作原理,提升嵌入式开发效率。

weixin_29179311的博客 366

spring boot+vue个人博客一:(后端)搭建spring boot脚手架

文章目录一、环境搭建1、数据库2、创建工程并引入依赖3、application.yaml4、启动类5、mybatis plus 分页插件6、代码生成器1、相关依赖2、配置(根据注释修改即可)3、编写controller并测试7、统一返回结果1、创建公共模块2、测试8、整合swagger测试接口 一、环境搭建 1、数据库 CREATE TABLE `m_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(64) DEFAUL

殷志鹏的博客 8723

badusb详细制作教程(两种方式实现远控)

badusb是一种几乎可以突破任何杀毒软件和防火墙的设备,堪称渗透测试利器 文章目录前言一、什么是badusb?二,badusb的优点三,badusb是如何攻击的?badusb原理HID攻击漏洞产生原因三、准备步骤1.设备2,编辑器的安装与设置四,编写代码(重点)第一种: SEToolkit + badusb实现PowerShell注入攻击第二种:Cobalt Strike + badusb 实现PowerShell注入攻击五,烧录六,badusb有多强大攻击场景总结 BadUSB是《黑客军团》中

Ghost_system的博客 1万+

SpringBoot+Vue博客系统---后端接口开发

Java后端接口开发 从零开始搭建一个项目骨架,最好选择合适,熟悉的技术,并且在未来易拓展,适合微服务化体系等。所以一般以Springboot作为我们的框架基础,这是离不开的了。 然后数据层,我们常用的是Mybatis,易上手,方便维护。但是单表操作比较困难,特别是添加字段或减少字段的时候,比较繁琐,所以这里我推荐使用Mybatis Plus CRUD 操作,从而节省大量时间。 作为一个项目骨架,权限也是我们不能忽略的,Shiro配置简单,使用也简单,所以使用Shiro作为我们的的权限。 考虑到项目可能需要

Frank---7的博客 7万+

Qt5+OSG实战:如何用osgQOpenGLWidget在窗口中加载3D模型(附完整代码)

本文详细解析了如何在Qt5中使用osgQOpenGLWidget加载和渲染3D模型,涵盖环境配置、核心架构、性能优化及高级技巧。通过实战案例和完整代码,帮助开发者实现工业级3D模型渲染,提升Qt与OSG的整合效率。

weixin_42741930的博客 263

STM32F4实战:手把手教你实现USB OTG双模式切换(Host/Slave)

本文详细介绍了如何在STM32F4上实现USB OTG双模式切换(Host/Slave),包括硬件架构解析、软件实现机制、主机模式下的U盘读写以及从机模式下的自定义设备开发。通过实战案例和优化技巧,帮助开发者快速掌握USB双角色功能,提升嵌入式系统开发的灵活性和效率。

u9v0w的博客 730

spring boot+vue个人博客五:完成登陆注册页面,nginx网关对接后端

文章目录一、页面整合1、下载bootstrap(去官网下载即可)2、编写单独样式表3、引入一个图标库4、配置nginx,编写异步接口5、api接口6、登陆页面开发二、逻辑测试与完善1、测试当前接口2、集中化管理数据3、方法重构4、测试三、获取登陆信息完善页面1、编写头部组件2、测试 一、页面整合 1、下载bootstrap(去官网下载即可) 2、编写单独样式表 /* Please ❤ this if you like it! */ @import url('https://fonts.google

殷志鹏的博客 1141

从零构建HFSS连接器仿真模型:以SMP接口的精准复现为例

本文详细介绍了如何从零构建HFSS连接器仿真模型,以SMP接口的精准复现为例。通过获取官方模型、导入技巧、信号路径提取和介质结构复现等步骤,帮助工程师避免常见错误,提升高频电路仿真的准确性。文章特别强调了模型精度对仿真结果的影响,并提供了实用的调试和管理建议。

weixin_29041767的博客 429

springboot+jwt+shiro+vue+elementUI+axios+redis+mysql完成一个前后端分离的博客项目

六万字长文带你入手springboot+jwt+shiro+vue+elementUI+axios+redis+mysql完成一个前后端分离的博客项目

Hanson的博客 1712

化工热力学软件:Aspen Plus二次开发_(9).过程控制与动态模拟v1

Aspen Plus 提供了丰富的过程控制工具和方法,可以用于静态和动态过程的模拟和控制策略的开发。通过控制回路设计、动态模拟、优化工具和数据交换接口,用户可以设计和优化各种复杂的控制策略,提高化工生产的稳定性和效率。此外,结合外部软件(如MATLAB、Simulink、LabVIEW等),可以实现更高级的控制策略,如自适应控制和模型预测控制,进一步提升过程控制的精度和灵活性。希望本文的内容能帮助用户更好地理解和应用Aspen Plus中的过程控制功能。

kkchenjj的博客 405

D19: OpenClaw 安全性配置指南

在 OpenClaw 的 30 天探索旅程中,我们已经探讨了邮件自动化(D18)。今天进入安全专题,我将分享一个至关重要的话题:OpenClaw 安全性配置指南。 随着 OpenClaw 深入工作流核心,它掌握了越来越多的敏感信息: API Keys(GitHub、飞书、Notion、Jira...) 企业数据(代码库、文档、聊天记录) 个人数据(日历、邮件、任务) 系统权限(文件读写、命令执行、网络访问) 安全不是可选项,而是必选项。一次配置失误可能导致: API Key 泄露 → 账号被盗用权限

刘一说的IT专栏 203

【SpringBoot框架篇】16.security整合jwt实现对前后端分离的项目进行权限认证

使用Spring Security+JWT实现对前后端分离的项目进行权限认证

皓亮君的博客 5655

《YOLOv12魔术师专栏》专栏介绍 & 专栏目录

《YOLOv12魔术师专栏》专栏介绍 & 专栏目录

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 5058

springboot+vue前后端分离整合shiro+jwt

0.实现逻辑 1.导入依赖 导入shiro-redis的starter包:还有jwt的工具包 <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis-spring-boot-starter</artifactId> <version>3.2.1</version> </dependency> <!-

weixin_45803046的博客 1157

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

老版QQ原生表情gif动态

这是老版的QQ表情,不过全是动态的的,Windows照片查看器是不会动的,可以右击用浏览器打开查看,再说一遍是老版的,只是分享给宁愿用老版也要动态的人。

spring boot+vue个人博客六:博客页面,观看博客,博客编辑功能

文章目录一、博客展示1、api接口2、代码二、博客编辑1、整合mavon-editor编辑器2、博客编辑页面 一、博客展示 1、api接口 import request from '@/utils/request.js' export default { limitSelect(current,size) { return request({ url: `/service_blog/m-blog/limitSelect/${current}/${size}`, met

殷志鹏的博客 1029

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

springBoot+shiro+jwt+vue(element-ui)完成用户登录接口(踩坑无数)

记录一下:springBoot+shiro+jwt+vue(element-ui)完成用户登录接口(踩坑无数)

ITmime的博客 1394

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

上一篇: spring boot+vue个人博客一:(后端)搭建spring boot脚手架
下一篇: spring boot+vue个人博客三:搭建前端vue整合element ui脚手架
ydenergy_殷志鹏
博客等级 码龄7年 5885粉丝 543原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ydenergy_殷志鹏

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值