JSON Web Token(JWT)是目前最流行的基于token的跨域身份验证解决方案。
在JWT极高的安全特性下保证了token的不可伪造和不可篡改,从而可实现易扩展、轻存储、高安全的无状态、分布式的Web应用授权。
本质上JWT是一个独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以及可以存储任何其他信息(自包含)。任何人都可以轻松读取和解析,并使用密钥来验证真实性。
JWT的特点:
JWT包含三个由点分隔的部分,它们分别是:
Header(头部)
Header通常由两部分组成:令牌的类型,即JWT。和用于签名加密的算法名称,如HMAC SHA256或RSA。Header部分的JSON被Base64Url编码,形成JWT的第一部分
{
“alg”: “HS256”,
“typ”: “JWT”
}
Payload(载荷)
Payload中放传输的信息,用户信息等用于传输的信息。Playload部分的JSON被Base64Url编码,形成JWT的第二部分。
{
“iss”: “发送人”,
“sub”: 主题”,
“aud”: “接受人”,
“exp”:”过期时间”,
“iat”:”签发时间”
“id”:”用户id”,
“name”:”用户姓名”
}
Signature(签名)
Signature用来验证发送请求者身份,由前两部分加密形成。要创建签名部分,您必须采用编码标头,编码有效载荷,秘钥,标头中指定的算法并签名。Signature部分的JSON被Base64Url编码,形成JWT的第三部分。
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
其中secret时加密用的密钥
最后:将三者经过Base64Url编码后的内容通过点分隔后合成一个字符串放入token,并将token放入cookies中传给前端保存,服务器只要保存统一的密钥用于解密就可以。
/**
* 登录相关
*
* @author 杨强光
* @version LoginController.java, v0.1 2019年3月01日 下午9:17
*/
public String login(HttpServletRequest request, HttpServletResponse response,
@RequestParam("username") String username,
@RequestParam("password") String password) {
logger.info("[LoginController.login] staff login, username:{}", username);
StaffEntity staffEntity = staffService.fetchStaffByUsername(username, null);
//空处理
if (username == null || "".equals(username) ) {
String message = ErrorMessageFormat.getMessage("not.exist.staff", new String[] {username});
throw new NotExistException(message);
}
if (staffEntity == null || StaffInfo.StaffStatus.DELETED.getStatus().equals(staffEntity.getIsDeleted()) ) {
String message = ErrorMessageFormat.getMessage("not.exist.staff", new String[] {username});
throw new NotExistException(message);
}
String pw = staffEntity.getPassword();
boolean validatePassword;
try {
password = Utils.EncoderByMd5(password);
validatePassword = PBKDF2WithHmacSHA1.validatePassword(password, pw);
} catch (Exception e) {
logger.error(String.format("[LoginController.login] validate password failed, username:%s", username), e);
throw new CloudDeskServerException();
}
if (!validatePassword) {
logger.error(String.format("[LoginController.login] password is not correct, username:%s", username));
throw new LoginPwdException();
}
//初始化token
JwtToken jwtToken = new JwtToken();
//设置payload
Map payload = new HashMap();
payload.put(Constant.PAYLOAD_UID_KEY, staffEntity.getStaffId());
payload.put(Constant.PATH_VARIABLE_KEY, staffEntity.getTenantId());
jwtToken.setPayload(payload);
//设置过期时间
long expireMillis = DateUtils.getGMTTimeInMillis() + Constant.JWT_TOKEN_EXPIRE_TIME;
jwtToken.setExpire(DateUtils.gmtTimeToDate(expireMillis));
//加密token
String token = Utils.encodeSignatureToken(jwtToken, cookieEncryptKey);
//将token存入cookie
CookieUtils.saveCookie(request, response, Constant.COOKIE_STAFF_TOKEN_KEY, token);
return ReturnJacksonUtils.resultOk();
}
public static String encodeSignatureToken(JwtToken jwtToken, String secret) {
JWTCreator.Builder builder = JWT.create().withHeader(new HashMap());
for (Map.Entry entry : jwtToken.getPayload().entrySet()) {
builder.withClaim(entry.getKey(), entry.getValue());
}
try {
return builder.withExpiresAt(jwtToken.getExpire()).sign(Algorithm.HMAC256(secret.getBytes("UTF-8")));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new CloudDeskException("400", "Token Expired", "200");
}
}
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (handler instanceof ResourceHttpRequestHandler) {
return true;
}
HandlerMethod handleMethod = (HandlerMethod)handler;
StaffLoginRequired staffLoginRequired = handleMethod.getMethodAnnotation(StaffLoginRequired.class);
if (staffLoginRequired == null) {
return Boolean.TRUE;
}
String staffUId = null;
try {
//拦截器验证JWT
staffUId = uIdService.getStaffUid(request, response);
} catch (Exception e) {
logger.error("getStaffUid, access path " + request.getRequestURI());
}
hostHolder.setUid(staffUId);
org.slf4j.MDC.put("UID", hostHolder.getUid());
return Boolean.TRUE;
}
public String getStaffUid(HttpServletRequest request, HttpServletResponse response) {
String variableUrl = "modify-password";
//获取租户id
Map pathVariables = (Map)request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (pathVariables == null || pathVariables.get(Constant.PATH_VARIABLE_KEY) == null) {
logger.error("[StaffLoginRequired.preHandle] tenantId is not exist in path.");
throw new MissingParameter("tenantId");
}
String tenantId = (String)pathVariables.get(Constant.PATH_VARIABLE_KEY);
String message = null;
String token = CookieUtils.getCookie(request, Constant.COOKIE_STAFF_TOKEN_KEY);
if (token == null) {
logger.error("[StaffLoginRequired.preHandle] token is not exist in cookie.");
message = ErrorMessageFormat.getMessage("not.exist.token", null);
throw new NotExistException(message);
}
//解密token
JwtToken jwtToken = Utils.decodeSignatureToken(token, cookieEncryptKey);
Map payload = jwtToken.getPayload();
if (!tenantId.equals(payload.get(Constant.PATH_VARIABLE_KEY))) {
throw new InvalidParameter(Constant.PATH_VARIABLE_KEY);
}
//验证有效期
long expire = jwtToken.getExpire().getTime();
long now = DateUtils.getGMTTimeInMillis();
if (now > expire) {
logger.error("[StaffLoginRequired.preHandle] token expire.");
message = ErrorMessageFormat.getMessage("not.exist.token", null);
throw new NotExistException(message);
} else if (now + Constant.JWT_TOKEN_REFRESH_TIME > expire) {
//刷新token有效期
expire += Constant.JWT_TOKEN_REFRESH_TIME;
jwtToken.setExpire(DateUtils.gmtTimeToDate(expire));
token = Utils.encodeSignatureToken(jwtToken, cookieEncryptKey);
CookieUtils.saveCookie(request, response, Constant.COOKIE_STAFF_TOKEN_KEY, token);
}
//判断登陆者是否存在
String uid = payload.get(Constant.PAYLOAD_UID_KEY);
StaffInfo staffInfo = staffService.fetchStaffInfo(tenantId, uid);
if (staffInfo == null) {
logger.error("[StaffLoginRequired.preHandle] staff is not exist, staffId:{}, tenantId:{}", uid, tenantId);
message = ErrorMessageFormat.getMessage("not.exist.staff", null);
throw new NotExistException(message);
}
if (!checkModifyPassword || request.getRequestURI().contains(variableUrl)
|| staffInfo.getIsModifyPassword() == PasswordIsModify.YES.getTarget()) {
return uid;
} else {
logger.error("[StaffLoginRequired.preHandle] staff password is not modified, staffId:{}, tenantId:{}",
uid, tenantId);
throw new PasswordNotModifiedException();
}
}