既然要做,就做的细致一点,对得起自己!
为什么使用token,请看这里
解决思路:
1.增加一个过滤器,对每一个请求都进行token的校验。如果为登录请求,则可以放过。
2.将过滤器设置进shiro中。
第一步:实现自定义ShiroFilter.这里选择继承FormAuthenticationFilter,至于为什么不继承AuthenticatingFilter,后面会解释。
从当前shiro中取出User信息,进行加密,将加密结果和客户端传过来的token比较,来判断token的合法性、有效性。
public class ShiroFilter extends FormAuthenticationFilter {
//加密的字符串,相当于签名
private static final String SINGNATURE_TOKEN = "加密token";
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
//这里只有返回false才会执行onAccessDenied方法,因为
// return super.isAccessAllowed(request, response, mappedValue);
return false;
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
String token = getRequestToken((HttpServletRequest) request);
String login = ((HttpServletRequest) request).getServletPath();
//如果为登录,就放行
if ("/user/login".equals(login)){
return true;
}
if (StringUtils.isBlank(token)){
System.out.println("没有token");
return false;
}
//从当前shiro中获得用户信息
User user = (User) SecurityUtils.getSubject().getPrincipal();
//对当前ID进行SHA256加密
String encryptionKey= DigestUtils.sha256Hex(SINGNATURE_TOKEN+user.getName());
if (encryptionKey.equals(token)){
return true;
}else{
System.out.println("无效token");
}
return false;
}
private String getRequestToken(HttpServletRequest request){
//默认从请求头中获得token
String token = request.getHeader("token");
//如果header中不存在token,则从参数中获取token
if(StringUtils.isBlank(token)){
token = request.getParameter("token");
}
return token;
}
}
说明:
1.为什么这里重写isAccessAllowed要直接返回false?查看源码你会发现,只有shiro中Filter最终会执行AccessControlFilter这个类下面的onPreHandle这个方法,而这个方法就一句代码,如下:
我们所有的token校验方法都是在onAccessDenied里进行的,而且这里还是一个或判断,一旦isAccessAllowed为 true,那就不会执行onAccessDenied,稍微懂点与或非的知识就能理解吧。
2.为什么要把主要token验证方法写在onAccessDenied里,而不是写在isAccessAllowed,isAccessAllowed也是可以返回false/true啊?上面的截图源码已经很清楚了,如果在isAccessAllowed里校验false/true,在token校验失败之后,依然还是会执行onAccessDenied这个方法,这个方法如下图:
可以看到,如果所有token验证失败,我们直接把结果返回给前台就可以了,没有必要再进行一次无效的验证。同时,在isAccessAllowed方法里,shiro做的判断很粗,他只是判断了一下,用户是否已经登录,而通过之后的所有请求都是合法的,这显然不符合我们的要求。源码如下:
登录之后,这里标红的地方就会为true。所以基于以上两点,我们的代码是第一步那样写。
第二步:当用户登录成功之后,也就是subkect.login(token)校验通过之后,我们会把token返回给客户端,以便于下次请求时进行token的验证。
try{
subject.login(token);
//返回的token
String encryptionKey= DigestUtils.sha256Hex(SINGNATURE_TOKEN+user.getName());
}catch (Exception e){
System.out.println("对具体异常处理");
}
第三步:让shiro执行我们自定义的过滤器。
除了不需要认证权限的操作,统一都由自定义的ShiroFilter去处理。
以上三步完成之后,就可以对所有需要鉴权的请求进行处理。省去了服务器内存和数据库存储和查询的消耗。
备注:以上实现加密方式是基于Apache.common.codec下的sha256加密的方法加密的,但是没有对应解密的方法,所以说,上篇文章提到的时间戳鉴定token过期时间就实现不了,目前上面的代码是没有加过期限制的。解决方法也很简单,你只需要使用Base64加密算法就可以,这是可以解密的,可以将时间戳加密到token里,然后再解密,校验token是否过期。
思考:在分布式系统中,如何保证多个服务之间可以使用一份token?自己简单的想法是这样的,可以参考 推送/订阅 这种方式可以有一个单独的token的认证服务器,负责token的创建、销毁、校验等。一旦token合法则把token返回给客户端,然后客户端拿着这个token去请求不同的服务A,B,C等。
不过好像有一个JWT提供的token也很厉害,目前还没有研究,懂的大神可以留言教下我,感激不尽!