JWT
JSON Web Token(JWT)是一种跨域身份验证解决方案。
最早的时候我写的安全验证都是基于session 的,后来做的一个分布式项目直接用redis做安全验证了。
最近的项目是小程序。
因为小程序本身的关系,用session的话会导致小程序挂起一阵子不理后可能会导致失效,虽然可以改造seasion的时间或者写一些监听事件。但是怪麻烦。
而且本身体积比较小,redis也是要基于时间存储,选型是有的不太舒服。
所以这里我用了jwt,虽然写工具也会有存储时间限制,但是他的本质状态,我觉得才是最适合我这个项目的。
区别
由于http协议是无状态的,每一次请求都无状态。当一个用户通过用户名和密码登录了之后,他的下一个请求不会携带任何状态,应用程序无法知道他的身份,那就必须重新认证。因此我们需要用户登录成功之后的每一次http请求,都能够保存他的登录状态。
这里单独说下他和session的区别
session
- 用户输入其登录信息
- 服务器验证信息是否正确,并创建session,存储
- 服务器为用户生成一个sessionId,将具有sesssionId的Cookie将放置在用户浏览器中
- 在后续请求中,会根据验证sessionID,如果有效,则接受请求
- 一旦用户注销应用程序,会话将在客户端和服务器端都被销毁
jwt
基于token(令牌)的用户认证
- 用户输入其登录信息
- 服务器验证信息是否正确,并返回已签名的token
- token储在客户端,小程序使用StorageSync存储
- 之后的HTTP请求都将token添加到请求头里
- 服务器解码JWT,并且如果令牌有效,则接受请求
- 用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌
jwt组成
jwt的认证原理
一个jwt实际上就是一个字符串,它由三部分组成,头部、载荷与签名,这三个部分都是json格式。
头部(Header)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。
{
"typ": "JWT",
"alg": "HS256"
}
载荷(Payload)
载荷可以用来放信息。
{
"iss": "sharenotes",
"exp": 1412252225,
"iat": 1441593502,
"aud": "MiNiapp",
"sub": "Lets share notes",
}
这里面的前五个字段都是由JWT的标准所定义的。
● it’s: 该JWT的签发者
● sub: 该JWT所面向的用户
● aud: 接收该JWT的一方
● exp(expires): 什么时候过期,这里是一个Unix时间戳
● iat(issued at): 在什么时候签发的
把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号.连接在一起(头部在前),形成新的字符串:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIHNoYXJlTm90ZXMgdG9rZW4iLCJhdWQiOiJNSU5JQVBQIiw
签名
上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密后的内容也是一个字符串,最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分,服务端也就无法通过,在jwt中,消息体是透明的,使用签名可以保证消息不被篡改。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIHNoYXJlTm90ZXMgdG9rZW4iLCJhdWQiOiJNSU5JQVBQIiwiaXNzIjoic2hhcmVOb3RlcyIsImV4cCI6MTU3MjEwMTc1OCwidXNlcklkIjo4LCJpYXQiOjE1NzE0ODk3NTh9.ffma0A0gazhRGD9Mbu_jxTMPd3WrtK59dLdc2hhfhdwj
开始使用
根据上面的知识点后,大概应该知道jwt是啥了
冲
我们以上一个文章中的登录为基础
到Controller下的WxAuthController.java
token String token = UserTokenManager.generateToken(1user.getId());
中。
我们以这里的UserTokenManager开始
先在这里打个断点,等等回回到这里。
开始操作
配置jwt
添加maven依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
创建工具类
JwtHelper.java
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import java.util.*;
public class JwtHelper {
// 秘钥
static final String SECRET = "F-shareNotes-Token";
// 签名是有谁生成
static final String ISSUSER = "shareNotes";
// 签名的主题
static final String SUBJECT = "this is shareNotes token";
// 签名的观众
static final String AUDIENCE = "MINIAPP";
public String createToken(Integer userId){
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map<String, Object> map = new HashMap<String, Object>();
Date nowDate = new Date();
// 过期时间:7天2小时
Date expireDate = getAfterDate(nowDate,0,0,7,2,0,0);
map.put("alg", "HS256");
map.put("typ", "JWT");
String token = JWT.create()
// 设置头部信息 Header
.withHeader(map)
// 设置 载荷 Payload
.withClaim("userId", userId)
.withIssuer(ISSUSER)
.withSubject(SUBJECT)
.withAudience(AUDIENCE)
// 生成签名的时间
.withIssuedAt(nowDate)
// 签名过期的时间
.withExpiresAt(expireDate)
// 签名 Signature
.sign(algorithm);
return token;
} catch (JWTCreationException exception){
exception.printStackTrace();
}
return null;
}
public Integer verifyTokenAndGetUserId(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUSER)
.build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
Claim claim = claims.get("userId");
return claim.asInt();
} catch (JWTVerificationException exception){
// exception.printStackTrace();
}
return 0;
}
public Date getAfterDate(Date date, int year, int month, int day, int hour, int minute, int second){
if(date == null){
date = new Date();
}
Calendar cal = new GregorianCalendar();
cal.setTime(date);
if(year != 0){
cal.add(Calendar.YEAR, year);
}
if(month != 0){
cal.add(Calendar.MONTH, month);
}
if(day != 0){
cal.add(Calendar.DATE, day);
}
if(hour != 0){
cal.add(Calendar.HOUR_OF_DAY, hour);
}
if(minute != 0){
cal.add(Calendar.MINUTE, minute);
}
if(second != 0){
cal.add(Calendar.SECOND, second);
}
return cal.getTime();
}
}
这里的代码分成了createToken,verifyTokenAndGetUserId,getAfterDate
createToken(Integer userId)
这里声明加密方法,还有过期时间,根据我们getAfterDate生成了时间数字
接着是设置了头部。
和我们上面介绍jwt的组成,载荷,签名一样。
重点是
withClaim("userId", userId)
我们这里把需要的userId根据key,value设置了对应的值随便存入了token中。
如果你有其他想要存入的也可以继续加上去。
生成token就这样了。
Integer verifyTokenAndGetUserId(String token)
这里是根据我们从http头部中获取的token了,解析。
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer(ISSUSER)
.build();
DecodedJWT jwt = verifier.verify(token);
Map<String, Claim> claims = jwt.getClaims();
这里就单纯的解密。
注意这一句
Map<String, Claim> claims = jwt.getClaims();
获取claims,也就是我们刚刚注入的kv
Claim claim = claims.get("userId");
return claim.asInt();
返回userId
测试
这里的头就是我们刚刚在createToken 声明的头部。
后面的值就是token值
我们放到代码中
工具类
public class UserTokenManager {
public static String generateToken(Integer id) {
JwtHelper jwtHelper = new JwtHelper();
return jwtHelper.createToken(id);
}
public static Integer getUserId(String token) {
JwtHelper jwtHelper = new JwtHelper();
Integer userId = jwtHelper.verifyTokenAndGetUserId(token);
if(userId == null || userId == 0){
return null;
}
return userId;
}
}
这里就不多做解释了,代码很简单
使用
public static final String LOGIN_TOKEN_KEY = "F-shareNotes-Token";
HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
String token = request.getHeader(LOGIN_TOKEN_KEY);
if (token == null || token.isEmpty()) {
return null;
}
Integer userId = UserTokenManager.getUserId(token);
这里有一个request 封装类。网上可自寻查询。或者可直接用Controller的request 中请求
封装方法到注解中
我们创建一个java LoginUser
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
这里Target声明了他只能使用在方法参数上面。
实现注解使用
创建 新的一个方法实现HandlerMethodArgumentResolver接口,他是在springframwork包中的。
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
同理,实现方法和上面引用一样。
不过我们要先实现两个接口
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
这是是判断变量类型的正确性。
只要是Integer类型,然后是前缀是LoginUser注解。
这里。我们就可以取得刚刚实现的注解。并且给他赋值了。
######## 赋值
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
String token = request.getHeader(LOGIN_TOKEN_KEY);
if (token == null || token.isEmpty()) {
return null;
}
return UserTokenManager.getUserId(token);
}
这里不做说明,就是在上面使用的方法。只是request 是根据NativeWebRequest中,但是request的本质都一样。
其是Spring 中的request方法。无大碍。
####### 具体代码
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
public static final String LOGIN_TOKEN_KEY = "F-shareNotes-Token";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) throws Exception {
String token = request.getHeader(LOGIN_TOKEN_KEY);
if (token == null || token.isEmpty()) {
return null;
}
return UserTokenManager.getUserId(token);
}
}
全局使用
我们可以直接在controller中直接
@GetMapping("/getDetail/{msg_id}")
public Object getAllCategories(@LoginUser Integer userId, @PathVariable("msg_id") Integer msg_id){
取得userId