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

1,544 阅读5分钟

前言

最近在做一个前后端分离的项目。前端使用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;
    }

最后欢迎大家访问我的个人博客网站:zShare个人博客