问题
今天在测试代码的时候突然报了这个异常:
原因
当用户处于未登录状态,向一个需要登录的接口发送GET请求时,shiro自带的拦截器会将这个请求原封不动地重定向到设置地登录接口。而由于登录接口是PostMapping,所以报了这个异常。
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//anon表示公共资源
map.put("/user/register","anon");//anon表示公共资源
map.put("/**","authc");//authc表示请求这个资源需要认证和授权
shiroFilterFactoryBean.setLoginUrl("/user/login");//设置重定向的登录接口
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);//设置过滤链
由于shiro是jsp时代(前后端不分离时代)的产物,因此这个拦截器的设计初衷是重定向页面而非重定向接口,在这里将其重定向到接口URL显然是不合适的。
这会形成两个问题:
- 重定向的两个接口所能接收的方法可能是不同的。
- 即使相同,前端也无法对未认证或未授权的页面做出处理。
下面先复习一下Shiro的几个默认拦截器再说怎么解决这个问题。
有关认证的拦截器:
拦截器简称 | 对应的默认拦截器类 | 说明 |
authc | FormAuthenticationFilter | 需要认证才可以访问 主要属性: loginUrl:登录的url,默认login.jsp,如果被这个过滤器拦截后,会重定向这个url successUrl:登录成功后重定向的url |
user | UserFilter | 需要认证或记住我才可以访问 |
logout | LogoutFilter | 退出url 主要属性: redirectUrl:退出后重定向的url,默认”/“ 如设置”/logout=logout”后,当访问/logout会被拦截,执行退出,再重定向到redirectUrl |
anon | AnonymousFilter | 不拦截,总会放行 |
有关授权的拦截器:
拦截器简称 | 对应的默认拦截器类 | 说明 |
roles | RolesAuthorizationFilter | 验证当前用户是否拥有所有角色 主要属性: unauthorizedUrl:未授权重定向的url loginUrl:登录页面的url 如配置”/admin/**=roles[user,admin]”则访问/admin/时,如果该用户没有同事拥有user和admin两种角色,则重定向到unauthorizedUrl |
perms | PermissionsAuthorizationFilter | 验证当前用户是否拥有所有权限,主要属性和roles一致 |
解决方法
首先我们来看一下这几个拦截器的类关系图(为了直观,省略了一部分)
我们可以看到几个需要权限或认证的拦截器都有一个共同父类AccessControlFilter,这个类中有一个方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}
过滤的原则由两个决定,isAccessAllowed即判断请求的url是否合法,onAccessDenied即对不合法的请求的拦截后执行的逻辑。我们想要更改的是令shiro拦截后返回json,所以仅需改onAccessDenied即可。
先看一下onAccessDenied方法的源码:
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 先判断访问的是否是登录页面,如果是则放行
if (isLoginRequest(request, response)) {
if (isLoginSubmission(request, response)) {
if (log.isTraceEnabled()) {
log.trace("Login submission detected. Attempting to execute login.");
}
return executeLogin(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Login page view.");
}
//allow them to see the login page ;)
return true;
}
} else {
// 若不是
if (log.isTraceEnabled()) {
log.trace("Attempting to access a path which requires authentication. Forwarding to the " +
"Authentication url [" + getLoginUrl() + "]");
}
//重定向
saveRequestAndRedirectToLogin(request, response);
return false;
}
}
可见,shiro对authc的url拦截后,首先判断它是否为loginUrl,若是则放行,若不是则重定向到loginUrl。当前后端分离后,shiro的loginUrl像是形同虚设,因此将这个方法直接改为返回json。
由于这里我们设置的拦截器是authc,因此只需要编写一个类来继承authc对应的FormAuthenticationFilter类,并重写其中的onAccessDenied方法,然后在shiro的配置类中配置上我们刚刚写重写的拦截器就可以了。
继承过滤器并重写过滤方法
重写的onAccessDenied方法:
package com.example.demo.filter;
import com.example.demo.util.HttpResponse;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @author: 風楪fy
* @create: 2021-07-19 00:54
**/
public class ShiroFormAuthenticationFilter extends FormAuthenticationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json; charset=utf-8");
PrintWriter out = resp.getWriter();
//HttpResponse httpResponse= HttpResponse.failure("未登录",4011);
//在这里写要返回的json
//out.write(httpResponse.toString());
out.flush();
out.close();
return false;
}
}
添加自定义过滤器配置
在shiro的配置类中,添加以下配置,使用自定义的拦截器对authc进行拦截
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//anon表示公共资源
map.put("/user/register","anon");//anon表示公共资源
map.put("/**","authc");//authc表示请求这个资源需要认证和授权
//设置过滤链
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//使用自定义的过滤器
Map<String, Filter> customizeMap = new HashMap<>();
customizeMap.put("authc", new ShiroFormAuthenticationFilter());
//添加自定义的过滤器
shiroFilterFactoryBean.setFilters(customizeMap);
拓展:自定义roles(角色授权拦截器)
为了让授权过滤器更加健全完备,我们也需要自定义拦截方案,重写其中的一些方法。
继承过滤器并重写过滤方法
package com.example.demo.filter;
import com.example.demo.util.HttpResponse;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.RolesAuthorizationFilter;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author: 風楪fy
* @create: 2021-07-19 02:26
**/
public class ShiroRolesAuthorizationFilter extends RolesAuthorizationFilter {
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletResponse resp = (HttpServletResponse) response;
resp.setContentType("application/json; charset=utf-8");
PrintWriter out = resp.getWriter();
Subject subject = getSubject(request, response);
HttpResponse httpResponse = null;
// 没有认证,先返回未认证的json
if (subject.getPrincipal() == null) {
httpResponse = HttpResponse.failure("未登录", 4011);
} else {
// 已认证但没有角色,返回为授权的json
httpResponse = HttpResponse.failure("权限错误", 4003);
}
out.write(httpResponse.toString());
out.flush();
out.close();
return false;
}
}
添加自定义过滤器配置
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
Map<String,String> map = new HashMap<>();
map.put("/user/login","anon");//anon表示公共资源
map.put("/user/register","anon");//anon表示公共资源
map.put("/**","authc");//authc表示请求这个资源需要认证和授权
//设置过滤链
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
//使用自定义的过滤器
Map<String, Filter> customizeMap = new HashMap<>();
customizeMap.put("authc", new ShiroFormAuthenticationFilter());
customizeMap.put("roles", new ShiroRolesAuthorizationFilter());
//添加自定义的过滤器
shiroFilterFactoryBean.setFilters(customizeMap);
解决。
最后我想吐槽一下,shiro这个框架有点落后啊,竟然没有为前后端分离额外写一套拦截器吗?(还是说我自己不知道)
附注: servlet的两种跳转方式:forward转发、redirect重定向 两者的区别: 1.地址栏 1)forward是服务器内部的跳转,服务器直接访问目标地址,客户端不知情,因此浏览器的网址不发生变化 2)redirect是服务器根据逻辑,发送一个状态码,告诉浏览器重新去请求另一个地址,所以地址栏显示新的地址 2.数据共享 forward在整个跳转过程中用的是同一个request,forward会将request的信息带到被跳转的jsp或者是servlet中使用,所以数据是共享的。而redirect是新的request,所以数据不共享。 3.运用 1) forward一般用于用户登录的时候,根据角色转发到相应的模块 2) redirect一般用于用户注销登录时返回主页或者跳转到其他网站 4.效率 forward效率高,redirect效率低 5.本质 forward转发时服务器上的行为,而redirect重定向是客户端的行为 6.请求次数 forward 一次,redirect两次
参考文章:
感谢前辈们的总结