上一个项目采用的是session存储uid的方式来确认API调用方的合法身份,这次的app则打算采用token的方式进行身份校验,此二者的优缺点有很多博客进行了论述,这里就不谈了。
这里对JWT进行简单的分析,并解释token中存储的具体内容。
JWT消息构成
一个token分3部分,按顺序为
- 头部(header)
- 载荷(payload)
- 签名(signature)
三部分之间用“.”号做分隔。例如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
header头部
- typ: 声明类型,这里是jwt
- alg: 声明加密的算法 通常直接使用 HMAC SHA256
(如果有需要的话可以自己实现算法并修改com.auth0.jwt的源码以提高安全性)
payload荷载
载荷就是存放有效信息的地方。jwt提供了一部分预设的属性,也可以向claim中添加自定义属性。
其中标准提供的数据为
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 自定义属性存放在claim中
体现在代码中:
token= JWT.create()
.withClaim("userId",user.getUserId()) //自定义属性
.withExpiresAt(DateUtil.rollMon(new Date(),1)) //一个月后过期
.sign(Algorithm.HMAC256(SECRET)); //指定加密算法并传入密钥
签名signature
jwt的第三部分是一个签证信息,这个签证信息算法如下:
-
头部、荷载分别进行base64加密后,以”.”连接获得content
-
按照指定的加密算法(是携带密钥的)将content进行加密得到signatureBytes
-
signatureBytes再进行base64加密 获得signature
-
将content和signature以”.”连接获得token。
分析一下:
token的三段明文是可以直接base64解密的,如:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 .eyJhdWQiOiIxMjMiLCJwYXNzd29yZCI6IjEyMyIsInVzZXJJZCI6MjMzfQ .0Piz_kzhriwwcoHI7o5A7evyeDMDGBXStK5PxTtmadQ
第一段解密可知第三段的加密算法类型。
第二段解密可知所有默认字段以及自定义字段,所以这里绝对不能明文传输一些敏感信息。
第三段解密后将得到signatureBytes。
如果不知道密钥,是无法通过header中指定的加密算法将token前两段映射到第三段的。
也就是说,如果不知道这个密钥,那么即使截获了token,得知前两段的内容,都没有办法伪造出能够通过服务器token校验的signature,JWT就是通过这一特性来确保签名的有效性。
如果校验通过了,说明这个签名确实是我们的服务器下发给客户端的,携带该token的用户具有合法的身份,根据payload中的信息,就能确认他的登陆身份。所以我们只需要在token的payload中放置一个userId即可(必要的话还要添加一个过期时间)。
下面上代码
依赖:`
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>`
创建token的工具类
public class TokenUtil {
public static String SECRET="你的密钥";
public static String getToken(User user) {
String token="";
token= JWT.create()
.withClaim("userId",user.getUserId()) //自定义属性
.withExpiresAt(DateUtil.rollMon(new Date(),1)) //一个月后过期
.sign(Algorithm.HMAC256(SECRET)); //指定加密算法并传入密钥
return token;
}
}
自定义注解—跳过token验证
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
自定义注解—需要token验证
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
拦截器
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.poorteam.poorguy.webservice.common.anotations.PassToken;
import com.poorteam.poorguy.webservice.common.anotations.UserLoginToken;
import com.poorteam.poorguy.webservice.repository.datasourse1.UserMapper;
import com.poorteam.poorguy.webservice.utils.TokenUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
public class AuthenticationInterceptor implements HandlerInterceptor {
@Autowired
UserMapper userMapper;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
String token = httpServletRequest.getHeader("token");// 从 http 请求头中取出 token
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//检查有没有需要用户权限的注解
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
// 执行认证
if (token == null) {
throw new RuntimeException("无token,请重新登录");
}
// 获取 token 中的 user id
String userId;
try {
userId = JWT.decode(token).getClaim("userId").asString();
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
if(userId==null||userId.length()==0){
throw new RuntimeException("userId为空");
}
// 验证 token
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TokenUtil.SECRET)).build();
try {
jwtVerifier.verify(token); //这里一旦出现异常就说明校验不通过
//把uid放到request中去
httpServletRequest.setAttribute("userId",Long.valueOf(userId));
} catch (JWTVerificationException e) {
throw new RuntimeException("401");
}
return true;
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
配置拦截器
/**
* 配置拦截器
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public AuthenticationInterceptor authenticationInterceptor() {
return new AuthenticationInterceptor();
}
}
在controller层根据需求给接口添加注解
@PassToken //注册接口 跳过token验证
@RequestMapping(value = "sendRegisterValidateCode", method = RequestMethod.POST)
BaseResponse sendRegisterValidateCode(@RequestBody JSONObject jsonObject) {
//获取参数
String phone = jsonObject.getString("phone"); //手机号
//校验参数
if(!phone.matches("^[1](([3][0-9])|([4][5-9])|([5][0-3,5-9])|([6][5,6])|([7][0-8])|([8][0-9])|([9][1,8,9]))[0-9]{8}")){
return BaseResponse.createByErrorMessage("手机号无效");
}
return loginServiceImpl.sendRegisterValidateCode(phone);
}
收工!