前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >拿来即用:实现token验证的后端api接口框架

拿来即用:实现token验证的后端api接口框架

作者头像
你好戴先生
发布2021-10-14 11:46:20
1.1K0
发布2021-10-14 11:46:20
举报
文章被收录于专栏:戴言泛滥

以便分别做相应的处理

代码语言:javascript
复制
token.private-key=hello-daijiyong
#token25分钟后自动刷新
token.expires.young=2500000
#token30分钟后过期
token.expires.old=3000000

设置token拦截处理器

将token放到header中,针对每一次请求都进行token验证处理

如果token不存在或者错误,则抛出异常

代码语言:javascript
复制
@Slf4j
@Component
public class AuthHandlerInterceptor implements HandlerInterceptor {

    /**
     * 权限认证的拦截操作.
     */
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws TokenAuthException {
        log.info("=======进入拦截器========");
        // 如果不是映射到方法直接通过,可以访问资源.
        if (!(object instanceof HandlerMethod)) {
            return true;
        }

        //为空就返回错误
        String token = httpServletRequest.getHeader(TokenUtil.REQUEST_HEADER_TOKEN_KEY);
        if (null == token || "".equals(token.trim())) {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }

        TokenVo tokenVo = TokenUtil.parseToken(token);
        long timeOfUse = System.currentTimeMillis() - tokenVo.getTimestamp();
        //1.判断 token 是否过期
        //年轻 token
        /*if (timeOfUse < TokenUtil.youngToken) {
            log.info("年轻 token");
        }*/
        //老年 token 就刷新 token
        if (timeOfUse >= TokenUtil.youngToken && timeOfUse < TokenUtil.oldToken) {
            TokenUtil.getToken(tokenVo);
            httpServletResponse.setHeader(TokenUtil.REQUEST_HEADER_TOKEN_KEY, tokenVo.getToken());
            httpServletResponse.setHeader(TokenUtil.REQUEST_HEADER_EXPIRES_KEY, String.valueOf(tokenVo.getTimestamp() + TokenUtil.oldToken));
        }
        //过期 token 就返回 token 无效.
        else {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }
        return false;
    }
}

配置拦截器内容,放行登录和测试接口等

代码语言:javascript
复制
@Configuration
public class AuthWebMvcConfigurer implements WebMvcConfigurer {

    @Resource
    AuthHandlerInterceptor authHandlerInterceptor;
    private final String[] excludePathPatters = new String[]{"/api/wechat/token/getToken", "/api/test/**"};

    /**
     * 给除了 excludePathPatters 配置的接口都配置拦截器,拦截转向到 authHandlerInterceptor
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authHandlerInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludePathPatters);
    }
}

token获取和更新接口

代码语言:javascript
复制
@Slf4j
@RestController
@RequestMapping("/api/wechat/token")
public class TokenController {

    @RequestMapping(path = "/getToken", method = RequestMethod.POST)
    public TokenDataDto getToken(TokenVo tokenVo) throws TokenAuthException {
        log.info("请求token参数:{}", tokenVo);
        TokenUtil.getToken(tokenVo);
        return new TokenDataDto(tokenVo);
    }

    @RequestMapping(path = "/refreshToken", method = RequestMethod.POST)
    public TokenDataDto refreshToken(HttpServletRequest request) throws TokenAuthException {
        String token = request.getHeader(TokenUtil.REQUEST_HEADER_TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }
        TokenVo tokenVo = TokenUtil.parseToken(token);
        long timeOfUse = System.currentTimeMillis() - tokenVo.getTimestamp();
        //判断 token 是否过期
        if (timeOfUse > TokenUtil.oldToken) {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }
        TokenUtil.getToken(tokenVo);
        return new TokenDataDto(tokenVo);
    }
}

## 全局异常捕获

根据自定义异常类型,进行全局异常捕获

定义接口处理异常类型

代码语言:javascript
复制
public class ResponseException extends Exception {
    static final long serialVersionUID = 1L;
    private final String status;

    public ResponseException(String status) {
        super();
        this.status = status;
    }

    public ResponseException(String message, String status) {
        super(message);
        this.status = status;
    }

    public ResponseException(ReturnCode returnCode) {
        super(returnCode.getMessage());
        this.status = returnCode.getStatus();
    }

    public String getStatus() {
        return status;
    }
}

通过@RestControllerAdvice注解捕捉特定异常类型

比如token异常、接口处理异常等

并做相应的处理

代码语言:javascript
复制
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    /**
     * 默认全局异常处理。
     *
     * @param e the e
     * @return ResultData
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.OK)
    public ResultData<String> exception(Exception e) {
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        return ResultData.fail(ReturnCode.FAIL.getStatus(), e.getMessage());
    }

    /**
     * 默认全局异常处理。
     *
     * @param e the e
     * @return ResultData
     */
    @ExceptionHandler(ResponseException.class)
    @ResponseStatus(HttpStatus.OK)
    public ResultData<String> runtimeException(ResponseException e) {
        log.error("全局异常信息 ex={}", e.getMessage(), e);
        return ResultData.fail(e.getStatus(), e.getMessage());
    }

