前言
shiro整合JWT系列,主要记录核心思路–如何在shiro+redis整合JWTToken。
上一篇中,主要讲如何在shiro框架中配置Jwt,以及token执行的流程。
该篇主要梳理整个代码的执行流程。
ps:本文主要以记录核心思路为主,以下都是个人理解,有问题请各位大佬指导一下。
根据token的流程,请求主要分为2个情况:
- 登录获取token
- 携带token请求
1、登录获取token
在shiro+redis+jwt中,Controller层的登录接口不用执行subject.login(token)
方法(没有用到Realm的doCredentialsMatch
验证,可以参照【Shiro】SimpleAuthenticationInfo如何验证password),而是在Controller层手写了校验方法。
- 逻辑如下:
@RequestMapping(value = "/login", method = RequestMethod.POST)
public xxx返回类 login(@RequestBody SysUser user) throws Exception{
// 1、获取前端请求的 用户名和密码
// 2、校验用户是否有效
2.1 根据用户名查询数据库,得到用户数据库信息BDUser(用户名,加密密码,盐值...)
2.2 判断有效性(用户是否存在,是否注销,是否被删除)
// 3、校验用户名或密码是否正确
3.1 使用(前端传来的用户名,密码,DBUser.盐值)进行加密计算,得到加密密码
3.2 判断是否正确,加密密码.equals(DBUser.getPassWord())
// 4、生成token,并将token存入redis缓存中
// 5、设置xxx返回类对象(主要把token返回去)
}
从上面逻辑可以看出,这个登录接口主要就给前端返回了一个token,并将其存入redis中。
这个登录接口,并没有真正进行登录操作subject.login(token)
,因此也没执行到Realm中的认证。
2、携带token请求
这里主要以流程介绍为主,通过简单的源码说明携带token请求的调用过程。通过【shiro】shiro整合JWT——1.需要创建的类 中的JwtFilter,我们可以看到,主要重写了preHandle
、isAccessAllowed
、executeLogin
、onAccessDenied
这4个方法(该例子为简单实现)。
在JwtFilter中,这4个方法主要的执行顺序是
preHandle
-> isAccessAllowed
-> executeLogin
-> onAccessDenied
2.1 preHandle
携带token的请求,先会被ShiroConfig中的filterChainDefinitionMap.put("/**", "jwt");
给拦截,然后执行到我们定义的JwtFilter中;在JwtFilter中,会根据Filter中具体的doFilter
方法实现(这里不细说,后续在细讲)。
/**
* 对跨域提供支持
* 过滤器链中拦截请求,判断是否为跨域请求
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 判断请求方式是否为跨域请求
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", httpServletRequest.getHeader("Access-Control-Request-Method"));
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
return false;
}
// 继续执行过滤器链
return super.preHandle(request, response);
}
preHandle
方法在最后return了super.preHandle(request, response);
,点进去查看,可以发现它重写PathMatchingFilter中的preHandle
,然后再回调回了PathMatchingFilter中的preHandle
。(看上去有点绕,看下图)
总结:感觉就是PathMatchingFilter类是主线流程,(重写的)preHandle方法B就是在preHandle方法A中加入我们自定义的内容,然后再继续执行preHandle方法A原有的内容。
顺着源码继续往下梳理流程,先看preHandle的代码,发现执行了isFilterChainContinued
方法,里面又执行了onPreHandle
方法。
- 疑惑:这个重写的是哪个方法?
回答:我们顺着JwtFilter的继承方法网上查找,如下图;可以发现,是由AccessControlFilter类重写的。
点进去AccessControlFilter类可以看到我们重写的方法isAccessAllowed
和onAccessDenied
,中间的关系是具有 先后顺序的或。
2.2 isAccessAllowed
/**
* 执行登录认证
*
* @param request
* @param response
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
executeLogin(request, response);
return true;
} catch (Exception e) {
throw new AuthenticationException("Token失效请重新登录");
}
}
里面执行了executeLogin
方法
2.3 executeLogin
/**
* 执行登录
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader("ACCESS_TOKEN");
JwtToken jwtToken = new JwtToken(token);
// 提交给realm进行登入,如果错误他会抛出异常并被捕获
// 这里真正实现shiro的登录,getSubject(request, response)直接SecurityUtils.getSubject()也一样
getSubject(request, response).login(jwtToken);
// 如果没有抛出异常则代表登入成功,返回true
return true;
}
对于getSubject(request, response).login(jwtToken);
有疑惑的可以看下【shiro】subject.login(token)流程源码分析
PS:以防万一,遗忘却又没有看,这里根据总结图做下提示,执行getSubject(request, response).login(jwtToken)
方法后,就会执行到Realm的认证方法。
2.4 onAccessDenied
根据源码,我们知道isAccessAllowed
执行返回为True的话,就不会执行onAccessDenied
(也就意味着登录成功,走向realm的两个方法);如果执行返回为False的话,就会执行onAccessDenied
,用来处理登录失败的情况。
/**
* 如果没有登录,直接返回401未授权提示
*
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
httpResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpResponse.setCharacterEncoding("UTF-8");
httpResponse.setContentType("application/json; charset=utf-8");
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
try {
// 返回到前端信息
httpResponse.getWriter().write("未登录或登录失效,请重新登录!");
} catch (IOException e) {
e.printStackTrace();
}
return false;
}