Springboot如何设计出优雅的后端(API)接口(二)

12 篇文章 0 订阅
3 篇文章 1 订阅

前言

接着上一篇的文章Springboot如何设计出优雅的后端(API)接口(一)
首先我们回顾一下之前所说需要解决的问题:

  1. 统一返回接口格式。
  2. 全局异常处理。
  3. 参数常规非空校验。
  4. 通用的分页对象。
  5. 日志统一格式。
  6. 常用工具类。
  7. 接口文档。
  8. 接口的安全。
    Springboot如何设计出优雅的后端(API)接口(一)已经解决了前4个问题,那么这篇文章就给大家解决下面4个问题。
    警告:本篇博客篇幅较长,建议各位大佬先下载demo,一边看demo一边看文章。spring-api-demo

日志的统一格式

日志的话我习惯性采用logback来配置:所谓的统一格式,也就是日志配置文件的内容:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 此xml在spring-boot-1.5.3.RELEASE.jar里 -->
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
    <!-- 开启后可以通过jmx动态控制日志级别(springboot Admin的功能) -->
    <!--<jmxConfigurator/>-->
    <springProperty scope="context" name="APP_NAME" source="spring.application.name" defaultValue="UNKNOWN_APP"/>
    <springProperty scope="context" name="LOG_SERVER" source="log.server" defaultValue="localhost"/>

    <!--控制台 appender 定义 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{60} [%file : %line] - %msg%n</pattern>
        </encoder>
    </appender>


    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--<File>/home/hfw-client/hfw_log/stdout.log</File>-->
        <file>/opt/logs/cj-api/${APP_NAME:-.}.log</file>
        <encoder>
            <pattern>%date [%level] [%thread] %logger{60} [%file : %line] %msg%n</pattern>
        </encoder>
        <!--这个是在总的日志文件中将用户日志排除-->
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                <expression>return logger.contains("userLogger");</expression>
            </evaluator>
            <onMatch>DENY</onMatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/opt/logs/cj-api/cj-api/%d{yyyy-MM,aux}/${APP_NAME:-.}-%d{yyyy-MM-dd}.%i.zip
            </fileNamePattern>
            <maxHistory>60</maxHistory>
            <maxFileSize>50MB</maxFileSize>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!-- 用户相关日志 -->
    <appender name="USER_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/opt/logs/cj-api/${APP_NAME:-.}-user.log</file>
        <encoder>
            <pattern>%date [%level] [%thread] %logger{60} [%file : %line] %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.core.filter.EvaluatorFilter">
            <evaluator class="ch.qos.logback.classic.boolex.JaninoEventEvaluator">
                <expression>return logger.contains("userLogger");</expression>
            </evaluator>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/opt/logs/cj-api/user/%d{yyyy-MM,aux}/${APP_NAME:-.}-user-%d{yyyy-MM-dd}.%i.zip
            </fileNamePattern>
            <maxHistory>60</maxHistory>
            <maxFileSize>50MB</maxFileSize>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <!--error级别日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/opt/logs/cj-api/${APP_NAME:-.}-error.log</file>
        <encoder>
            <pattern>%date [%level] [%thread] %logger{60} [%file : %line] %msg%n</pattern>
        </encoder>
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>/opt/logs/cj-api/error/%d{yyyy-MM,aux}/${APP_NAME:-.}-error-%d{yyyy-MM-dd}.%i.zip
            </fileNamePattern>
            <maxHistory>60</maxHistory>
            <maxFileSize>50MB</maxFileSize>
            <totalSizeCap>3GB</totalSizeCap>
        </rollingPolicy>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
        <appender-ref ref="USER_FILE"/>
        <appender-ref ref="ERROR_FILE"/>
    </root>
</configuration>

简要说明下:
这个日志分类:根据业务逻辑进行区分,包含“userLogger”的日志会区分到不同的文件中。
日志的格式:

%date [%level] [%thread] %logger{60} [%file : %line] %msg%n

controller中使用:

 /**
 注意:引入的包
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 **/
 private Logger logger = LoggerFactory.getLogger("userLogger");
 logger.info("消息");

控制台打印:

2020-12-03 10:01:28.317 [http-nio-8082-exec-2] INFO  userLogger [UserController.java : 31] - 消息

分别是: 时间-线程名称- 日志级别 logger名称-文件名

常用工具类

我推荐
HuTool工具类
话不多说,大佬们可以具体查看下这个工具类,东西蛮全的。

接口文档

我经常使用的是swagger。
如何集成swagger主要分成三步:

  1. 引入maven包
