问题描述
将项目升级为前后端分离的过程中,关于shiro的权限控制遇到了一些问题,这里记录下问题以及解决方法;
使用springboot+shiro前后端分离后出现的跨域问题
在项目进行前后端分离后,前端发送给后端的请求总是报跨域问题
Access to XMLHttpRequest at ‘http://localhost:8081/******’ from origin ‘http://localhost:9090’ has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.
原因是项目前后端分离以后,前端在发送给后端的每个请求中都会在header字段中附上Authorization:*********;来进行权限的验证,这样等于自定义了header字段,导致前端发送的真正请求前都会默认发送OPTIONS请求,来探测服务器。如果返回状态200成功信息,则继续实际的POST/GET正常请求,否则返回跨域问题。
而OPTIONS请求不会带上认证信息,会被项目中的shiro的拦截器拦截下来,导致OPTIONS请求不能返回前端成功,所以导致跨域问题出现。
解决方法
自定义拦截器CorsFilter
@Override
public void init(FilterConfig config) throws ServletException {
this.config = config;
}
@Override
public void destroy() {
this.config = null;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) servletResponse;
HttpServletRequest request = (HttpServletRequest) servletRequest;
response.setHeader( "Access-Control-Allow-Origin", request.getHeader("Origin"));
// 允许请求的方法
response.setHeader( "Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PUT" );
// 多少秒内,不需要再发送预检验请求,可以缓存该结果
response.setHeader( "Access-Control-Max-Age", "3600" );
// 表明它允许跨域请求包含xxx头
response.setHeader( "Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
//是否允许浏览器携带用户身份信息(cookie)
response.setHeader( "Access-Control-Allow-Credentials", "true" );
// response.setHeader( "Access-Control-Expose-Headers", "*" );
if (request.getMethod().equals( "OPTIONS" )) {
response.setStatus( 200 );
return;
}
filterChain.doFilter( servletRequest, response );
所有请求都允许跨域,并且只要是OPTIONS请求直接返回成功,而且要将拦截器CorsFilter的优先级设为最高,这样请求会先到拦截器CorsFilter进行处理,不然会被shiro拦截器拦截掉。
项目改为前后端分离后各种登录、鉴权的状态返回
之前的项目前端都是jsp,各种没有访问权限、跳转登录、登出都是返回一个页面然后跳转就行了,现在前后端分离后,就要返回前端约定的状态码,让前端控制页面的跳转,这样就涉及到重写shiro的各种过滤器
解决方法
如果用户未登录,访问了请求连接,就需要返回前端403状态吗,这里需要重写FormAuthenticationFilter里的onAccessDenied方法
NewFormAuthenticationFilter:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
if (this.isLoginRequest(request, response)) {
if (this.isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return this.executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(new Response(403,"请登录"));
return true;
}
} else {
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the Authentication url [" + this.getLoginUrl() + "]");
}
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(new Response(403,"请登录"));
return false;
}
}
如果用户访问未授权资源,就要返回状态吗401,这是重写PermissionsAuthorizationFilter里的onAccessDenied方法
NewPermissionsAuthorizationFilter:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(new Response(401,"你没有权限访问该资源"));
return false;
}
如果用户登出,就要返回登出成功信息,重写LogoutFilter里的preHandle方法
NewLogoutFilter:
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = this.getSubject(request, response);
String redirectUrl = this.getRedirectUrl(request, response, subject);
try {
subject.logout();
WebUtils.toHttp(response).setContentType("application/json; charset=utf-8");
WebUtils.toHttp(response).getWriter().print(new Response(000000,"成功登出"));
} catch (SessionException var6) {
log.debug("Encountered session exception during logout. This can generally safely be ignored.", var6);
}
return false;
}
重写这三个过滤器时出现的一些问题,当时写好后过滤器注册进项目中后,发现所有连接都被拦截了,就算是标明annon的资源也不例外;最后发现的将这三个拦截器设为单例模式放入的springboot容器中了,这样不仅shiro中有着三个拦截器,spring中也有这些拦截器,而发过来的请求被spring中的拦截器拦截掉了。
这里将拦截器的注册改为如下形式:
Map<String, Filter> filters = factoryBean.getFilters();
filters.put("authc",new NewFormAuthenticationFilter());
filters.put("logout",new NewLogoutFilter());
filters.put("perms",new NewPermissionsAuthorizationFilter());
factoryBean.setFilters(filters);
这样就只注册进shiro里。