token被劫持,DOS攻击,重复提交等,接口安全尤为重要,接口安全主要围绕Token、Timestamp和Sign三个机制展开设计,保证接口的数据不会被篡改和重复调用。
Token授权
token是客户端访问服务端的凭证。用户通过账号密码登录后,服务器返回token给客户端,并存储在缓存中,服务器接受到请求后进行token验证,如果token不存在说明未登录,
时间戳超时
时间戳超时机制是防御DOS攻击的有效手段。每次请求都带上当前时间的时间戳timestamp,服务端接收到timestamp后跟当前时间进行比对,如果时间差大于一定时间(比如5秒钟),则认为该请求失效。
签名
签名机制保证了数据不会被篡改。将Token和时间戳加上其他请求参数再用MD5或其他算法加密,加密后的数据就是本次请求的签名sign,服务端接收到请求后以同样的算法得到签名,并跟当前的签名进行比对,如果不一致,说明参数被更改过,直接返回错误。
IP限制
通过IP解密,防止token被截取后在别的网络环境发出请求,在服务器通过请求ip与这个ip必须一致才能解密
拒绝重复调用
客户端第一次访问时,将签名sign存放到缓存中,超时时间设定为5秒,则5秒内只能访问一次。如果使用同一个URL再次访问,发现缓存中已经存在了本次签名,则拒绝服务。
- 客户端通过用户名密码登录服务器并获取Token
import com.example.user.cache.CacheUtil;
import com.example.user.result.Result;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
@RestController
public class UserController {
@GetMapping("login")
public Result login(@RequestParam String username, @RequestParam String password){
String token = null;
if("zhangshan".equals(username) && "123456".equals(password)){
token = UUID.randomUUID().toString().replaceAll("-","");
CacheUtil.newNoCache().put(token,username);
}
return Result.success(token);
}
@GetMapping("users/{id}")
public Result users(@PathVariable String id){
Map user = new HashMap();
if("1".equals(id)){
user.put("username","zhangsan");
user.put("password","123456");
user.put("sex","man");
}
return Result.success(user);
}
}
-
客户端生成时间戳timestamp,并将timestamp作为其中一个参数
-
客户端将Token和timestamp按照自己的算法(这里使用MD5)进行排序加密得到签名sign
-
将token、timestamp和sign作为请求时必须携带的参数加在每个请求的URL后边(http://localhost/users/1?token=334d1e48834b494daef3657dfac47af7×tamp=1618815569248&sign=30819c9c1504795b74a198e0f35b3789)
-
服务端写一个过滤器对token、timestamp和sign进行验证,只有在token有效、timestamp未超时、缓存服务器中不存在sign三种情况同时满足,本次请求才有效
import com.alibaba.fastjson.JSON;
import com.example.user.cache.CacheUtil;
import com.example.user.crypto.SignUtil;
import com.example.user.result.CodeMsg;
import com.example.user.result.Result;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
/**
* 登录过滤器
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getParameter("token");
String timestamp = request.getParameter("timestamp");
String sign = request.getParameter("sign");
String uri = request.getRequestURI();
if(null == token || null == timestamp || null == sign){
render(response,CodeMsg.UNAUTHORIZED);
return false;
}
//验证token
if(null == CacheUtil.newNoCache().get(token)){
render(response,CodeMsg.TOKEN_ERROR);
return false;
}
//校验超时
long currentTime = System.currentTimeMillis();
long timeOut = currentTime-Long.valueOf(timestamp);
if(timeOut > 5000 || timeOut < 0){
render(response,CodeMsg.REQ_TIMEOUT);
return false;
}
//验证签明
String data = token+timestamp;
if(!SignUtil.verify(data, sign)){
render(response,CodeMsg.SIGN_ERROR);
return false;
}
//防止重复提交
if(null != CacheUtil.newNoCache().get(token+uri)){
render(response,CodeMsg.DUPLICATE_SUBMISSION);
return false;
}else {
CacheUtil.newNoCache().put(token+uri,"1",10L);
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm) throws Exception {
response.setContentType("application/json;charset=UTF-8");
OutputStream out = response.getOutputStream();
String str = JSON.toJSONString(Result.codeMsg(cm));
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
}
注册拦截器
import com.example.user.interceptor.LoginInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Bean
public LoginInterceptor getLoginInterceptor() {
return new LoginInterceptor();
}
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getLoginInterceptor()).addPathPatterns("/users/*");
super.addInterceptors(registry);
}
}
简化版签名工具类
import org.springframework.util.DigestUtils;
public class SignUtil {
/**
* 签名
* @param data
* @return
*/
public static String sign(String data){
String signed = DigestUtils.md5DigestAsHex(data.getBytes());
return signed;
}
/**
* 验证签名
* @param data 元数据
* @param signed 签名数据
* @return
*/
public static boolean verify(String data,String signed){
String sign = SignUtil.sign(data);
return sign.equals(signed);
}
}
简化版统一返回
public enum CodeMsg {
SUCCESS(200, "成功"),
ERROR(500, "服务器异常"),
UNAUTHORIZED(401,"您还未登录"),
TOKEN_ERROR(401,"token校验失败"),
SIGN_ERROR(401,"签名认证失败"),
REQ_TIMEOUT(403,"请求超时"),
DUPLICATE_SUBMISSION(403,"重复提交");
private int code;
private String msg;
CodeMsg(int code, String msg) {
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
public class Result<T> {
private int code;
private String msg;
private T data;
private Result() {
}
protected Result(int code, String msg) {
this.code = code;
this.msg = msg;
}
protected Result(CodeMsg cm) {
this.code = cm.getCode();
this.msg = cm.getMsg();
}
protected Result(CodeMsg cm, T data) {
this.code = cm.getCode();
this.msg = cm.getMsg();
this.data = data;
}
public static <T> Result success(T data) {
return new Result(CodeMsg.SUCCESS, data);
}
public static Result codeMsg(CodeMsg cm) {
return new Result(cm);
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}