????集成方式
项目采用了前后端分离的结构,前端使用的是vue
框架,后端使用的是springboot
框架,登录认证框架是shiro
,同时使用了单点登录。
整个登录认证的逻辑:
- 通过
shiro
判断用户是否登陆了 - 如果没有登录或者登录过期,通过
shiro
的配置跳转到指定的controller
- 在
controller
中重定向到单点登录页面
????问题描述
- 当登录过期了前端先发送一个请求到接口
A
-
shiro
判断了用户已经登录过期了,返回状态码302,并让前端重新请求到接口B
- 在接口
B
,返回状态码302,并让前端重新请求到单点登录地址 - 前端重新请求这个单点登录地址
- 浏览器判断单点登录地址和当前系统地址不是同一个,于是就出现跨域错误
????解决思路
这个问题在网上找了很多解决的办法都是通过解决跨域,比如使用注解@CrossOrigin
,但是都没有生效。
后来思考了下,跨域是重定向和父页面不同的地址出现的,这个时候在后端怎么配置其实已经没有用了。
现在核心问题,当登录失效之后就不返回状态码302,而是直接调用退出登录接口,这样也能达到重新登录的目的。
????源码分析
为了解决这个问题,我们需要先研究一下shiro
的源码如何来判断用户登录过期的,并且返回302重定向到新接口的。
通过上面的时序图可以看出最后调用的是AccesssControlFilter
类的onPreHandle
方法,这个方法调用了两个方法:isAccessAllowed
和onAccessDenied
。
isAccessAllowed
:判断用户是否登录,如果登陆了直接返回了,如果没有登录就会执行第二个方法onAccessDenied
:是否拒绝登录,在这里面就可以自定义一些返回值
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
从这里可以看出,如果我们想自己定义返回值的话就要重写这两个方法。
????解决办法
-
首先定义一个
Filter
,这个Filter
继承AuthorizationFilter
,然后重写了isAccessAllowed
和onAccessDenied
在
onAccessDenied
中Response
就返回401
,前端拿到这个401
就请求退出接口
public class RefreshAuthorizationFilter extends AuthorizationFilter {
private ISecurityService securityService;
public RefreshAuthorizationFilter(ISecurityService securityService) {
this.securityService = securityService;
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return securityService.isLogin();
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
response.getWriter().write("401");
return false;
}
}
- 使用配置将
Filter
加入Filter
链中,并且加上@Primary
注解,这样优先级才会优先执行。
@Primary
@Component
public class RefreshALoginConfig extends ALoginConfig {
@Autowired
private ISecurityService securityService;
@Override
protected void addFilters() {
addFilter("restartRefresh", new RefreshAuthorizationFilter(securityService));
}
protected void initFilterChainDefinitions() {
filter("/api/**", "restartRefresh");
}
}