shiro + jwt 实现 请求头中的 rememberMe 时间限制功能
前言:
上一篇提出, 通过修改 rememberMe 的编码来实现 rememberMe的功能的设想, 事后我去尝试实现了一番, 发现太麻烦, 还是不要那么做吧. 程序还是要越简单越好.
那功能总是要实现的啊, 总不能因为麻烦, 就把功能给砍了吧.
so, 换条路试试:
在前后端项目中, app和后台之间的登录, 并不能通过cookie来维持, 有一种常使用的技术: jwt, 这个技术, 其实就是通过在请求头或者参数中加入一个参数 token (这个token是经过jwt加密的)的方式, 来实现登录认证的.具体的原理并不讨论. jwt加密的时候, 是可以加入时间限制的.
在shiro里面, 如果我将 rememberMe 也进行jwt加密, 然后再赋值回rememberMe , 给出去. 当我从请求头中拿到rememberMe , 然后再解密成之前的数据, 不就可以了么.
实现:
一. jwt帮助类
import ccdc.zykt.model.vo.UserExt; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultClaims; import org.apache.commons.lang.time.DateUtils; import org.apache.shiro.codec.Base64; import java.util.Date; public class JWTUtil { private static final String KEY = Base64.encodeToString("jwt.key".getBytes()); public static String createJWT(String token) { Date now = new Date(); return Jwts.builder() .setSubject(token) .setIssuedAt(now) .setExpiration(DateUtils.addMinutes(now, 1)) .signWith(SignatureAlgorithm.HS512, KEY).compact(); } public static String createJWT(String token, int amount){ Date now = new Date(); return Jwts.builder().setSubject(token).setIssuedAt(now).setExpiration(DateUtils.addHours(now, amount)).signWith(SignatureAlgorithm.HS512, KEY).compact(); } public static boolean validate(String jwt){ try { Jwts.parser().setSigningKey(KEY).parse(jwt); return true; } catch (Throwable t) { return false; } } public static String validateJWT(String jwt) { try { Jwt parse = Jwts.parser().setSigningKey(KEY).parse(jwt); DefaultClaims body = (DefaultClaims) parse.getBody(); String phone = body.getSubject(); return phone; } catch (Throwable t) { return null; } } }
此处我将过期时间设置为1分钟, 在实际使用中, 可以将这个参数变成可配置的, 到时候根据实际需要, 将时间改长一些就可以了.
二. 改写HeaderRememberMeManager类
package ccdc.zykt.web.shiro.headtoken; import ccdc.zykt.web.util.JWTUtil; import com.google.common.base.Strings; import org.apache.commons.compress.utils.ByteUtils; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.codec.Base64; import org.apache.shiro.mgt.AbstractRememberMeManager; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.SubjectContext; import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.subject.WebSubjectContext; import org.apache.shiro.web.util.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.ArrayList; import java.util.List; /** * 将remember放到响应头中去, 然后从请求头中解析 * @author: elvin * @time: 2018-07-05 15:11 * @desc: **/ public class HeaderRememberMeManager extends AbstractRememberMeManager { private static final transient Logger log = LoggerFactory.getLogger(HeaderRememberMeManager.class); // header 中 固定使用的 key public static final String DEFAULT_REMEMBER_ME_HEADER_NAME = "remember-me"; @Override protected void rememberSerializedIdentity(Subject subject, byte[] serialized) { if (!WebUtils.isHttp(subject)) { if (log.isDebugEnabled()) { String msg = "Subject argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to set the rememberMe cookie. Returning immediately and ignoring rememberMe operation."; log.debug(msg); } } else { HttpServletResponse response = WebUtils.getHttpResponse(subject); String base64 = Base64.encodeToString(serialized); base64 = JWTUtil.createJWT(base64); // 设置 rememberMe 信息到 response header 中 response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, base64); } } private boolean isIdentityRemoved(WebSubjectContext subjectContext) { ServletRequest request = subjectContext.resolveServletRequest(); if (request == null) { return false; } else { Boolean removed = (Boolean) request.getAttribute(ShiroHttpServletRequest.IDENTITY_REMOVED_KEY); return removed != null && removed; } } @Override protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { if (!WebUtils.isHttp(subjectContext)) { if (log.isDebugEnabled()) { String msg = "SubjectContext argument is not an HTTP-aware instance. This is required to obtain a servlet request and response in order to retrieve the rememberMe cookie. Returning immediately and ignoring rememberMe operation."; log.debug(msg); } return null; } else { WebSubjectContext wsc = (WebSubjectContext) subjectContext; if (this.isIdentityRemoved(wsc)) { return null; } else { HttpServletRequest request = WebUtils.getHttpRequest(wsc); // 在request header 中获取 rememberMe信息 String base64 = request.getHeader(DEFAULT_REMEMBER_ME_HEADER_NAME); if ("deleteMe".equals(base64)) { return null; } else if (base64 != null) { base64 = JWTUtil.validateJWT(base64); if(Strings.isNullOrEmpty(base64)){ return null; } base64 = this.ensurePadding(base64); if (log.isTraceEnabled()) { log.trace("Acquired Base64 encoded identity [" + base64 + "]"); } byte[] decoded = Base64.decode(base64); if (log.isTraceEnabled()) { log.trace("Base64 decoded byte array length: " + (decoded != null ? decoded.length : 0) + " bytes."); } return decoded; } else { return null; } } } } private String ensurePadding(String base64) { int length = base64.length(); if (length % 4 != 0) { StringBuilder sb = new StringBuilder(base64); for (int i = 0; i < length % 4; ++i) { sb.append('='); } base64 = sb.toString(); } return base64; } @Override protected void forgetIdentity(Subject subject) { if (WebUtils.isHttp(subject)) { HttpServletRequest request = WebUtils.getHttpRequest(subject); HttpServletResponse response = WebUtils.getHttpResponse(subject); this.forgetIdentity(request, response); } } @Override public void forgetIdentity(SubjectContext subjectContext) { if (WebUtils.isHttp(subjectContext)) { HttpServletRequest request = WebUtils.getHttpRequest(subjectContext); HttpServletResponse response = WebUtils.getHttpResponse(subjectContext); this.forgetIdentity(request, response); } } private void forgetIdentity(HttpServletRequest request, HttpServletResponse response) { //设置删除标示 response.setHeader(DEFAULT_REMEMBER_ME_HEADER_NAME, "deleteMe"); } }
rememberMe 在解析的时候, 就会将时间算进去, 如果超时了, 解析会返回false. 这样, 就可以为rememberMe设置一个超时时间
结果展示:
1. 登录之后, 使用postmen 尝试访问
2. 耐心等待1分钟, 然后再去访问这个接口试试
试验证明, 还是可行的.
进行jwt处理之后, rememberMe字符串会比较长, 这里提供一种思路:
1. 将过期时间转换成 yyyy-MM-dd HH:mm:ss 格式的时间字符串, 进行 base64编码, 会发现, 长度都是28位的. 假设定义为变量 timeBase64
2. 在 rememberSerializedIdentity 方法中, 对 timeBase64 进行截取, 分为两段, 拼接到 上面 base64字符串中去, 这中方式就相当于是进行了简单的加密, 当然, 不进行此操作, 也完全可以, 直接拼接到 base64的开始处, 或者结尾处.
3. 在 getRememberedSerializedIdentity 方法中, 对base64字符串进行截取, 可以得到时间字符串, 之后对时间进行判断, 就能知道, 是否过期.