基于SpringBoot+Vue开发的前后端分离博客项目一一后端开发


前言

为了巩固SpringBoot、Redis、Vue、Shiro框架整合的学习,拿这个小demo来练练手


一、Java后端接口开发

在这里插入图片描述

1. 新建SpringBoot 项目

1.1 开发技术栈:

  • mysql、druid
  • SpringBoot
  • maven
  • mybatis-Plus
  • Shiro、JWT
  • Redis

1.2 pom中jar包引入:

<dependencies>
        <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.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </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>
            <scope>test</scope>
        </dependency>

        <!--mp-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.2.0</version>
        </dependency>
        <!--mp代码生成器、模板引擎-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.2.0</version>
        </dependency>

        <!--redis_shiro-->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis-spring-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>
        <!-- jwt -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.1</version>
        </dependency>

        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.4.3</version>
        </dependency>

        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.22</version>
        </dependency>
        <!--log4j-->
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>

        <!--validation-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

    </dependencies>

1.3 配置文件:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/vueblog?useUnicode=true&characterEncoding=utf-8&userSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    initialsize: 5
    minIdle: 1
    maxActive: 20
    maxWait: 60000
    # 每隔多长时间进行空闲连接回收
    timeBetweenEvictionRunMillis: 60000
    # 每个连接的最小存活时间
    minEvictableIdleTimeMillis: 300000
    # 检验连接是否正常
    validationQuery: SELECT 1 FROM DUAL
    # 空闲时对连接进行检查
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 配置拦截统计的filters, 去掉后监控界面的sql无法统计
    filters: stat,wall,log4j

2. 整合mybatis Plus

2.1 引入pom的jar包

<!--mp-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.2.0</version>
</dependency>
<!--mp代码生成器、模板引擎-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.2.0</version>
</dependency>

2.2 配置分页插件、代码生成器

参考官方文档:
分页插件
代码生成器

2.3 配置文件

mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml
  configuration:
    map-underscore-to-camel-case: true

3. Restful风格结果封装

封装一个Result类,包括Integer类型的code、String类型的msg、Object类型的data,封装success和fail方法。
在这里插入图片描述


4. 整合Shiro+JWT,并会话共享

考虑到之后可能会做集群、负载均衡等,所以就需要开启会话共享,而shiro的缓存和会话信息,我们一般使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,同时也需要整合redis.

而因为我们做的是前后端分离项目的骨架,所以一般会采用token或者jwt作为跨域身份验证解决方案。所以整合shiro过程中,我们需要引入jwt的身份验证过程。

4.1 认证流程

在这里插入图片描述

4.2整合步骤

  • ShiroConfig [Shiro主配置类,主要配置了安全管理器、实体数据源、缓存等]
//这里只展示了核心代码
  //shiro权限数据和会话信息能够保存到redis中,实现会话共享
    @Bean
    public SessionManager sessionManager(RedisSessionDAO redisSessionDAO){
        DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO);
        return sessionManager;
    }

    @Bean(name = "securityManager")
    public SecurityManager securityManager(UserRealm userRealm,
                                           SessionManager sessionManager,
                                           RedisCacheManager redisCacheManager){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setCacheManager(redisCacheManager);
        securityManager.setSessionManager(sessionManager);
        securityManager.setRealm(userRealm);

        //关闭shiro自带的session
        DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
        evaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(evaluator);
        securityManager.setSubjectDAO(subjectDAO);
        return securityManager;
    }

    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
                                                         ShiroFilterChainDefinition shiroFilterChainDefinition){
        ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String,Filter> filterMap=new HashMap<>();
        filterMap.put("jwt",jwtFilter);
        shiroFilterFactoryBean.setFilters(filterMap);

        Map<String, String> filterChainMap = shiroFilterChainDefinition.getFilterChainMap();
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

  • UseRealm [用户实体数据源]
    UserRealm继承于AuthorizingRealm类,重写了doGetAuthorizationInfo(用于授权)、doGetAuthenticationInfo(用于认证)、supports(用于支持jwt的凭证校验)
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        JWTToken token=(JWTToken)authenticationToken;
        log.info("jwt-----------{}",token);

        String userId =jwtUtils.getClaimByToken((String) token.getPrincipal()).getSubject();
        User user = userService.getById(userId);

        if(user==null)
            throw new UnknownAccountException("用户不存在");
        if(user.getStatus()==-1)
            throw new LockedAccountException("账号被锁定");
        ProfileUser profileUser = new ProfileUser();
        BeanUtils.copyProperties(user,profileUser);
        log.info("profile---------{}",profileUser.toString());
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(profileUser, token.getCredentials(), this.getName());
        return simpleAuthenticationInfo;
    }

  • JWTToken [自定义Token]
    自定义token继承于AuthenticationToken,用于包装jwt
