一、理論基礎(chǔ)
1.1 如何實現(xiàn)一個相對健壯的接口
接口設計應該假設所有的調(diào)用者都是不靠譜的,所以需要做全方位的防御措施并盡可能考慮到各種因素
正常訪問
一個接口能正常訪問是最基本的、最低的要求。不管調(diào)用者傳遞什么參數(shù),接口應該都能給予良好的反饋,即使參數(shù)是錯的。
當用戶參數(shù)傳遞錯誤時,應該將錯誤信息反饋給用戶,比如缺少參數(shù)或者參數(shù)格式不正確等
返回值統(tǒng)一化
標準化的返回格式,是絕對有利于同事間的感情發(fā)展的。
如果你一會返回個S,一會返回個B,你一定會被詛咒成這個返回值的拼接體的(人工狗頭)
返回該返回的
盡可能的精簡返回值,不要返回過多的冗余信息,比如調(diào)用方只需要兩個字段,如果返回幾十個字段,對于調(diào)用者來講使用起來也不是很方便,而且還可能會暴露內(nèi)部的業(yè)務邏輯。
報錯信息全局處理
最常見的一個場景就是接口調(diào)用錯誤會將內(nèi)部代碼返給調(diào)用者,遇到過一個報名頁面報錯后,直接返回了幾行代碼,而且因為報錯的很多都是關(guān)鍵邏輯,連代碼注釋的作者都能看到。
所以未知的錯誤最好統(tǒng)一處理下,比如“網(wǎng)絡錯誤“等
及時更新的文檔
寫文檔是最不受待見的一件事,那么一個自動化接口文檔就很必要了,Swagger雖然繁瑣,但是用起來很香。
而且可以設置默認的一些參數(shù)值,直接調(diào)試接口,真的香。
版本管理
不管會不會迭代,最好加個版本號。一來可以讓接口看起來像”正規(guī)軍”,二來可以有效應對隨時到來的需求變更。
健壯的業(yè)務邏輯
這個就不多說了,相信每個人都有自己的一套方法論。
1.2 本實例集成的功能
- 接口異常全局處理
- Knife4j接口文檔
- 簡單的token驗證
- Validator參數(shù)校驗
- 返回值的統(tǒng)一化
- 解決跨域問題
- AOP統(tǒng)計接口訪問次數(shù)
1.3 示例源碼地址
https://github.com/lysmile/spring-boot-demo/tree/master/spring-boot-api-handler-demo
二、接口異常全局處理
/**
* 接口訪問統(tǒng)一異常處理
* - 當程序報錯時,由此類進行統(tǒng)一處理
* - 防止將程序內(nèi)部錯誤暴露給用戶
* @author smile
*/
@RestControllerAdvice
@Slf4j
public class ControllerExceptionHandleAdvice {
@ExceptionHandler(ValidatorRuntimeException.class)
public CodeResult validationExceptionHandler(HttpServletRequest request, ValidatorRuntimeException e) {
if (log.isDebugEnabled()) {
log.error("接口[{}]請求發(fā)生[接口檢驗異常],錯誤信息:{}", request.getRequestURI(), e.getMessage());
}
return new CodeResult(ResponseEnum.REQUEST_PARAM_ERROR.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public CodeResult exceptionHandler(HttpServletRequest request, Exception e) {
e.printStackTrace();
log.error("接口[{}]請求發(fā)生異常,錯誤信息:{}", request.getRequestURI(), e.getMessage());
return new CodeResult(ResponseEnum.SERVICE_ERROR);
}
@ExceptionHandler(TokenException.class)
public CodeResult exceptionHandler(HttpServletRequest request, TokenException e) {
if (log.isDebugEnabled()) {
log.error("接口[{}]請求發(fā)生[token驗證異常],錯誤信息:{}, token:{}", request.getRequestURI(), e.getMessage(), request.getHeader("token"));
}
return new CodeResult(ResponseEnum.TOKEN_ERROR.getCode(), e.getMessage());
}
}
三、Knife4j接口文檔
官網(wǎng)文檔:https://xiaoym.gitee.io/knife4j/
3.1 依賴引入
<!-- knife4j api文檔 -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.7</version>
</dependency>
3.2 配置文件
knife4j:
# 開啟增強配置
enable: true
# 開啟Swagger的Basic認證功能,默認是false
basic:
enable: true
username: smile
password: 123456
# 配置文檔路徑
documents:
-
group: 0.1
name: 說明文檔
# 某一個文件夾下所有的.md文件
locations: classpath:markdown/*
-
group: 0.1
name: 說明文檔2
# 某一個文件夾下所有的.md文件
locations: classpath:markdown/*
3.3 配置文件
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig {
private final OpenApiExtensionResolver openApiExtensionResolver;
public Knife4jConfig(OpenApiExtensionResolver openApiExtensionResolver) {
this.openApiExtensionResolver = openApiExtensionResolver;
}
@Bean(value = "defaultApi2")
public Docket defaultApi2() {
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder()
.title("Springboot API Handler")
.description("Springboot API 處理方法集成")
.version("0.1")
.build())
//分組名稱
.groupName("0.1")
.select()
//這里指定Controller掃描包路徑
.apis(RequestHandlerSelectors.basePackage("com.smile.demo.apihandler.controller"))
.paths(PathSelectors.any())
.build()
// 此處的0.1對應配置文件中的document-group
.extensions(openApiExtensionResolver.buildExtensions("0.1"));
return docket;
}
}
3.4 簡單應用
@Api(tags = "接口測試")
@RestController
@ApiSort(102)
@Slf4j
@RequestMapping("v0.1")
public class MainController {
@ApiImplicitParam(name = "name", value = "姓名", required = true)
@ApiOperation(value = "測試接口")
@GetMapping("first-demo")
public CodeResult firstDemo(@RequestParam(value = "name") String name) {
log.info("!!成功進入接口, 參數(shù)是:[{}]", name);
return new CodeResult(ResponseEnum.SUCCESS, name);
}
}
完整代碼可參考本實例源碼
四、簡單的token驗證
本示例只實現(xiàn)了簡單的token驗證(驗證用戶和token是否過期)
優(yōu)化方向
- Springboot全家桶:集成Springboot Security
- 自實現(xiàn)功能:Jwt + Redis可實現(xiàn)較為完善的Token驗證功能
4.1 依賴引入
<!-- java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
4.2 配置文件
my:
security:
username: api
password: 123456
4.3 Jwt工具類
@Component
public class JwtUtils {
@Value("${my.security.username}")
private String defaultUsername;
@Value("${my.security.password}")
private String defaultPassword;
/**
* 過期時間,單位毫秒
* - 10分鐘
*/
private static final long EXPIRE_TIME = 1000 * 60 * 10;
public TokenInfo createToken(String username, String password) throws TokenException {
if (!username.equals(defaultUsername) || !password.equals(defaultPassword)) {
throw new TokenException("用戶名或密碼不正確");
}
//設置頭信息
HashMap<String, Object> header = new HashMap<>(2);
header.put("typ", "JWT");
header.put("alg", "HMAC256");
long expireAt = System.currentTimeMillis() + EXPIRE_TIME;
String token = JWT.create()
.withHeader(header)
// 存入需要保存在token的信息
.withAudience(username)
// 過期時間
.withExpiresAt(new Date(expireAt))
.sign(Algorithm.HMAC256(password));
return new TokenInfo(token, expireAt);
}
public void verify(String token) throws TokenException {
Algorithm algorithm = Algorithm.HMAC256(defaultPassword);
JWTVerifier verifier = JWT.require(algorithm).build();
try {
DecodedJWT jwt = verifier.verify(token);
if(!defaultUsername.equals(jwt.getAudience().get(0))) {
throw new TokenException("用戶不存在");
}
} catch(TokenExpiredException e) {
throw new TokenException("token已過期");
} catch(Exception e) {
e.printStackTrace();
throw new TokenException("token無效");
}
}
}
4.4 攔截器
4.4.1 攔截器功能實現(xiàn)
/**
* 攔截器
* - 驗證token
* @author yangjunqiang
*/
@Component
@Slf4j
public class UserTokenInterceptor implements HandlerInterceptor {
private final JwtUtils jwtUtils;
public UserTokenInterceptor(JwtUtils jwtUtils) {
this.jwtUtils = jwtUtils;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("!!進入攔截器方法");
String token = request.getHeader("token");
if (StringUtils.isBlank(token)) {
throw new TokenException("請傳入正確的token");
}
jwtUtils.verify(token);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
4.4.2注冊攔截器
@Component
public class UserTokenAppConfig implements WebMvcConfigurer {
private final UserTokenInterceptor userTokenInterceptor;
public UserTokenAppConfig(UserTokenInterceptor userTokenInterceptor) {
this.userTokenInterceptor = userTokenInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userTokenInterceptor)
// 配置需要被攔截的接口
// 注意此處是controller中的路徑,配置文件中的server.servlet.context-path不能在此處加上,否則攔截器不會生效
.addPathPatterns("/v0.1/**")
// 不被攔截的接口
.excludePathPatterns("/token/get")
;
}
}
五、Validator參數(shù)校驗
5.1 依賴引入
<!-- 參數(shù)檢驗 -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
5.2 快速失敗配置(可選)
/**
* 參數(shù)校驗快速失敗配置
* - 此模式下只要發(fā)現(xiàn)一個參數(shù)不匹配便會快速返回錯誤
* @author smile
*/
@Configuration
public class ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
5.3 工具類
/**
* 參數(shù)驗證通用方法提取
* @author smile
*/
public class ValidatorUtils {
/**
* controller bindingResult處理
* @param bindingResult 參數(shù)驗證結(jié)果集
*/
public static void handleBindingResult(BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
throw new ValidatorRuntimeException(bindingResult.getAllErrors().get(0).getDefaultMessage());
}
}
}
5.4 簡單應用
關(guān)鍵詞:@Validated、BindingResult
配合全局異常處理一塊使用
@PostMapping("get")
@ApiOperationSupport(author = "smile")
@ApiOperation(value = "獲取token")
public CodeResult getToken(@RequestBody @Validated User user, BindingResult bindingResult) throws TokenException {
ValidatorUtils.handleBindingResult(bindingResult);
return new CodeResult(ResponseEnum.SUCCESS, JSON.toJSON(jwtUtils.createToken(user.getUsername(), user.getPassword())));
}
六、接口返回值統(tǒng)一化
6.1 接口返回實體定義
@Data
public class CodeResult {
private String errCode;
private String errMsg;
private Object data;
public CodeResult() { }
public CodeResult(String errCode, String errMsg) {
this.errCode = errCode;
this.errMsg = errMsg;
}
public CodeResult(ResponseEnum response) {
this.errCode = response.getCode();
this.errMsg = response.getMsg();
}
public CodeResult(ResponseEnum response, Object data) {
this.errCode = response.getCode();
this.errMsg = response.getMsg();
this.data = data;
}
}
定義枚舉
/**
* 錯誤碼
* 0 :成功
* 1*:業(yè)務內(nèi)自定義錯誤碼
* 2*:
* 3*:
* 4*:網(wǎng)絡錯誤
* 5*:系統(tǒng)內(nèi)部錯誤,包含代碼執(zhí)行異常等
* @author smile
*
*/
public enum ResponseEnum {
/**
* 請求成功
*/
SUCCESS("0", "success"),
/**
* 請求參數(shù)錯誤
*/
REQUEST_PARAM_ERROR("1001", "參數(shù)錯誤"),
/**
* token驗證失敗
*/
TOKEN_ERROR("1002", "token驗證失敗"),
/**
* 服務內(nèi)部異常,包括代碼執(zhí)行錯誤及一些不確定錯誤
*/
SERVICE_ERROR("5001", "服務異常,請稍后再試!")
;
private String code;
private String msg;
ResponseEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
七、解決跨域問題
/**
* 解決跨域問題
* @author smile
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
final UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
final CorsConfiguration corsConfiguration = new CorsConfiguration();
/*是否允許請求帶有驗證信息*/
corsConfiguration.setAllowCredentials(true);
/*允許訪問的客戶端域名*/
corsConfiguration.addAllowedOrigin("*");
/*允許服務端訪問的客戶端請求頭*/
corsConfiguration.addAllowedHeader("*");
/*允許訪問的方法名,GET POST等*/
corsConfiguration.addAllowedMethod("*");
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(urlBasedCorsConfigurationSource);
}
}