<!-- swagger-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.8.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.8.0</version>
        </dependency>
  1. 配置相关的swagger配置:
package com.cj.demo.config;

import io.swagger.annotations.Api;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.Parameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * 描述:
 *
 * @author cj
 * @create 2019-11-18 17:40
 */
@EnableSwagger2
@Configuration
public class SwaggerConfig {

    @Bean
    public Docket newsApi() {
//        ParameterBuilder parameterBuilder = new ParameterBuilder();
//        List<Parameter> parameters = new ArrayList<Parameter>();
//        parameterBuilder.name("Authentication").
//                description("token值").
//                modelRef(new ModelRef("string")).parameterType("header").required(false).build();
//        parameters.add(parameterBuilder.build());
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("CJ-API")
                .apiInfo(new ApiInfoBuilder().title("CJ-API").description("CJ-API接口地址").version("1.0.0").build())
                .select()
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .build();
//                .globalOperationParameters(parameters);
    }
}

  1. 使用swagger的注解配置接口文档:
    主要使用的注解有以下几种:
注解说明
@Api写在类上标明类的作用
@ApiOperation写在方法上,对方法的说明
@ApiParam写在请求的参数上,标明参数的含义(一般适用于get请求中的参数说明)
@ApiModel对实体类的说明
@ApiModelProperty也是对请求参数的说明(一般是在请求实体类中,对某一个实体类中的某个字段进行说明)

其实我们仔细想一下:作为前端而言最关心的是什么?接口地址+接口参数(说明)+接口返回结果(说明)。那么上面5种注解都已经包含了这些前端最关心的内容,下面是我自己写的一些注解,光看说明还是不了解的话,可以看下下面的代码:

package com.cj.demo.controller;

import com.cj.demo.bean.ResponseBean;
import com.cj.demo.bean.request.UserRequestVO;
import com.cj.demo.bean.user.UserBean;
import com.cj.demo.service.UserService;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;
import java.util.List;

/**
 * 描述:
 *
 * @author caojing
 * @create 2020-11-27-15:44
 */
@Api(tags = "通用接口")
@RestController
public class UserController {
    private Logger logger = LoggerFactory.getLogger("userLogger");
    @Autowired
    private UserService userService;

    @ApiOperation("获取指定学生信息")
    @GetMapping("/user")
    public ResponseBean<UserBean> getUserInfo(@ApiParam("学生id") @RequestParam("id") Integer id) {
        logger.info("消息");
        UserBean userBean = userService.selectById(id);
        ResponseBean responseBean = new ResponseBean();
        responseBean.setData(userBean);
        responseBean.setCode(0);
        return responseBean;
    }

    @ApiOperation("分页获取学生信息")
    @PostMapping("/user/page")
    public ResponseBean<PageInfo<List<UserBean>>> getUserInfoPage(@RequestBody UserRequestVO userRequestVO) {
        PageHelper.startPage(userRequestVO.getPageNum(), userRequestVO.getPageSize());
        List<UserBean> userBean = userService.selectPage();
        PageInfo pageInfo = new PageInfo(userBean);
        pageInfo.setList(userBean);
        ResponseBean responseBean = new ResponseBean();
        responseBean.setData(pageInfo);
        responseBean.setCode(0);
        return responseBean;
    }

    @ApiOperation("新增学生信息")
    @PostMapping("/user")
    public ResponseBean addUser(@Valid @RequestBody UserRequestVO userBean) {
        return new ResponseBean();
    }
}

实体类

package com.cj.demo.bean.request;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

/**
 * 描述:
 *
 * @author caojing
 * @create 2020-12-02-14:29
 */
@ApiModel(description = "用户请求实体类")
public class UserRequestVO extends BasePageRequestVO {
    @ApiModelProperty("用户姓名")
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return super.toString();
    }
}

接口安全

我个人感觉,这篇文章最重要的相对而言稍微有点难度的就是这个接口安全的集成了。
主要的技术点就是springboot+shiro+jwt。
实现的主要逻辑:

  1. 提供一个登录接口:POST 用户名与密码到 /login 进行登入,如果成功返回一个加密 token,失败的话直接返回 401 错误。
  2. 后续的用户访问每一个需要权限的网址请求必须在 header 中添加 Authorization 字段,例如 Authorization: token ,token 为密钥。后台会进行 token 的校验,如果有误会直接返回 401。
    之前我有文章介绍了springboot中如何集成shiro,感兴趣的同学可以去看下:SpringBoot2.0集成Shiro
    本篇文章主要还是实战为主,具体有关jwt的一些知识点,以为为什么要这么做,我在这边就不给大家介绍了,感兴趣的同学可以百度看看。
    直接上代码:
  • shiro配置类

