当前位置: 首页 > 工具软件 > Shiro-auth > 使用案例 >

springboot整合shiro-spring-boot-web-starter实现前后端分离的跨域问题

昝晗昱
2023-12-01

 

最近从github上发现一个不错的项目,项目使用的是springboot整合shiro-spring-boot-web-starter实现前后端分离技术,

但部署的启动后,出现了一些问题。

这里是借鉴的文章:

https://segmentfault.com/a/1190000013630601

https://www.cnblogs.com/yfzhou/p/9813177.html

https://www.jianshu.com/p/dbe441dcdbcf

https://segmentfault.com/a/1190000014479154

这里是github上面的项目连接:

https://github.com/CaiBaoHong/biu

这里是shiro官方文档:

https://shiro.apache.org/spring-boot.html

项目本身采用的是gradle项目架构,我将其改为maven后发现了一个问题,关于登陆时,获取用户详细信息被shiro的过滤器拦截了下来。导致无法获取数据。同样的代码,在gradle上,没有问题,反而在maven架构上,出现了问题。百思不得其,实在没办法后只能通过修改shiro的过滤器,让其放行OPTIONS的请求。具体实现如下:

首先是jar:

<dependency>
       <groupId>org.apache.shiro</groupId>
       <artifactId>shiro-spring-boot-web-starter</artifactId>
       <version>1.4.0</version>
</dependency>

前后端分离跨域解决方案是采用CORS

//这里实不实现接口没有影响
@Configuration
public class WebConfig implements WebMvcConfigurer {
//解决跨域
    @Bean
    public CorsFilter corsFilter() {
        CorsConfiguration conf = new CorsConfiguration();
        conf.addAllowedHeader("*");
        conf.addAllowedMethod("*");
        conf.addAllowedOrigin("*");
        //允许cookie
        conf.setAllowCredentials(true);
        conf.setMaxAge(3600L);
        conf.addExposedHeader("set-cookie");
        conf.addExposedHeader("access-control-allow-headers");
        conf.addExposedHeader("access-control-allow-methods");
        conf.addExposedHeader("access-control-allow-origin");
        conf.addExposedHeader("access-control-max-age");
        conf.addExposedHeader("X-Frame-Options");
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", conf); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

这个是我shiro的验证类:

package com.mrlv.api.shiro;

import com.baomidou.mybatisplus.mapper.EntityWrapper;
import com.mrlv.api.entity.SysUser;
import com.mrlv.api.service.ISysPermService;
import com.mrlv.api.service.ISysRoleService;
import com.mrlv.api.service.ISysUserService;
import com.mrlv.api.vo.AuthVo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 这个类是参照JDBCRealm写的
 */
public class UserRealm extends AuthorizingRealm  {

    private static final Logger log = LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private ISysUserService sysUserService;
    @Autowired
    private ISysRoleService sysRoleService;
    @Autowired
    private ISysPermService sysPermService;


    //适配密码,编写加密代码
//    @Override
//    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
//        //设置用于匹配密码的CredentialsMatcher
//        HashedCredentialsMatcher hashcredentialsMatcher = new HashedCredentialsMatcher();
//        //采用算法:Md5Hash,Sha1Hash,Sha256Hash
//        hashcredentialsMatcher.setHashAlgorithmName(Md5Hash.ALGORITHM_NAME);
//        //是否采用16进制,默认是true
//        hashcredentialsMatcher.setStoredCredentialsHexEncoded(false);
//        //哈希值
//        hashcredentialsMatcher.setHashIterations(1024);
//        super.setCredentialsMatcher(hashcredentialsMatcher);
//    }

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        SysUser user = (SysUser) getAvailablePrincipal(principals);
        Set<AuthVo> roles = user.getRoles();
        Set<AuthVo> perms = user.getPerms();
        log.info("获取角色权限信息: roles: {}, perms: {}",roles,perms);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles.stream().map(AuthVo::getVal).collect(Collectors.toSet()));
        info.setStringPermissions(perms.stream().map(AuthVo::getVal).collect(Collectors.toSet()));
        return info;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken userToken = (UsernamePasswordToken)token;
        String username = userToken.getUsername();
        if (username == null){
            //账户问题发生异常,抛出此异常
            throw new AccountException("用户名不能为空");
        }
        SysUser user = sysUserService.selectOne(new EntityWrapper<SysUser>().eq("login_name", username));
        if (user == null){
            //当用户不存在的时候,抛出此异常
            throw new UnknownAccountException("找不到用户(" + username + ")的账号信息");
        }
        //查询用户的角色和权限存到SimpleAuthenticationInfo中,这样在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息,包括角色和权限
        Set<AuthVo> roles = sysRoleService.getRolesByUserId(user.getId());   //用户所有角色值,用于shiro做角色权限的判断
        Set<AuthVo> perms = sysPermService.getPermsByUserId(user.getId());    //用户所有权限值,用于shiro做资源权限的判断
        user.getRoles().addAll(roles);
        user.getPerms().addAll(perms);
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        return info;
    }
}

