欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

java前后端分离项目中使用shiro权限框架遇到的那些坑

程序员文章站 2022-07-03 16:34:40
...

前言

最近在做一个前后端分离的项目。前端使用vue,后端使用的是spring boot,因为需要做权限管理。就选择集成shiro框架。以前都是在传统项目中使用shiro。第一次在前后端分离的项目中使用shiro。给我带来了很大的困扰。遇到了很多麻烦。所以在此记录。方便以后查阅。也希望能让同样面临同样问题的人能节约点时间。

坑点总结

1.前后端分离项目没有部署在同一台服务器上,要面临跨域问题。
2.使用token 作为shiro认证标识
3.前后端分离项目中,未登录时用返回json代替重定向。

详解

1. 解决跨域问题

spring boot 跨域问题很好解决。使用下面代码。或者在网上搜索springboot解决跨域问题。很快便可以完成此步骤。

package com.common.config.cors;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;


@Configuration
public class CorsConfig {

    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许任何域名使用
        corsConfiguration.addAllowedOrigin("*");
        // 允许任何头
        corsConfiguration.addAllowedHeader("*");
        // 允许任何方法(post、get等)
        corsConfiguration.addAllowedMethod("*");
        return corsConfiguration;
    }


    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 对接口配置跨域设置
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }
}

2.使用token 作为shiro认证标识

一开始面临这个问题,感觉无比的复杂。shiro根据sessionId来判断是不是同一个用户发起的request请求。但是前后端分离的项目中。用户的每次请求都相当于新的请求。sessionId可能会发生变化。
我们需要的就是解决这种问题。最先想到的是使用token来代替session,让前端发送的请求都携带登录成功后返回的token令牌。(其实和session原理一样。就是以前的sessionId存储在cookie中,现在用token,将sessionId存储在了请求头中。)下面看代码。

package com.**.*.config;

import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.web.servlet.ShiroHttpServletRequest;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;

/** shiro 的 session 管理
 *      自定义session规则,实现前后分离,在跨域等情况下使用token 方式进行登录验证才需要,否则没必须使用本类。
 *      shiro默认使用 ServletContainerSessionManager 来做 session 管理,它是依赖于浏览器的 cookie 来维护 session 的,调用 storeSessionId  方法保存sesionId 到 cookie中
 *      为了支持无状态会话,我们就需要继承 DefaultWebSessionManager
 *      自定义生成sessionId 则要实现 SessionIdGenerator
 * @author zzy
 * @date 2020/11/18 11:23
 */
public class ShiroSession extends DefaultWebSessionManager {
    private static final String AUTH_TOKEN = "authorization";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";


    public ShiroSession() {
        super();
        //设置 shiro session 失效时间,默认为30分钟,这里现在设置为15分钟
        //setGlobalSessionTimeout(MILLIS_PER_MINUTE * 15);
    }



    /**
     * 获取sessionId,原本是根据sessionKey来获取一个sessionId
     * 重写的部分多了一个把获取到的token设置到request的部分。这是因为app调用登陆接口的时候,是没有token的,登陆成功后,产生了token,我们把它放到request中,返回结
     * 果给客户端的时候,把它从request中取出来,并且传递给客户端,客户端每次带着这个token过来,就相当于是浏览器的cookie的作用,也就能维护会话了
     * @param request
     * @param response
     * @return
     */
    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        //获取请求头中的 AUTH_TOKEN 的值,如果请求头中有 AUTH_TOKEN 则其值为sessionId。shiro就是通过sessionId 来控制的
        String sessionId = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
        if (StringUtils.isEmpty(sessionId)){
            //如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
            return super.getSessionId(request, response);

        } else {
            //请求头中如果有 authToken, 则其值为sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            //sessionId
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return sessionId;
        }
    }
}

在shiro配置类中创建安全管理器的时候使用自定义ShiroSession来做会话管理。

@Bean
    public SecurityManager getSecurityManager(CustomRealm realm) {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        //设置自定义的realm
        defaultWebSecurityManager.setRealm(realm);
        //自定义的shiro session 缓存管理器
        defaultWebSecurityManager.setSessionManager(sessionManager());

        return defaultWebSecurityManager;
    }
    
  /**
     * 自定义的 shiro session 缓存管理器,用于跨域等情况下使用 token 进行验证,不依赖于sessionId
     * @return
     */
    @Bean
    public SessionManager sessionManager(){
        //将我们继承后重写的shiro session 注册
        ShiroSession shiroSession = new ShiroSession();
        //如果后续考虑多tomcat部署应用,可以使用shiro-redis开源插件来做session 的控制,或者nginx 的负载均衡
        shiroSession.setSessionDAO(new EnterpriseCacheSessionDAO());
        return shiroSession;
    }

3.登录失败时,用返回json来代替重定向

一步一个坑。在解决完第二步骤的时候,发现shiro基本算是配置成功了。但是发现如果没有登录的时候访问具有登录权限的接口总是会报404错误。发现这些请求都是被重定向到了很目录下面的index.jsp页面。因为本地没有这个页面。所以引发404错误。在经过查阅资料后发现,进本都是通过配置过滤器来解决问题的。
代码如下。

package com.**.*.filter;

import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import springfox.documentation.service.ResponseMessage;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @author zzy
 * @date 2020/11/19 11:09
 */
public class MyFormAuthenticationFilter 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();
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code", "100");
        jsonObject.put("desc", "请前往登录页面");
        try {
            flushMsgStrToClient(response, jsonObject);
        } catch (Exception e) {
            e.printStackTrace();
        }
        out.flush();
        out.close();
        return false;
    }

    public static void flushMsgStrToClient(ServletResponse response, Object object)
            throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSONObject.toJSONString(object));
        response.getWriter().flush();
    }
}

在配置类的过滤工厂中添加配置

 public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        //1.创建过滤器工厂
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        Map<String, Filter> filters = new HashMap<>();
        MyFormAuthenticationFilter myFormAuthenticationFilter = new MyFormAuthenticationFilter();
        filters.put("authc",myFormAuthenticationFilter);
        filterFactory.setFilters(filters);
        //2.设置安全管理器
        filterFactory.setSecurityManager(securityManager);
        //4.设置过滤器集合

        /**
         * 设置所有的过滤器:有顺序map
         *     key = 拦截的url地址
         *     value = 过滤器类型
         *
         */
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/System/SystemLogin","anon");
        filterMap.put("/**","authc");
        filterFactory.setFilterChainDefinitionMap(filterMap);

        return filterFactory;
    }