package com.deepbluebi.aip.iot.config.shiro;

import com.deepbluebi.aip.iot.config.jwt.JwtFilter;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

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

/**
 * @ClassName: ShiroConfiguration
 * @Description:TODO(Shiro配置文件)
 * @author: wangshuai
 * @date: 2017年7月14日 上午11:18:15
 * @Copyright: 2017 www.deepbluebi.com Inc. All rights reserved.
 */
@Configuration
public class ShiroConfiguration {

    private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);

    /**
     * * Shiro的Web过滤器Factory 命名:shiroFilter<br />
     * * * @param securityManager * @return
     */
    @Bean(name = "shiroFilter")
    public ShiroFilterFactoryBean shiroFilterFactoryBean(org.apache.shiro.mgt.SecurityManager securityManager) {
        logger.info("注入Shiro的Web过滤器-->shiroFilter", ShiroFilterFactoryBean.class);
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JwtFilter());

        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/test/401");

        /*
         * 自定义url规则
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        // 所有请求通过我们自己的JWT Filter
        filterRuleMap.put("/**", "jwt");
//        filterRuleMap.put("/swagger**/**", "anon");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/test/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(UserRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }
    /**
     * Shiro生命周期处理器 * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * *
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * *
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * * @return
     */
	@Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}
  • UserRealm

真正的對token值進行校验的实体类

package com.cj.demo.config;

import com.cj.demo.bean.user.UserBean;
import com.cj.demo.common.RedisUtils;
import com.cj.demo.common.Tools;
import com.cj.demo.config.jwt.JwtToken;
import com.cj.demo.config.jwt.JwtUtil;
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.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * 集成Shiro核心内容:
 * ShiroFilterFactory,Shiro过滤器工程类,具体的实现类是:ShiroFilterFactoryBean,此实现类是依赖于SecurityManager安全管理器。主要配置Filter就好。
 * SecurityManager,Shiro的安全管理,主要是身份认证的管理,缓存管理,cookie管理,所以在实际开发中我们主要是和SecurityManager进行打交道的。
 * Realm,用于身份信息权限信息的验证。开发时集成AuthorizingRealm,重写两个方法:doGetAuthenticationInfo(获取即将需要认真的信息)、doGetAuthorizationInfo(获取通过认证后的权限信息)。
 * 参考资料 http://www.infocool.net/kb/Apache/201609/190994.html
 * http://blog.csdn.net/catoop/article/details/50520958
 *
 * @ClassName: UserRealm
 * @Description:TODO(shiro权限验证)
 * @author: cj
 * @date: 2017年3月30日 下午9:08:21
 * @Copyright:  Inc. All rights reserved.
 */
@Slf4j
@Service
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private RedisUtils redisUtils;

    //
//    /**
//     * 大坑!,必须重写此方法,不然Shiro会报错
//     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 获取通过认证后的权限信息
     * 只有当需要检测用户权限的时候才会调用此方法
     * principals 也就是在doGetAuthenticationInfo 这个方法中最后SimpleAuthenticationInfo这个方法中设置的userBean对象
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        UserBean sysUserBean = (UserBean) principals.getPrimaryPrincipal();
        log.info("----------通过后认证权限信息 ---------  currentLoginName=" + sysUserBean.getName());
        List<String> userRoles = new ArrayList<String>();
        //这里可以根据帐号类型不同区分能够访问的接口不同
//        // 从数据库中获取当前登录用户的详细信息(我这里简单一点,只要区分主账号和子账号就可以了)
//        //0是主账号,1是子账号
//        if (sysUserBean.getAccountType() == 0) {
//            userRoles.add("farther");
//        } else {
//            userRoles.add("children");
//        }
        // 为当前用户设置角色和权限
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.addRoles(userRoles);
        //暂时不需要这个权限
