一、重写过滤器
shiro自己帮我们封装了一系列过滤器,对于常见的一些操作,比如登陆,登出,认证不通过等,shiro会通过我们配置的url,找到相应过滤器,做完操作会跳转到我们配置的跳转url中。但是对于前后端分离项目,前端通过ajax调用接口,获取返回值展示后再跳。比如展示“暂无权限”等提示语,友好一些。所以要对shiro已有的过滤器进行继承重写。
登出过滤器:
public class UserLogoutFilter extends LogoutFilter {
private Logger logger = LoggerFactory.getLogger(UserLogoutFilter.class);
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
//在这里执行退出系统前需要清空的数据
Subject subject = getSubject(request, response);
try {
subject.logout();
} catch (SessionException ise) {
logger.error("logout session Exception", ise);
}
//给前端返回结果
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization,X-TOKEN,XMLHttpRequest");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
HttpUtil.writeResult(httpServletResponse, RevertResult.ok());
//返回false表示不执行后续的过滤器
return false;
}
}
认证过滤器:
public class UserAccessFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (this.isLoginRequest(request, response)) {
return true;
} else {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,Content-Type,Accept,Authorization,X-TOKEN,XMLHttpRequest");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "*");
HttpUtil.writeResult(response, RevertResult.failResult(ErrorCodes.USER_NO_LOGIN));
//返回false表示不执行后续的过滤器
return false;
}
}
}
shiro配置文件:
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//获取filters
Map filters = shiroFilterFactoryBean.getFilters();
filters.put("userAccessFilter", new UserAccessFilter());
filters.put("logout", new UserLogoutFilter());
// 设置 securityManager
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 登录的 url
shiroFilterFactoryBean.setLoginUrl(loginUrl);
LinkedHashMap filterChainDefinitionMap = new LinkedHashMap<>();
// 设置免认证 url
String[] anonUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(anonUrl, ",");
for (String url : anonUrls) {
filterChainDefinitionMap.put(url, "anon");
}
filterChainDefinitionMap.put(logoutUrl, "logout");
filterChainDefinitionMap.put("/**", "userAccessFilter");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
httpUtil:
public class HttpUtil {
/**
* response输出方法
*
* @param response
* @param revertResult
*/
public static void writeResult(ServletResponse response, RevertResult revertResult) {
//重置response
//设置编码格式
PrintWriter writer = null;
try {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
writer = response.getWriter();
writer.write(JSON.toJSONString(revertResult));
writer.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != writer) {
writer.close();
}
}
}
}
二、跨域问题
由于是前后端分离项目,前后端部署在不同服务器上,不同主域情况下,以上通过response写出的返回值会被拦截。需要在basefilter进行配置。ps:spring拦截器是在shiro过滤器后执行的,所以不能通过拦截器配置。
@Component
@WebFilter(urlPatterns = "/*", filterName = "baseFilter")
public class BaseFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("Filter初始化中");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String headerOrigin = httpServletRequest.getHeader("origin");
if (headerOrigin != null) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", headerOrigin);
}
httpServletResponse.setHeader("Access-Control-Expose-Headers", "page, per_page,status,total_count");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE,PUT");
if (httpServletRequest.getMethod().equals("OPTIONS")) {
httpServletResponse.setStatus(200);
return;
}
//调用该方法后,表示过滤器经过原来的url请求处理方法
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
System.out.println("Filter销毁中");
}
}
Access-Control-Allow-Origin这一项,不可为"*",而是取前端传的主域。
三、全局异常拦截
由于shiro在鉴权不通过时,会直接抛出exception,我们需要封装一层友好的提示返回前端。
@Slf4j
@RestControllerAdvice
@Order(value = Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(value = UnauthorizedException.class)
public RevertResult handleUnauthorizedException(UnauthorizedException e) {
log.error("UnauthorizedException", e);
return RevertResult.failResult(ErrorCodes.NO_ACCESS);
}
/**
* 登录认证异常
*/
@ExceptionHandler({UnauthenticatedException.class, AuthenticationException.class})
public RevertResult authenticationException(Exception e) {
log.error("UnauthenticatedException", e);
return RevertResult.failResult(ErrorCodes.USER_NO_LOGIN);
}
}
四、缓存中间件
shiro默认使用sessionId+本地缓存来做登录状态,但分布式部署时sessionId需要存储在统一中间件中,所以配置redis集成shiro
五、permissionstrings不可为null
在doAuthorization时,存入permissionStrings时要过滤掉数据库里permission为空的菜单。
六、超级坑爹缓存名称不一致
存authenticationCache时,传入的对象是userId。存arthorizationCache时,传入的对象是user对象