/**
 * 自定义JWTToken
 */
public class JWTToken implements AuthenticationToken {
    private String token;

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }

    public JWTToken(String token){
        this.token=token;
    }
}

  • JWTFilter [自定义拦截器,拦截所有请求进行jwt判断校验]
    继承于AuthenticatingFilter(或者是BasicHttpAuthenticationFilter),本次采用AuthenticatingFilter
//自定义token,然后交给shiro验证
    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt=request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt))
            return null;
        else
            return new JWTToken(jwt);
    }

    //拦截校验
    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");

        //请求头没有jwt,身份为游客,直接放行
        if(StringUtils.isEmpty(jwt))
            return true;
        //否则用户需要进行登录验证操作
        else{
            //验证jwt有效
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim==null || jwtUtils.isJwtExpire(claim.getExpiration()))
                throw new ExpiredCredentialsException("token已经过期,请重新登录");
        }
        //shiro登录验证
        return executeLogin(servletRequest,servletResponse);
    }

当JWTFilter拦截处理后,拥有jwt并且有效的用户会执行executeLogin方法进行登录,实质还是交给UserRealm进行处理,此时UserRealm中存储的就是封装的JWTToken数据。

  • ProfileUser [UserRealm登录认证通过后保存在Subject中的数据]
@Data
public class ProfileUser implements Serializable {
    private Integer id;
    private String username;
    private String avatar;
}

5. 异常处理

  • 程序中需要处理 一般的运行时异常、shiro处理异常、前端数据实体校验异常、数据处理结果Assert断言异常
  • 全局异常处理由@RestControllerAdvice指明,在每种异常处理前使用@ExceptionHandler指明异常类型,@ResponseStatus指定返回的状态码。
    //捕捉shiro异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public Result shiroHandler(ShiroException e){
        log.error("shiro认证异常---------");
        return Result.fail(401,e.getMessage());
    }

    //捕捉实体校验异常
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result methodHandler(MethodArgumentNotValidException e){
        log.error("实体数据异常--------");
        BindingResult bindingResult = e.getBindingResult();
        ObjectError error = bindingResult.getAllErrors().stream().findFirst().get();
        return Result.fail(error.getDefaultMessage());
    }

6. 实体校验

  • Java后端实体校验常用技术:JSR303、JSR-349、hibernate validation、spring validation等。其中JSR303是一种规范,JSR-349是它的升级版本,它们规范了一些校验注解,比如@Null、@NotNull、@Pattern,它们位于javax.validator.constraints包下,只提供规范但是不提供实现。
  • hibernate validation则提供相应的实现,并且提供了一些新的注解,比如@Email、@Length、@Range、@Size等,它们位于org.hibernate.validator.constraints包下。
  • spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,加快了web开发速度。

校验技术的使用:
由于引入的spring-boot-starter-web依赖的子依赖中包含了hibernate-validator、jackson-databind依赖,不需要额外引入

