用JWT机制实现Token身份验证
1.JWT(JSON WEB TOKEN)
由于HTTP的无状态性,我们无法判断是哪个客户端在请求接口。这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端再发送请求时候,还得再验证一下。
解决的方法就是,当用户请求登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,然后把这条记录的 ID 号发送给客户端,客户端收到以后把这个 ID 号存储在 Cookie 里,下次这个用户再向服务端发送请求的时候,可以带着这个 Cookie ,这样服务端会验证一个这个 Cookie 里的信息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
上面说的就是 Session,我们需要在服务端存储为登录的用户生成的 Session ,这些 Session 可能会存储在内存,磁盘,或者数据库里。我们可能需要在服务端定期的去清理过期的 Session 。
但是有些情况下session并不那么好用,一个是session会占用后端内存资源,还有如果用户cookie中的sessionId被窃取,很容易就可以获取用户的私有数据.这时我们可以用token来解决这些问题
使用基于 Token 的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
- 1.客户端使用用户名跟密码请求登录
- 2.服务端收到请求,去验证用户名与密码
- 3.验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
- 4.客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 Local Storage 里
- 5.客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
- 6.服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据
实际上,JWT的Token就是 JSON数据转换后的一串字符串 JWT主要包括三个部分:header(头部)payload(数据)signature(签名)
- header
header表示头部数据
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT
- payload
Payload 里面是 Token 的具体内容
{
"id": 1,
"username": "wintershii",
"passwd": "",
"admin": true
}
- signature
JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret (私匙),这个相当于是一个密码,这个密码秘密地存储在服务端。
最终我们将这三部分内容用"."分开,并用Base64编码
2.使用Token进行身份验证
作为一个后端为SSM框架的项目,首先现在pom.xml中引入依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.5.0</version>
</dependency>
然后编写Token工具类
public class TokenUtil {
//token有效时间
private static final long EXPIRE_TIME = 15 * 60 * 1000;
private static final String TOKEN_SECRET = "thefirsttoken777";//这里填写私匙
/**
* 颁发签名
* @return
*/
public static String sign(Integer id, String phone) {
try {
// 设置过期时间
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
// 私钥和加密算法
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
// 设置头部信息
Map<String, Object> header = new HashMap<>(2);
header.put("Type", "Jwt");
header.put("alg", "HS256");
// 返回token字符串
return JWT.create()
.withHeader(header)
.withClaim("id",id.toString())//在这里可以放自己需要放的数据
.withClaim("phone",phone)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 检验token是否是正确格式(是否过期,是否可用)
* @param token
* @return
*/
public static boolean verify(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
JWTVerifier verifier = JWT.require(algorithm).build();
DecodedJWT jwt = verifier.verify(token);
return true;
} catch (Exception e){
return false;
}
}
/**
* 从token中获取相关信息
* @param token
* @return
*/
public static String getInfo(String token ,String type){
try {
DecodedJWT jwt = JWT.decode(token);
if (type.equals("phone")) {
return jwt.getClaim("phone").asString();
}
if (type.equals("id")) {
return jwt.getClaim("id").asString();
}
} catch (JWTDecodeException e){
e.printStackTrace();
return null;
}
return null;
}
}
根据上面的TokenUtil,在用户登录后,我们可以签发给该用户Token,并将其添加到响应头(或者在响应体里加也可以),然后下次客户端请求我们的时候就要带着这个Token来请求接口
/**
* 用户登录
* @return
*/
@RequestMapping(value = "/login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse<User> login(String phone, String password, HttpServletResponse response) {
ServerResponse<User> serverResponse = userService.login(phone,password);
if (serverResponse.isSuccess()) {
Integer id = serverResponse.getData().getId();
//生成Token
String token = TokenUtil.sign(id,phone);
if (token != null) {
//添加到响应头中
response.addHeader("token",token);
return serverResponse;
}
}
return ServerResponse.createByErrorMessage("登录失败");
}
那么如何判断请求是否带有Token呢?我们当然可以在每个Controller中都进行判断,但是这也太麻烦了,这时我们想到了过滤器,也就是Spring MVC拦截器.我们把除了注册,登录这些不需要用户登录的接口的其他接口用拦截器拦截,并检测其请求头中是否带有Token信息
/**
* token拦截器
*/
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
response.setCharacterEncoding("utf-8");
String token = request.getHeader("token");
if (token != null){
boolean result = TokenUtil.verify(token);
if(result){
System.out.println("通过拦截器");
return true;
}
}
System.out.println("认证失败");
return false;
}
}<!--配置拦截器-->
<mvc:interceptors>
<!--<bean class="com.ma.interceptor.CustomeInterceptor" />-->
<!--拦截器1-->
<mvc:interceptor>
<!--配置拦截器的作用路径-->
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/user/register.do"/>
<mvc:exclude-mapping path="/user/login.do"/>
<!--定义在<mvc:interceptor>下面的表示匹配指定路径的请求才进行拦截-->
<bean class="com.winter.interceptor.TokenInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
下面是spring mvc配置文件中的拦截器相关注解
<!--配置拦截器-->
<mvc:interceptors>
<!--<bean class="com.ma.interceptor.CustomeInterceptor" />-->
<!--拦截器1-->
<mvc:interceptor>
<!--配置拦截器的作用路径-->
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/user/register.do"/>
<mvc:exclude-mapping path="/user/login.do"/>
<!--定义在<mvc:interceptor>下面的表示匹配指定路径的请求才进行拦截-->
<bean class="com.winter.interceptor.TokenInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
这样我们就可以使用Token进行会话了,但当我们需要进行某些敏感操作(删除,修改信息)时,我们需要在Controller中解析Token中的数据,并将其与要操作的用户数据进行对比,如果不是同一用户的数据,那就拒绝操作,返回失败.
最后,因为JWT如果被窃取,服务器也没有办法去识别,所以为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。