    /**
     * token异常处理。
     *
     * @param e the e
     * @return ResultData
     */
    @ExceptionHandler(TokenAuthException.class)
    @ResponseStatus(HttpStatus.OK)
    public ResultData<String> tokenAuthException(TokenAuthException e) {
        log.error("token异常信息 ex={}", e.getMessage(), e);
        return ResultData.fail(e.getStatus(), e.getMessage());
    }
}

可通过以下测试接口测试一下

代码语言:javascript
复制
@RestController
@RequestMapping("/api/test")
public class TestController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private TestService testService;

    @RequestMapping("/success")
    public String test() {
        logger.info("数据接口测试成功!!!");
        testService.test();
        return "数据测试成功";
    }

    @RequestMapping("/fail")
    public void fail() {
        throw new RuntimeException("异常测试");
    }

}

## 统一接口返回实体封装

传统的处理方式我们需要定义一个如下的实体类

代码语言:javascript
复制
@Data
public class ResultData<T> {
    private String status;
    private String message;
    private T data;
    private long timestamp;
    //省略
}

然后在每一个接口返回的地方new一个新对象

并将数据实体set到data中

很是繁琐且不优雅

下面实现接口返回实体自动封装的功能

比如定义的token获取和更新接口,只需返回数据实体即可

自动封装成特定的数据格式

代码语言:javascript
复制
@RestController
@RequestMapping("/api/test")
public class TestController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private TestService testService;

    @RequestMapping("/success")
    public String test() {
        logger.info("数据接口测试成功!!!");
        testService.test();
        return "数据测试成功";
    }

    @RequestMapping("/fail")
    public void fail() {
        throw new RuntimeException("异常测试");
    }

    @RequestMapping(path = "/refreshToken", method = RequestMethod.POST)
    public TokenDataDto refreshToken(HttpServletRequest request) throws TokenAuthException {
        String token = request.getHeader(TokenUtil.REQUEST_HEADER_TOKEN_KEY);
        if (StringUtils.isBlank(token)) {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }
        TokenVo tokenVo = TokenUtil.parseToken(token);
        long timeOfUse = System.currentTimeMillis() - tokenVo.getTimestamp();
        //判断 token 是否过期
        if (timeOfUse > TokenUtil.oldToken) {
            throw new TokenAuthException(ReturnCode.INVALID_TOKEN);
        }
        TokenUtil.getToken(tokenVo);
        return new TokenDataDto(tokenVo);
    }
}

通过@RestControllerAdvice注解捕捉所有接口返回结果

并对返回结果进行统一的封装处理

代码语言:javascript
复制
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Resource
    ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {

        if (o instanceof String) {
            try {
                return objectMapper.writeValueAsString(ResultData.success(o));
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
        }

        // 如果已经封装了返回实体,则不再进行封装
        if (o instanceof ResultData) {
            return o;
        }
        return ResultData.success(o);
    }
}

通过以上设置,可以避免繁琐的操作,全局统一封装返回实体

代码语言:javascript
复制
{
    "status": "000000",
    "message": "操作成功!",
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwYXNzd29yZCI6ImRhaWppeW9uZyIsInVzZXJOYW1lIjoiZGFpaml5b25nIiwidGltZXN0YW1wIjoxNjMyNTc3OTI5MDc0fQ.gdrwQmyMStNnCxUqPYPay_igjdPBNmbvuUIoavYnbhM",
        "expires": 1632580929074
    },
    "timestamp": 1632577929076
}

## 后续安排

逐步完善框架功能,集成反向工程功能、集成分页插件、接口文档、代码优化等

文/戴先生@2021年10月05日

---end---

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 你好戴先生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档