//校验示例
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

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

    /**
     * 用户头像
     */
    private String avatar;

    /**
     * 用户邮箱
     */
    @Email(message = "邮箱不符合规范")
    private String email;

    /**
     * 登录密码
     */
    @Pattern(regexp = "^[a-z A-Z 0-9]{6,}",message = "密码必须为字母或者数字的6位组合")
    private String password;
    }


//SpringMVC校验写法如下,bindingResult 是结果集,可能会包含异常
@Controller
public class FooController {

    @RequestMapping("/foo")
    public String foo(@Validated Foo foo <1>, BindingResult bindingResult <2>) {
        if(bindingResult.hasErrors()){
            for (FieldError fieldError : bindingResult.getFieldErrors()) {
                //...
            }
            return "fail";
        }
        return "success";
    }

}

校验注解总结:

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    


Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

7. 跨域问题

什么是跨域?
跨域就是指浏览器不能执行其它网站的脚本,它是由于浏览的同源策略造成的,是浏览器对JavaScript实施的安全限制。

首先狭义的同源就是指:域名、协议、端口均相同:
同源策略限制了Cookie、LocalStorage无法读取;DOM和JS对象无法获取;Ajax请求发送不出去。

http://www.yyy.cn/index.html 调用 http://www.xxxyyy.cn/server.php 非跨域

http://**www.xxxyyy.cn**/index.html 调用  http://**www.xxx.cn**/server.php  跨域,主域不同

http://**abc**.xxxyyy.cn/index.html 调用  http://**def**.xxx.cn/server.php  跨域,子域名不同

http://www.xxx.cn:**8080**/index.html 调用  http://www.xxx.cn/server.php  跨域,端口不同

**https**://www.xxx.cn/index.html 调用  **http**://www.xxx.cn/server.php  跨域,协议不同

如何解决跨域问题?

  • 跨域资源共享CORS 这是目前的主流方案,全称是(Cross-origin resource sharing),它允许浏览器向跨源服务器发送请求,克服了Ajax只能同源使用的限制。

  • 整个CORS过程不需要用户的参与,都是浏览器自动完成。浏览器一旦发现Ajax请求跨域,就会自动添加一些头信息,甚至是进行一次附加请求。因此实现CORS的关键是服务器,对服务器动手脚使得浏览器能够跨域。

  • 分为两种请求:简单请求和复杂请求

(1) 简单请求 [请求方式为HEAD、POST或者GET]
浏览器发送CORS请求,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。Origin字段表明此次请求来源于哪个源(协议+域名+端口)。

GET /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0

服务器根据这个值确定是否同意这次请求,如果Origin不在许可范围内,服务器会返回一个正常的HTTP回应,状态码可能为200。浏览器此时发现返回的响应头中不包含Access-Control-Allow-Origin,就抛出一个错误被XMLHttpRequest回调函数捕获。

如果Origin在许可范围内,就会返回如下字段:

  #该字段必须,表示请求的来源
  Access-Control-Allow-Origin: http://api.bob.com  
  
  #可选,表示是否允许发送Cookie,如果要发送,还必须在AJAX请求中打开withCredential属性
  Access-Control-Allow-Credentials: true
  
  #可选,用于拿到其它额外的字段
  Access-Control-Expose-Headers: FooBar
   
  Content-Type: text/html; charset=utf-8

(2)非简单请求 [请求方式为PUT、DELETE或者Content-Type是applicaiton/json]
非简单会在发送请求前进行一次预检请求,用于询问服务器当前域名是否在许可名单之中,以及可以使用哪些HTTP动词和头信息字段。
预检请求:

OPTIONS /cors HTTP/1.1   #请求方式为OPTIONS

Origin: http://api.bob.com    #指定请求来源

Access-Control-Request-Method: PUT  #必须字段,用于指出CORS会用到哪些HTTP方法

Access-Control-Request-Headers: X-Custom-Header  #必须,用于指出CORS会额外发送的头信息字段,上例为x-Custom-Header

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0...