然后配置shiroConfig:

@Configuration
public class ShiroConfig {

    @Bean
    public Realm realm() {
        return new UserRealm();
    }

    @Bean
    public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        /**
         * setUsePrefix(false)用于解决一个奇怪的bug。在引入spring aop的情况下。
         * 在@Controller注解的类的方法中加入@RequiresRole注解,会导致该方法无法映射请求,导致返回404。
         * 加入这项配置能解决这个bug
         */
        creator.setUsePrefix(true);
        return creator;
    }

    /**
     * 这里统一做鉴权,即判断哪些请求路径需要用户登录,哪些请求路径不需要用户登录。
     * 这里只做鉴权,不做权限控制,因为权限用注解来做。
     * @return
     */
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
        //哪些请求可以匿名访问
        chain.addPathDefinition("/auth/login", "anon");
        chain.addPathDefinition("/auth/logout", "anon");
        chain.addPathDefinition("/page/401", "anon");
        chain.addPathDefinition("/page/403", "anon");
        chain.addPathDefinition("/page/index", "anon");

        //除了以上的请求外,其它请求都需要登录,这里改成corsFilterAAAA。使用这个自定义过滤器
        chain.addPathDefinition("/**", "corsFilterAAAA");
        return chain;
    }
}

还有application-shiro.yml

shiro:
#  未经身份验证的用户重定向到登录页面时使用的登录URL
  loginUrl: /auth/page/401
#  页面将用户重定向到未经授权的页面(403页)
  unauthorizedUrl: /auth/page/403
#  用户登录后的默认登录页面(如果在当前会话中找不到替代)
  successUrl: /auth/page/index

接下来是重点:

从github上面的项目源码上,并没有看到有其他过滤器。但偏偏能完美运行,这里我采用maven构建后的项目会出现被拦截的问题。所以只能修改ShiroWebFilterConfiguration

这里我创建了一个类,继承了ShiroWebFilterConfiguration

代码如下

@Configuration
public class ShiroWebFilter extends ShiroWebFilterConfiguration {
    @Override
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        //采用父类的默认方法生成shiroFilterFactoryBean
        ShiroFilterFactoryBean shiroFilterFactoryBean = super.shiroFilterFactoryBean();
        //获取shiroFilterFactoryBean里的Filters集合
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        //put进一个自己编写的过滤器,并命名,上面会引用到
        filters.put("corsFilterAAAA", new CorsAuthenticationFilter());
        shiroFilterFactoryBean.setFilters(filters);
        return shiroFilterFactoryBean;
    }
}

注意,这里根据借鉴的博客中直接使用shiroConfig来继承ShiroWebFilterConfiguration 的话,会出现循环依赖的bug而导致无法启动。

最后是我自定义的一个过滤器CorsAuthenticationFilter。

public class CorsAuthenticationFilter extends FormAuthenticationFilter {

    //这个方法是判断是否能通过
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        System.out.println("测试是否通过自定义");
        boolean allowed = super.isAccessAllowed(request, response, mappedValue);
        if (!allowed) {
            // 判断请求是否是options请求
            String method = WebUtils.toHttp(request).getMethod();
            if (StringUtils.equalsIgnoreCase("OPTIONS", method)) {
                return true;
            }
        }
        return allowed;
    }

//下面这个方法,重写了登陆失败后的输出信息。
//    @Override
//    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//        HttpServletResponse res = (HttpServletResponse)response;
//        res.setHeader("Access-Control-Allow-Origin", "*");
//        res.setStatus(HttpServletResponse.SC_OK);
//        res.setCharacterEncoding("UTF-8");
//        PrintWriter writer = res.getWriter();
//        Map<String, Object> map= new HashMap<>();
//        map.put("code", 702);
//        map.put("msg", "未登录");
//        writer.write(JSON.toJSONString(map));
//        writer.close();
//        return false;
//    }
}

如此,便解决了我遇到的问题。

 类似资料: