blog前后端分离项目01:Java后端接口开发

前言

1、项目背景

学习完SpringBoot、SSM框架、VUE框架,一致苦于没有项目去结合起来。通过这个博客练习项目,锻炼自己,巩固学习。此项目并非原创,是在b站中搜索到的,来自`MarkerHub UP主`,[【实战】基于SpringBoot+Vue开发的前后端分离博客项目完整教学](https://www.bilibili.com/video/BV1PQ4y1P7hZ?p=14),学完此项目,后续可能够会增加自己的一些内容。

Java后端接口开发

1、前言

技术栈:

  • SpringBoot 2.4.3
  • mybatis plus
  • shiro
  • lombok
  • redis
  • hibernate validatior
  • jwt

2、项目搭建

开发工具及环境:

  • idea
  • mysql
  • jdk 8
  • maven3.3.9

idea创建工程,工程名ergoublog-api,选上 热加载工具,Lombok,web,mysql驱动,其他的需要的后续在加

新建立好的项目结构如下,springboot版本2.4.3

pom的jar包引入如下

<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>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
  • devtools:项目的热加载重启插件
  • lombok:简化代码的工具

3、整合mybatis plus

整合mybatis plus 目的让我们快速进行单表的CRUD操作,详情见官网:https://mp.baomidou.com/guide/install.html#release

1、导入jar

pom中导入mybatis plus的jar包,因为后面会涉及到代码生成,所以我们还需要导入页面模板引擎,这里我们使用 Freemarker

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

2、再application.yml文件中配置mybatis plus

# DataSource Config
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3308/vueblog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: root
mybatis-plus:
  mapper-locations: classpath*:/mapper/**Mapper.xml

上面除了配置数据库的信息,还配置了myabtis plus的mapper的xml文件的扫描路径,这一步不要忘记了。

3、开启mapper接口扫描,添加分页插件

  • com.sen.config.MybatisPlusConfig
@Configuration
@EnableTransactionManagement
@MapperScan("com.sen.mapper")
public class MybatisPlusConfig {
    // 旧版
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        // paginationInterceptor.setOverflow(false);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        // paginationInterceptor.setLimit(500);
        // 开启 count 的 join 优化,只针对部分 left join
        paginationInterceptor.setCountSqlParser(new JsqlParserCountOptimize(true));
        return paginationInterceptor;
    }
}

4、数据库建表

CREATE DATABASE `vueblog`;
USE `vueblog`;

CREATE TABLE `m_user` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `username` VARCHAR(64) DEFAULT NULL,
  `avatar` VARCHAR(255) DEFAULT NULL,
  `email` VARCHAR(64) DEFAULT NULL,
  `password` VARCHAR(64) DEFAULT NULL,
  `status` INT(5) NOT NULL,
  `created` DATETIME DEFAULT NULL,
  `last_login` DATETIME DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8;
CREATE TABLE `m_blog` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT(20) NOT NULL,
  `title` VARCHAR(255) NOT NULL,
  `description` VARCHAR(255) NOT NULL,
  `content` LONGTEXT,
  `created` DATETIME NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  `status` TINYINT(4) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4;

注:

如果【Mysql 建表时报错 invalid ON UPDATE clause for 'create_date' column】

这个错误是由MySQL版本问题导致的,建议换mysql8.0 ,官方下载地址:https://downloads.mysql.com/archives/community/

但是我想mysql5.5和mysql8.0共存,参考文章:https://blog.csdn.net/zemuerqi/article/details/107329378

参考文章https://juejin.cn/post/6844903823966732302#heading-3

4、代码生成

得到

简洁!方便!经过上面的步骤,基本上我们已经把mybatis plus框架集成到项目中了。

在UserController中写个测试:

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserService userService;
    
    @GetMapping("/{id}")
    public Object test(@PathVariable("id") Long id){
        return  userService.getById(id);
    }
}

访问:http://localhost:8080/user/1 获得结果如下,整合成功!

4、统一结果封装

这里我们用一个Result的类,这个用于我们的异步统一返回的结果封装。一般来说,结果里面有几个要素必要的

  • 是否成功,可用code表示,(如300表示成功,400表示异常)
  • 结果消息
  • 结果数据

所以可得到封装如下

  • com.sen.common.lang.Result

    @Data
    public class Result implements Serializable {
        private String code;
        private String msg;
        private Object data;
    
        public static Result succ(Object data) {
            return succ("操作成功",data);
        }
        public static Result succ(String mess, Object data) {
            Result m = new Result();
            m.setCode("0");
            m.setData(data);
            m.setMsg(mess);
            return m;
        }
    
        public static Result succ(String mess) {
            return fail(mess,null);
        }
        
        public static Result fail(String mess, Object data) {
            Result m = new Result();
            m.setCode("-1");
            m.setData(null);
            m.setMsg(mess);
            return m;
        }
    }
    

5、整合shiro+jwt,并会话共享

考虑到后面可能需要做集群、负载均衡等,所以就需要会话共享,而shiro的缓存和会话信息,我们一般考虑使用redis来存储这些数据,所以,我们不仅仅需要整合shiro,配置简单,这里也推荐大家使用

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

登录逻辑:

用户访问api:

参考文档:https://github.com/alexxiyang/shiro-redis/blob/master/docs/README.md#spring-boot-starter

1、导入shiro-redis的starter的jar包,还有jwt的工具包,以及为了简化开发,我引入hutool(https://hutool.cn/docs/#/

<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>

2、编写配置

ShiroConfig

  • com.sen.config.ShiroConfig

    //shiro启用注解拦截器
    @Configuration
    public class ShiroConfig {
    
        @Autowired
        JwtFilter jwtFilter;
    
        @Bean
        public SessionManager sessionManager(RedisSessionDAO redisSessionDAO) {
            DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    
            // inject redisSessionDAO
            sessionManager.setSessionDAO(redisSessionDAO);
    
            // other stuff...
    
            return sessionManager;
        }
    
        @Bean
            public SessionsSecurityManager securityManager(AccountRealm accountRealm,
                                                           SessionManager sessionManager,
                                                           RedisCacheManager redisCacheManager) {
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(accountRealm);
    
            //inject sessionManager
            securityManager.setSessionManager(sessionManager);
    
            // inject redisCacheManager
            securityManager.setCacheManager(redisCacheManager);
    
            // other stuff...
    
            return securityManager;
        }
    
        @Bean
        public ShiroFilterChainDefinition shiroFilterChainDefinition() {
            DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
            Map<String, String> filterMap = new LinkedHashMap<>();
            //所有链接经过jwt
            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;
        }    
    }
    

上面ShiroConfig,我们主要做了几件事情:

  1. 引入RedisSessionDAO和RedisCacheManager,为了解决shiro的权限数据和会话信息能保存到redis中,实现会话共享。
  2. 重写了SessionManager和DefaultWebSecurityManager,同时在DefaultWebSecurityManager中为了关闭shiro自带的session方式,我们需要设置为false,这样用户就不再能通过session方式登录shiro。后面将采用jwt凭证登录。
  3. 在ShiroFilterChainDefinition中,我们不再通过编码形式拦截Controller访问路径,而是所有的路由都需要经过JwtFilter这个过滤器,然后判断请求头中是否含有jwt的信息,有就登录,没有就跳过。跳过之后,有Controller中的shiro注解进行再次拦截,比如@RequiresAuthentication,这样控制权限访问。

那么,接下来,我们聊聊ShiroConfig中出现的AccountRealm,还有JwtFilter。

AccountRealm

AccountRealm是shiro进行登录或者权限校验的逻辑所在,算是核心了,我们需要重写3个方法,分别是

  • supports:为了让realm支持jwt的凭证校验
  • doGetAuthorizationInfo:权限校验
  • doGetAuthenticationInfo:登录认证校验

我们先来总体看看AccountRealm的代码,然后逐个分析:

  • com.sen.shiro

    @Component
    public class AccountRealm extends AuthorizingRealm {
        @Autowired
        JwtUtils jwtUtils;
    
        @Autowired
        UserService userService;
    
        @Override
        public boolean supports(AuthenticationToken token) {
            return token instanceof JwtToken;
        }
    
        //获取权限信息,封装返回用户信息
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            return null;
        }
    
        //获取身份验证 获取token,校验成功返回信息
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
            JwtToken jwtToken = (JwtToken) token;
    
            String userId = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal()).getSubject();
    
            User user = userService.getById(Long.valueOf(userId));
            if (user == null){
                throw new UnknownAccountException("账号不存在!");
            }
    
            if (user.getStatus() == -1) {
                throw new LockedAccountException("账户已被锁定!");
            }
    
            AccountProfile profile = new AccountProfile();
            BeanUtil.copyProperties(user,profile);
    
            return new SimpleAuthenticationInfo(profile,jwtToken.getCredentials(),getName());
        }
    }
    

    其实主要就是doGetAuthenticationInfo登录认证这个方法,可以看到我们通过jwt获取到用户信息,判断用户的状态,最后异常就抛出对应的异常信息,否者封装成SimpleAuthenticationInfo返回给shiro。 接下来我们逐步分析里面出现的新类:

    1、shiro默认supports的是UsernamePasswordToken,而我们现在采用了jwt的方式,所以这里我们自定义一个JwtToken,来完成shiro的supports方法。

JwtToken

  • com.sen.shiro
public class JwtToken implements AuthenticationToken {
    private String token;

    public JwtToken(String jwt) {
        this.token = jwt;
    }

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

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

2、JwtUtils是个生成和校验jwt的工具类,其中有些jwt相关的密钥信息是从项目配置文件中配置的:

JwtUtils

  • com.sen.util
/**
 * jwt工具类
 */
@Slf4j
@Data
@Component
@ConfigurationProperties(prefix = "sen.jwt")
public class JwtUtils {

    private String secret;
    private long expire;
    private String header;

    /**
     * 生成jwt token
     */
    public String generateToken(long 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();
    }

    public Claims getClaimByToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            log.debug("validate is token error ", e);
            return null;
        }
    }

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