//        Set<String> permission = new HashSet<>();
//        permission.add("admin");
//        authorizationInfo.addStringPermissions(permission);

        return authorizationInfo;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     * 这边抛出异常之后,会在上面一层捕获到,具体是在isAccessAllowed 方法中捕获
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.info("----------认证用户登录信息 ---------");
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String signOld = redisUtils.get(token);
        if (signOld == null) {
            throw new AuthenticationException("token timeout");
        }
        String username = JwtUtil.getUsername(signOld);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }
        //这段其实是从数据库取出用户信息进行对比
        UserBean userBean =new UserBean();
        userBean.setName("cj");
        userBean.setPassword(Tools.MD5Pwd(userBean.getName(),"cj"));
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }
        //这边还是需要验证一次,防止用户在后台修改了密码之后,没有清除token。以前的token值还能用的情况
        if (!JwtUtil.verify(signOld, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }
        //这个地方是保证在调用一次接口之后,刷新token值(我理解的token值过期时间,是在没有操作的前提下才会去算这个过期时间
        // 既然都在操作了,过期时间肯定是要刷新)
        String signNew = JwtUtil.sign(username, userBean.getPassword());
        redisUtils.setEx(token, signNew, 20 * 60, TimeUnit.SECONDS);
        return new SimpleAuthenticationInfo(userBean, token, getName());
    }

}

  • jwt的过滤器

package com.cj.demo.config.jwt;

import com.alibaba.fastjson.JSONObject;
import com.cj.demo.bean.ResponseBean;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
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;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

/**
 * 描述:
 *
 * @author caojing
 * @create 2019-11-19-10:04
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
    /**
     * 判断用户是否想要登入。
     * 检测header里面是否包含Authentication字段即可
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authentication = req.getHeader("Authentication");
        return authentication != null;
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authentication = httpServletRequest.getHeader("Authentication");
        JwtToken token = new JwtToken(authentication);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(token);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }

    /**
     * 这里我们详细说明下为什么最终返回的都是true,即允许访问
     * 例如我们提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以我们在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Authentication,Origin, X-Requested-With, Content-Type, Accept");
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 将非法请求跳转到 /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {

        HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
        httpServletResponse.setHeader("Access-control-Allow-Origin", "*");
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", "Authentication,Origin, X-Requested-With, Content-Type, Accept");
//            httpServletResponse.setStatus(401);
        String jsonString = JSONObject.toJSONString(new ResponseBean<>(401, "登陆态失效,请重新登陆", ""));
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json; charset=utf-8");
        OutputStream out = null;

        try {
            out = httpServletResponse.getOutputStream();
            out.write(jsonString.getBytes(StandardCharsets.UTF_8));
            out.flush();
        } catch (IOException e) {
            log.error("非法请求", e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    log.error("非法请求", e);
                }
            }
        }
    }

    /**
     * 为response设置header,实现跨域
     */
    private void setHeader(HttpServletRequest request, HttpServletResponse response) {
        //跨域的header设置
        response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", request.getMethod());
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
        //防止乱码,适用于传输JSON数据
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
    }
}
  • jwtToken认证

package com.cj.demo.config.jwt;

import org.apache.shiro.authc.AuthenticationToken;

/**
 * 描述:这边重写了AuthenticationToken,对应的是 UserRealm中身份验证的过程,重写原来的getPrincipal和getCredentials方法。
 * getPrincipal 是获取身份(也就是用户名)
 * getCredentials 是凭据
 *
 * @author caojing
 * @create 2019-11-19-10:00
 */
public class JwtToken implements AuthenticationToken {

    private String token;

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

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

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

}

工具类就不贴了,太tm长了。

几个小问题

  1. swagger中如何加入header参数:
    swagger的配置修改:
  @Bean
    public Docket newsApi() {
        ParameterBuilder parameterBuilder = new ParameterBuilder();
        List<Parameter> parameters = new ArrayList<Parameter>();
        parameterBuilder.name("Authentication").
                description("token值").
                modelRef(new ModelRef("string")).parameterType("header").required(false).build();
        parameters.add(parameterBuilder.build());
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("CJ-API")
                .apiInfo(new ApiInfoBuilder().title("CJ-API").description("CJ-API接口地址").version("1.0.0").build())
                .select()
                .apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
                .build()
                .globalOperationParameters(parameters);
    }
  1. 如果访问一个需要token的接口,该请求没有携带token值,后台就会报错,如何清除报错信息(其实对于后台来讲,此报错非真报错)
    全局的异常处理加入:
 /**
     * 捕捉shiro的异常
     */
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        if (e instanceof UnauthorizedException) {
            return new ResponseBean(1,"无对应权限",null);
        } else if (e instanceof AuthenticationException) {
            return new ResponseBean(1,e.getMessage(),null);
        }
        return new ResponseBean(401, "Shiro错误," + e.getMessage(), null);
    }

总结:

基本上这个项目算是一个开发api接口的基础项目了,后续如果有项目过来的话,直接修改下包名、项目名就可以进行开发了。一些基础的逻辑都已经写好了。如果有什么问题的或者说在平常开发中有什么觉得好的技巧、框架啥的,大家都可以在文章下留言。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值