shiro的重定向在前后端分离项目中的解决方案
本文最后更新于 1001 天前,其中的信息可能已经有所发展或是发生改变。

问题

今天在测试代码的时候突然报了这个异常:

原因

当用户处于未登录状态,向一个需要登录的接口发送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显然是不合适的。

这会形成两个问题:

  1. 重定向的两个接口所能接收的方法可能是不同的。
  2. 即使相同,前端也无法对未认证或未授权的页面做出处理。

下面先复习一下Shiro的几个默认拦截器再说怎么解决这个问题。

有关认证的拦截器:

拦截器简称对应的默认拦截器类说明
authcFormAuthenticationFilter需要认证才可以访问
主要属性:
loginUrl:登录的url,默认login.jsp,如果被这个过滤器拦截后,会重定向这个url
successUrl:登录成功后重定向的url
userUserFilter需要认证或记住我才可以访问
logoutLogoutFilter退出url
主要属性:
redirectUrl:退出后重定向的url,默认”/“
如设置”/logout=logout”后,当访问/logout会被拦截,执行退出,再重定向到redirectUrl
anonAnonymousFilter不拦截,总会放行

有关授权的拦截器:

拦截器简称对应的默认拦截器类说明
rolesRolesAuthorizationFilter验证当前用户是否拥有所有角色
主要属性:
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两次

参考文章:

设置shiro认证和授权失败返回json而不是重定向_Sirm23333-CSDN博客

Shiro:未登录时请求跳转问题 – 自北徂南 – 博客园 (cnblogs.com)

感谢前辈们的总结
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