对外提供数据接口,安全性是必须考虑的问题。JWT无需存储客户端状态,也无须预先分配api_key、secrity_key,仅需校验数据签名,检查时间戳,就能保证请求的身份信息的完整性和防止重放攻击;而对于客户端来说,也没有跨域问题,不失为一种轻量的REST API安全机制。
一、概述
WebService有两种方案:基于SOAP的RPC和基于HTTP的REST API。REST API轻便,无状态,伸缩性更好,得到广泛使用,但缺少对安全性的直接支持,需要自己解决安全性问题。
安全性设计是个宏大的命题。这里说的REST API的安全性,只包括身份验证和授权策略2个方面,再有就是数据传输。数据传输现在一般都采用https,传输过程是加密的;授权策略,对于REST API来说,主要工作在服务器端,不会有什么大问题。所以REST API的安全性设计主要是身份验证。
REST API的身份验证方案,大约有这么几种:HTTP Basic、HTTP Digest、API KEY、Oauth 和 JWT。
1、HTTP Basic
每次请求,简单的将用户名和密码 base64 编码放到header中,传送给服务器。所以安全性较低。一定要配合ssl进行数据传输,否则无异于裸奔。
2.API KEY
Client 端向服务端注册,获得api_key以及security_key。请求的时候,客户端根据 api_key、secrity_key、timestrap、rest_uri 采用 hmacsha256 算法得到一个 hash 值 sign,发送给服务端。
服务端收到该请求后,首先验证 api_key 和 security_key,接着验证 timestrap 是否超过时间限制,最后计算 sign 值,和传过来的sign 值做校验。这样的设计就防止了数据被篡改和重放攻击。
最典型的应用,莫过于微信的接口。
3.Oauth1.0a 或者 Oauth2
OAuth 协议适用于为外部应用授权访问本站资源的情况。可见拙作:oAuth
4.JWT
JWT(JSON Web Token)。第一次身份认证后,服务器生成一个token给客户端,客户端之后每次请求,都带上这个token。听上去与第一点的http basic类似,但这个token有时间戳和签名,同样可以保证token的完整性和防止重放攻击。
不过,token里的信息,除了签名,其余部分也只是使用base64简单处理,跟明文无异,靠签名校验来保证安全性,因此也应该使用SSL进行数据传输,同时有效时间也不宜设置过久,30分钟足矣。(当然,原始token产生以后,我们自行进行加密也是可以的)
二、JWT的工作原理
1、工作过程
2、JWT的数据结构
大约类似这样:
它是一个很长的字符串,中间用点(.)分隔成三个部分。注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。
JWT 的三个部分依次如下。
Header(头部) ,元数据描述
Payload(负载) ,传输的数据
Signature(签名),Header + Payload的签名
写成一行,就是下面的样子
Header.Payload.Signature
- 1
3、Signature
Signature 部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
- 1
- 2
- 3
- 4
算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。
三、示例
1、服务器端代码结构(JAVA)
2、代码概述
控制器与外部交互,提供接口给客户端,包括身份认证接口、数据获取接口等;
身份认证时,系统验证客户端提交的用户名和密码,通过后生成JWT返回客户端;
客户端请求数据时,将JWT附在header中。拦截器会检查JWT,有效则返回数据。
3、代码明细
1)TokenUser.java
public class TokenUser {
private String name;
private String password;
private String ip;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
2)JWT静态类及相关pom.xml
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JwtUtils {
private static String SECRET = "略去"; //密钥
/*
为了保证令牌的安全性,jwt令牌由三个部分组成,分别是:
header:令牌头部,记录了整个令牌的类型和签名算法
payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改
它们组合而成的完整格式是:header.payload.signature
*/
/**
* 生成token
*
* @param map //传入payload
* payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
* @return 返回token
*/
public static String getToken(Map<String, String> map) {
JWTCreator.Builder builder = JWT.create();
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.MINUTE, 30);//有效期30分钟?
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(SECRET)).toString();
}
/**
* 验证token
*
* @param token
*/
public static void verify(String token) {
JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
/**
* 获取token明文
*
* @param token
* @return
*/
public static DecodedJWT getTokenPlainText(String token) {
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
pom.xml中要引入jwt类库
<!--引入JWT-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.0</version>
</dependency>
- 1
- 2
- 3
- 4
- 5
- 6
3)拦截器及拦截器使能
(1)拦截器
mport com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.gzdd.rainapi.utils.JwtUtils;
import org.springframework.http.HttpMethod;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* JWT验证拦截器
*/
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
/**
* 前后端分离有时候会有两次请求,第一次为OPTIONS请求,默认会拦截所有请求,但是第一次请求又获取不到jwt,所以会出错。
**/
if (HttpMethod.OPTIONS.toString().equals(request.getMethod())) {
return true;
}
Map<String, Object> map = new HashMap<>();
//令牌建议是放在请求头中,获取请求头中令牌
String token = request.getHeader("token");
try {
JwtUtils.verify(token);//验证令牌
return true;//放行请求
} catch (SignatureVerificationException e) {
map.put("msg", "无效签名");
map.put("status", 401);
} catch (TokenExpiredException e) {
map.put("msg", "token过期");
map.put("status", 401);
} catch (AlgorithmMismatchException e) {
map.put("msg", "token算法不一致");
map.put("status", 401);
} catch (Exception e) {
map.put("msg", "token失效");
map.put("status", 401);
}
map.put("state", false);//设置状态
//将map转化成json,response使用的是Jackson
System.out.println(map);
Map res = new HashMap();
res.put("data", map);
String json = new ObjectMapper().writeValueAsString(res);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().print(json);
return false;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
(2)使用拦截器
import com.gzdd.rainapi.interceptor.JwtInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class SecurityConfiguration implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtI;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtI)
.addPathPatterns("/data/*");//取数据的接口,必须检查JWT
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
4)控制器
(1)身份认证
import com.gzdd.rainapi.config.AppConfig;
import com.gzdd.rainapi.pojo.TokenUser;
import com.gzdd.rainapi.utils.IPUtils;
import com.gzdd.rainapi.utils.JwtUtils;
import com.gzdd.rainapi.utils.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
public class IndexController {
@Autowired
AppConfig appConfig;
@RequestMapping(value = {"/test"})
public String noCheck(Model model) {
return "Hello world!";
}
//登录时生成token
@PostMapping(value = "/token")
public Result getToken(HttpServletRequest request, @RequestBody TokenUser user) {
if (!appConfig.isValidUser(user.getName(), user.getPassword())) {
return Result.error("非法的用户");
}
Map<String, String> payload = new HashMap<>();
payload.put("ip", IPUtils.getIpAddr(request));
payload.put("name", user.getName());
payload.put("time", System.currentTimeMillis() + "");
String token = JwtUtils.getToken(payload);//生成JWT令牌
return Result.ok().put("token", token);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
(2)数据接口
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//注意这些接口以/data/开头,符合拦截器使能条件
@RestController
@RequestMapping(value = "/data")
public class DataController {
@RequestMapping(value = {"/test"})
public String test(Model model) {
return "welcome to data api!";
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
5)测试结果
(1)身份认证
(2)数据获取