自定义拦截器
- 自定义拦截器可以扩展一些功能,如,从Subject获取用户身份信息绑定到Request、验证码验证、在线用户信息的保存等。
扩展OncePerRequestFilter
- OncePerRequestFilter保证请求只调用一次doFilterInternal()。
public class MyOncePerRequestFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws ServletException, IOException {
System.out.println("once per request filter");
chain.doFilter(request, response);
}
}
- shiro.ini: 指定拦截器为自定义拦截器
[main]
;注册Filter
myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter
;或在这里注册
#[filters]
#myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter
;然后在urls配置url与filter的映射关系即可
[urls]
;访问任意url,后台调用myFilter1拦截器,该拦截器在控制台打印一行字
/**=myFilter1
扩展AdviceFilter
- 提供了aop的功能。其中preHandle()返回false将中断后续拦截器链的执行。
public class MyAdviceFilter extends AdviceFilter {
//根据返回值决定是否继续处理,true:继续过滤过滤链,可以通过它实现权限控制。
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response)
throws Exception {
System.out.println("====预处理/前置处理====");
//返回false将中断后续拦截器链的执行
return true;
}
//执行完过滤器后正常返回则调用该方法。
@Override
protected void postHandle(ServletRequest request, ServletResponse response)
throws Exception {
System.out.println("====后处理/后置返回处理====");
}
//不过最后有没有异常都会执行,完成如清理资源等功能。
@Override
public void afterCompletion(ServletRequest request, ServletResponse response,
Exception exception) throws Exception {
System.out.println("====完成处理/后置最终处理====");
}
}
- shiro.ini
[filters]
myFilter1=com.haien.shiroChapter8.web.filter.MyOncePerRequestFilter
myFilter2=com.haien.shiroChapter8.web.filter.MyAdviceFilter
[urls]
/**=myFilter1,myFilter2
- 运行结果
扩展PathMatchingFilter
- 继承了AdviceFilter,提供了url模式过滤的功能,适用于需要对指定请求进行处理的情况。
public class MyPathMatchingFilter extends PathMatchingFilter {
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));
return true;
}
}
- preHandle:将当前url与已有url进行匹配,成功则调用onPreHandle();否则直接返回true。
onPreHandle: 被调用后获取ini配置文件中为拦截器配置的参数,默认什么都不处理直接返回true。
shiro.ini
[filters]
myFilter3=com.github.zhangkaitao.shiro.chapter8.web.filter.MyPathMatchingFilter
[urls]
/**= myFilter3[config]
config:拦截器的参数,指定权限或角色,多个参数用逗号分隔。onPreHandle使用mappedVaule接收该参数。
打印结果:url matches,config is null
扩展AccessControllerFilter
- 继承了PathMatchingFilter,扩展了两个方法:
public boolean onPreHandle(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue)
|| onAccessDenied(request, response, mappedValue);
}
- isAccessAllowed:是否允许访问,返回true表示允许。
- onAccessDenied:拒绝访问时是否需要继续处理,返回true表示是,则返回上一级调用链,继续过滤链执行,false不用继续处理了,已在方法内处理好了(如重定向到另一个页面,也就不能继续执行过滤链)。
- 这俩方法的实现有两种模型:
- isAccessAllowed直接返回false,然后校验逻辑全在onAccessDenied实现。会在需要权限的方法执行前调用这俩,比如判断用户是否已登录,未登录则重定向至登录页面,正在登录则调用以下login()接收表单验证,已登录则继续过滤链,最终访问到目标方法。
- isAccessAllowed实现校验逻辑,onAccessDenied做校验失败处理,如,把失败信息存入request。代码示例:ideaProjects/shiro-chapter22/jcaptcha/JCaptchaValidateFilter
- 自定义AccessControllerFilter:
/**
* @Author haien
* @Description 以下两个方法同时被onPreHandle()调用
* @Date 2019/3/1
**/
public class MyAccessControlFilter extends AccessControlFilter {
/**
* @Author haien
* @Description 是否允许访问,true表示允许
* @Date 2019/3/1
* @Param [request, response, mappedValue]
* @return boolean
**/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
System.out.println("access allowed");
return true;
}
/**
* @Author haien
* @Description 拒绝访问时是否自己处理,返回true表示自己不处理且继续执行拦截器,
* false表示自己已经处理了(如重定向到另一页面)
* @Date 2019/3/1
* @Param [request, response]
* @return boolean
**/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//访问未被拒绝的话不会执行该方法
System.out.println("访问拒绝也不自己处理,继续拦截器链的执行");
return true;
/*or
...重定向到另一页面...
return false;
*/
}
}
- shiro.ini:
[filters]
myFilter4=com.haien.shiroChapter8.web.filter.MyAccessControlFilter
[urls]
/**=myFilter4
- 打印结果:access allowed
基于表单的拦截器
- 之前我们已经接触过Shiro内置的基于表单的拦截器了,具体事例参见笔记:Shiro第七章-与web集成。其实就是截获表单、封装成token传给Subject.login(token)执行登录而已。
- 现在我们自定义一个:
public class FormLoginFilter extends PathMatchingFilter {
private String loginUrl = "/login.jsp";
private String successUrl = "/";
/**
* 总方法
*/
@Override
protected boolean onPreHandle(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
//1、判断是否已登录且非登录请求,是则直接进入过滤链下一步
if(SecurityUtils.getSubject().isAuthenticated() && !isLoginRequest(req)) {
return true;//已经登录过且非登录请求
}
//2、未登录则判断是否为登录请求,是,则若是get请求,继续过滤链(跳转登录
//页面),若是post请求,认为是表单验证请求,进行表单验证,执行subject.login();
//否,若是get方法的其他页面请求则保存当前请求并重定向到登录页面,非get请求可能报错吧
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if(isLoginRequest(req)) { //是登录请求
if("post".equalsIgnoreCase(req.getMethod())) { //form表单提交
boolean loginSuccess = login(req); //登录
if(loginSuccess) { //登录成功
return redirectToSuccessUrl(req, resp);
}
}
//是get请求|登录失败,继续过滤器链,可能是被分配到controller处理/login
return true;
}
else { //不是登录请求,保存当前地址并重定向到登录界面
saveRequestAndRedirectToLogin(req, resp);
return false;
}
}
/**
* 登录成功后调用,若有之前的请求则重定向到它,否则到默认成功页面
*/
private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
WebUtils.redirectToSavedRequest(req, resp, successUrl);
return false;
}
/**
* 保存当前请求并跳转登录页面
*/
private void saveRequestAndRedirectToLogin(HttpServletRequest req,
HttpServletResponse resp) throws IOException {
WebUtils.saveRequest(req);
WebUtils.issueRedirect(req, resp, loginUrl);
}
/**
* 表单验证,执行登录方法
*/
private boolean login(HttpServletRequest req) {
String username = req.getParameter("username");
String password = req.getParameter("password");
try {
SecurityUtils.getSubject().login(new UsernamePasswordToken(
username, password));
} catch (Exception e) {
req.setAttribute("shiroLoginFailure", e.getClass());
return false;
}
return true;
}
/**
* 判断是否登录请求
*/
private boolean isLoginRequest(HttpServletRequest req) {
return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
}
}
- 第一步,直接结束的前提除了是否已登录,还要有是否为登录请求,可能用户已登录但仍然进入登录页面重新登录;否则直接返回后被前端url为/login的controller处理,而该controller只负责失败处理,会重新重定向到登录页面,导致死循环。
- 表单验证和登录成功的重定向已经实现了,剩下的就是请求登录页面的get请求和登录失败的处理需要自己在controller中实现。
也可以继承AuthenticatingFilter来实现,它提供了很多登录相关的基础代码。另外可以参考Shiro内置FormAuthenticationFilter源码,思路是一样的。
测试:访问/test.jsp,自动跳转登录页面,登录成功后跳回test.jsp。
任意角色授权拦截器
- Shiro内置的roles(RolesAuthorizationFilter)拦截器验证用户是否拥有指定的所有角色,但没有提供能验证用户拥有任意角色的拦截器。
- 我们自定义一个:
public class AnyRolesFilter extends AccessControlFilter {
private String unauthorizedUrl = "/unauthorized.jsp";
private String loginUrl = "/login.jsp";
/**
* 权限验证
*/
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
Object mappedValue) throws Exception {
String[] roles = (String[])mappedValue;
//如果没有设置角色参数,默认成功
if(roles == null) {
return true;
}
//判断用户是否拥有所需权限
for(String role : roles) {
if(getSubject(request, response).hasRole(role)) { //未登录情况获取不到任何角色
return true;
}
}
//拒绝访问
return false;//跳到onAccessDenied处理
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response)
throws Exception {
Subject subject = getSubject(request, response);
//未登录
if (subject.getPrincipal() == null) {
//重定向到登录页面
saveRequest(request);
WebUtils.issueRedirect(request, response, loginUrl);
}
//已登录但授权失败
else {
//如果有未授权页面跳转过去
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
}
//否则返回401未授权状态码
else {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
}
}
- 401
可以在web.xml指定方法返回401时映射的错误页面
<error-page> <error-code>401</error-code> <location>/unauthorized.jsp</location> </error-page>
代码实例:ideaProjects/shiroChapter8
- 参考文章