3、而在AccountRealm我们还用到了AccountProfile,这是为了登录成功之后返回的一个用户信息的载体,

AccountProfile

  • com.sen.shiro
//封装用户信息
@Data
public class AccountProfile implements Serializable {

    private Long id;

    private String username;

    private String avatar;

    private String email;
}

第三步,ok,基本的校验的路线完成之后,我们需要少量的基本信息配置:

application.yml

#shiro Config
shiro-redis:
  enabled : true
  redis-manager:
      host:127.0.0.1:6379

#jwt Config
sen:
  jwt:
    # 加密秘钥
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # token有效时长,7天,单位秒
    expire: 604800
    header: Authorization

spring-devtools.properties

第四步:另外,如果你项目有使用spring-boot-devtools,需要添加一个配置文件,在resources目录下新建文件夹META-INF,然后新建文件spring-devtools.properties,这样热重启时候才不会报错。

  • resources/META-INF/
restart.include.shiro-redis=/shiro-[\\w-\\.]+jar

第五步:定义jwt的过滤器JwtFilter。

JwtFilter

这个过滤器是我们的重点,这里我们继承的是Shiro内置的AuthenticatingFilter,一个可以内置了可以自动登录方法的的过滤器,有些同学继承BasicHttpAuthenticationFilter也是可以的。

我们需要重写几个方法:

  1. createToken:实现登录,我们需要生成我们自定义支持的JwtToken
  2. onAccessDenied:拦截校验,当头部没有Authorization时候,我们直接通过,不需要自动登录;当带有的时候,首先我们校验jwt的有效性,没问题我们就直接执行executeLogin方法实现自动登录
  3. onLoginFailure:登录异常时候进入的方法,我们直接把异常信息封装然后抛出
  4. preHandle:拦截器的前置拦截,因为我们是前后端分析项目,项目中除了需要跨域全局配置之外,我们再拦截器中也需要提供跨域支持。这样,拦截器才不会在进入Controller之前就被限制了。

下面我们看看总体的代码:

  • com.sen.shiro
@Component
public class JwtFilter extends AuthenticatingFilter {

    @Autowired
    JwtUtils jwtUtils;

    @Override
    protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            //用户没带jwt,返回空
            return null;
        }
        return new JwtToken(jwt);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        String jwt = request.getHeader("Authorization");
        if(StringUtils.isEmpty(jwt)){
            return true;
        } else {
            //校验jwt
            Claims claim = jwtUtils.getClaimByToken(jwt);
            if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())){
                throw  new ExpiredCredentialsException("token已失效,请重新登录");
            }

            //执行登录
            return executeLogin(servletRequest,servletResponse);
        }
    }

    //处理登录失败,封装成Result类型,供前端使用
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;

        Throwable throwable = e.getCause() == null ? e : e.getCause();
        Result result = Result.fail(throwable.getMessage());

        String json = JSONUtil.toJsonStr(result);

        try {
            httpServletResponse.getWriter().print(json);
        } catch (IOException ioException) {

        }
        return false;
    }

    //跨域处理(cv
    @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);
    }
}

那么到这里,我们的shiro就已经完成整合进来了,并且使用了jwt进行身份校验。

6、异常处理