预检响应:

HTTP/1.1 200 OK

Date: Mon, 01 Dec 2008 01:15:39 GMT

Server: Apache/2.0.61 (Unix)

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Methods: GET, POST, PUT   #必需字段,服务器支持的所有请求方式

Access-Control-Allow-Headers: X-Custom-Header  #必须字段,服务器支持所有头信息字段

Content-Type: text/html; charset=utf-8

当服务器通过了预检请求,接下来每次浏览器的CORS请求都和简单请求一样,会包含Origin头信息字段。服务器的回应也会包含Access-Control-Allow-Origin字段。


好像扯远了。。。。回到正题!

正题:
在JWTFilter中需要重写preHandle方法来操纵服务器通过浏览器的跨域请求。

  //对跨域提供支持
    @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),这里直接返回正常状态
        if(httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request,response);
    }

另外还需要配置一下全局跨域的处理:

/**
 * 解决跨域问题
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
         registry.addMapping("/**")
                 .allowedOrigins("*")
                 .allowedMethods("GET","POST","PUT","HEAD","OPTIONS","DELETE")
                 .allowCredentials(true)
                 .allowedHeaders("*");
    }
}

8. 登录接口开发

登录的逻辑:用户登录就是一个刷新jwt的过程,接收用户的用户名和密码然后生成相应的jwt,再将该jwt放在header上返回给前端。

    @PostMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
        QueryWrapper<User> wrapper = new QueryWrapper<>();
        wrapper.eq("username",loginDto.getUsername());
        //判断用户名和密码
        User one = userService.getOne(wrapper);
        Assert.notNull(one,"用户不存在!");

        if(!one.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
            return Result.fail("密码错误!");
        }
        //生成jwt放入响应头,以后每次请求都会携带jwt
        String jwt = jwtUtils.generateToken(one.getId().toString());
        response.setHeader("Authorization",jwt);
        //在涉及跨域请求时,response中大部分header需要源服务端同意才能拿到,所以需要在response中增加一个如下header
        response.setHeader("Access-Control-Expose-Headers","Authorization");
        return Result.success("登录成功", MapUtil.builder()
                                    .put("id",one.getId())
                                    .put("username",one.getUsername())
                                    .put("avatar",one.getAvatar())
                                    .put("email",one.getEmail())
                                    .put("id",one.getId())
                                    .put("id",one.getId()));
    }

9. 博客接口开发

简单的增删改查,如果需要用户权限,在后端的接受方法上加上@RequirePermissions、@RequireRoles、@RequireAuthentication等注解。
具体看源码哈~


参考文章,感谢以下所有作者:
项目原作者:MarkerHub

项目视频地址: https://www.bilibili.com/video/BV1PQ4y1P7hZ

项目文档地址:https://juejin.im/post/6844903823966732302

学习过程:

shiro整合Springboot : https://www.jianshu.com/p/ef0a82d471d2

SpringBoot整合Jwt: https://www.jianshu.com/p/3c51832f1051

JWT构造详解:https://www.jianshu.com/p/1ebfc1d78928

什么是跨域,如何实现: https://www.jianshu.com/p/f049ac7e2220

https://blog.csdn.net/swl1993831/article/details/90905040

redis学习: https://blog.csdn.net/xgangzai/article/details/82661940

@Validate注解: https://blog.csdn.net/u013815546/article/details/77248003/

java8 stream(): https://blog.csdn.net/m0_37556444/article/details/84975355

@Bean注解: https://www.cnblogs.com/cxuanBlog/p/11179439.html

@Autowried注解: https://blog.csdn.net/Fire_Sky_Ho/article/details/83214513?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param

shiro报错 UnavailableSecurityManagerException :https://www.cnblogs.com/ginponson/p/6217057.html


总结

顺手给个star吧 ❤
代码地址: https://github.com/iStitches/vueBlog
好好学习,坚持下去!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值