有时候不可避免服务器报错的情况,如果不配置异常处理机制,就会默认返回tomcat或者nginx的5XX页面,对普通用户来说,不太友好,用户也不懂什么情况。这时候需要我们程序员设计返回一个友好简单的格式给前端。

处理办法如下:通过使用@ControllerAdvice来进行统一异常处理,@ExceptionHandler(value = RuntimeException.class)来指定捕获的Exception各个类型异常 ,这个异常的处理,是全局的,所有类似的异常,都会跑到这个地方处理。

  • com.sen.common.exception

定义全局异常处理,@ControllerAdvice表示定义全局控制器异常处理,@ExceptionHandler表示针对性异常处理,可对每种异常针对性处理。

//捕获异步异常,
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHander {


    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result hander(RuntimeException e){
        log.error("运行时异常:------------>",e);
        return  Result.fail(e.getMessage());
    }

    // 捕捉shiro的异常
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(value = ShiroException.class)
    public Result hander(ShiroException e){
        log.error("运行时异常:------------>",e);
        return  Result.fail(401,e.getMessage(),null);
    }

}

上面我们捕捉了几个异常:

  • ShiroException:shiro抛出的异常,比如没有权限,用户登录异常
  • IllegalArgumentException:处理Assert的异常
  • MethodArgumentNotValidException:处理实体校验的异常
  • RuntimeException:捕捉其他异常

7、实体校验

当我们表单数据提交的时候,前端的校验我们可以使用一些类似于jQuery Validate等js插件实现,而后端我们可以使用Hibernate validatior来做校验。

我们使用springboot框架作为基础,那么就已经自动集成了Hibernate validatior。

那么用起来啥样子的呢?

第一步:首先在实体的属性上添加对应的校验规则,比如:

@TableName("m_user")
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

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

    @NotBlank(message = "昵称不能为空")
    private String username;

    private String avatar;

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

第二步 :这里我们使用@Validated注解方式,如果实体不符合要求,系统会抛出异常,那么我们的异常处理中就捕获到MethodArgumentNotValidException。

  • com.sen.controller
@RestController
@RequestMapping("/user")
public class UserController {

    //测试实体校验
    @PostMapping("/save")
    public Result save(@Validated @RequestBody User user){
        return  Result.succ(user);
    }
}
  • com.sen.common.exception
//捕获异步异常,
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHander {   
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public Result hander(MethodArgumentNotValidException e){
        log.error("实体校验异常:------------{}",e);
        //简要输出错误信息
        BindingResult bindingResult = e.getBindingResult();
        ObjectError objectError = bindingResult.getAllErrors().stream().findFirst().get();

        return  Result.fail(401,objectError.getDefaultMessage(),null);
    }
    ...
}

注:springboot在2.3版本之后不在主动提供实体校验方法,需要手动添加依赖包

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

测试:

8、跨域问题

因为是前后端分析,所以跨域问题是避免不了的,我们直接在后台进行全局跨域处理:

  • com.sen.config.CorsConfig
/**
 * 解决跨域问题(cv)
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
                .allowCredentials(true)
                .maxAge(3600)
                .allowedHeaders("*");
    }
}

因为用户请求在进入controller之前是会先经过我们的jwtFilter的,所以jwtFilter在做事情之前,我们需要进行跨域处理

//跨域处理(cv
@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);
}

到此,我们springboot的脚手架 基本上已经搭建完成,接下来进行接口开发,这次系统开发的接口比较简单,所以我就不集成swagger2,也比较简单

9、登录接口开发

登录的逻辑其实很简答,只需要接受账号密码,然后把用户的id生成jwt,返回给前端,为了后续的jwt的延期,所以我们把jwt放在header上。具体代码如下:

  • com.sen.common.dto

封装用户信息

@Data
public class LoginDto implements Serializable {
    @NotBlank(message = "昵称不能为空")
    private String username;

    @NotBlank(message = "密码不能为空")
    private String password;
}
  • com.sen.common.exception.GlobalExceptionHander

    添加断言异常

    //断言异常
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = IllegalArgumentException.class)
    public Result hander(IllegalArgumentException e){
        log.error("Assert异常:------------{}",e);

        return  Result.fail(e.getMessage());
    }
  • com.sen.controller.AccountController
@RestController
public class AccountController {

    @Autowired
    UserService userService;

    @Autowired
    JwtUtils jwtUtils;

    @RequestMapping("/login")
    public Result login(@Validated @RequestBody LoginDto loginDto, HttpServletResponse response){
        User user = userService.getOne(new QueryWrapper<User>().eq("username", loginDto.getUsername()));
        Assert.notNull(user,"用户不存在");//断言拦截
        //判断账号密码是否错误 因为是md5加密所以这里md5判断
        if(!user.getPassword().equals(SecureUtil.md5(loginDto.getPassword()))){
            //密码不同则抛出异常
            return Result.fail("密码不正确");
        }
        String jwt = jwtUtils.generateToken(user.getId());

        //将token 放在我们的header里面
        response.setHeader("Authorization",jwt);
        response.setHeader("Access-control-Expose-Headers","Authorization");

        return Result.succ(MapUtil.builder()
                .put("id",user.getId())
                .put("username",user.getUsername())
                .put("avatar",user.getAvatar())
                .put("email",user.getEmail()).map()

        );
    }

    //需要认证权限才能推出登录
    @RequiresAuthentication
    @GetMapping("/logout")
    public Result logout(){
        SecurityUtils.getSubject().logout();
        return Result.succ(null);
    }
}

测试

10、博客接口开发

我们的骨架已经完成,接下来,我们就可以添加我们的业务接口了,下面我以一个简单的博客列表、博客详情页为例子开发:

  • com.sen.controller.BlogController
@RestController
public class BlogController {

    @Autowired
    BlogService blogService;

    //列表页
    @GetMapping("/blogs")
    public Result list(@RequestParam(defaultValue = "1") Integer currentPage){

        //分页
        Page page = new Page(currentPage,5);
        IPage pageData = blogService.page(page, new QueryWrapper<Blog>().orderByDesc("created"));

        return Result.succ(pageData);
    }

    //详情页
    @GetMapping("/blog/{id}")
    public Result detail(@PathVariable(name = "id") Long id){
        Blog blog = blogService.getById(id);

        //如果没有查询到,断言告诉前端,记录查询为空
        Assert.notNull(blog,"该博客已被删除");

        return Result.succ(blog);
    }

    //编辑页,编辑和添加博客放在一起,需要权限才能编辑
    @RequiresAuthentication
    @PostMapping("/blog/edit")
    public Result list(@Validated @RequestBody Blog blog){

        Blog temp = null;
        //编辑博客
        if (blog.getId() != null){
            temp = blogService.getById(blog.getId());
            //只能编辑自己的文章
            Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(),"你没有权限编辑");
        } else {
            temp = new Blog();
            temp.setUserId(ShiroUtil.getProfile().getId());
            temp.setCreated(LocalDateTime.now());
            temp.setStatus(0);
        }

        BeanUtils.copyProperties(blog,temp,"id","userId","created","status");
        blogService.saveOrUpdate(temp);
        return Result.succ(null);
    }
    //删除页,需要权限才能删除
    @RequiresAuthentication
    @GetMapping("/blog/delete/{id}")
    public Result delete(@PathVariable(name = "id") Long id){
        Blog blog = blogService.getById(id);

        //如果没有查询到,断言告诉前端,记录查询为空
        Assert.notNull(blog,"该博客已被删除");

        blogService.removeById(id);
        return Result.succ(null);
    }

}

注意@RequiresAuthentication说明需要登录之后才能访问的接口,其他需要权限的接口可以添加shiro的相关注解。 接口比较简单,我们就不多说了,基本增删改查而已。注意的是edit方法是需要登录才能操作的受限资源。

  • com.sen.entity.Blog

实体校验

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("m_blog")
public class Blog implements Serializable {

    private static final long serialVersionUID = 1L;

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

    private Long userId;

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

    @NotBlank(message = "博客摘要不能为空")
    private String description;

    @NotBlank(message = "博客内容不能为空")
    private String content;
    ...
}

其中用到自定义的工具类ShiroUtil

  • com.sen.util.ShiroUtil
public class ShiroUtil {

    public static AccountProfile getProfile(){
        return (AccountProfile)SecurityUtils.getSubject().getPrincipal();
    }
}

测试:

登录,取请求头的jwt


详情页

检查实体校验

检测权限,@RequiresAuthentication起作用


测试编辑

这里可能是long封装问题

Assert.isTrue(temp.getUserId().longValue() == ShiroUtil.getProfile().getId().longValue(),"你没有权限编辑");

查看数据库更改成功


删除

11、小结

后端的脚手架,接口已经初步搭建完成,也测试成功,后面就开始做前端页面开发!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容