使用shiro-starter在前后端分离的Springboot项目

最近项目上使用shiro作为权限框架,因为是前后端分离的springboot项目,使用传统的shiro-spring包完全不能体现springboot的自动配置和简化配置,优化开发,所有深入探究了一下shiro-spring-boot-web-starter的使用,在此对于项目中使用到的进行记录,并对和shiro整合spring的包进行比较。

传统的shiroconfig文件

@Configuration
public class ShiroConfig {


    /**
     * ShiroFilterFactoryBean 处理拦截资源文件问题。
     * 注意:初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager
     * Web应用中,Shiro可控制的Web请求必须经过Shiro主过滤器的拦截
     * @param securityManager
     * @return
     */
    @Bean(name = "shirFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") SecurityManager securityManager) {

        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //必须设置 SecurityManager,Shiro的核心安全接口
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //这里的/login是后台的接口名,非页面,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        shiroFilterFactoryBean.setLoginUrl("/login");
        //这里的/index是后台的接口名,非页面,登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //未授权界面,该配置无效,并不会进行页面跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        //自定义拦截器限制并发人数,参考博客:
        //LinkedHashMap<String, Filter> filtersMap = new LinkedHashMap<>();
        //限制同一帐号同时在线的个数
        //filtersMap.put("kickout", kickoutSessionControlFilter());
        //shiroFilterFactoryBean.setFilters(filtersMap);

        // 配置访问权限 必须是LinkedHashMap,因为它必须保证有序
        // 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 一定要注意顺序,否则就不好使了
        LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //配置不登录可以访问的资源,anon 表示资源都可以匿名访问
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/druid/**", "anon");
        //logout是shiro提供的过滤器
        filterChainDefinitionMap.put("/logout", "logout");
        //此时访问/userInfo/del需要del权限,在自定义Realm中为用户授权。
        //filterChainDefinitionMap.put("/userInfo/del", "perms[\"userInfo:del\"]");

        //其他资源都需要认证  authc 表示需要认证才能进行访问
        filterChainDefinitionMap.put("/**", "authc");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        return shiroFilterFactoryBean;
    }

    /**
     * 配置核心安全事务管理器
     * @param shiroRealm
     * @return
     */
    @Bean(name="securityManager")
    public SecurityManager securityManager(@Qualifier("shiroRealm") ShiroRealm shiroRealm) {
        DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();
        //设置自定义realm.
        securityManager.setRealm(shiroRealm);
        //配置记住我 参考博客:
        //securityManager.setRememberMeManager(rememberMeManager());

        //配置 redis缓存管理器 参考博客:
        //securityManager.setCacheManager(getEhCacheManager());

        //配置自定义session管理,使用redis 参考博客:
        //securityManager.setSessionManager(sessionManager());

        return securityManager;
    }

    /**
     * 配置Shiro生命周期处理器
     * @return
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     *  身份认证realm; (这个需要自己写,账号密码校验;权限等)
     * @return
     */
    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm shiroRealm = new ShiroRealm();
        return shiroRealm;
    }

    /**
     * 必须(thymeleaf页面使用shiro标签控制按钮是否显示)
     * 未引入thymeleaf包,Caused by: java.lang.ClassNotFoundException: org.thymeleaf.dialect.AbstractProcessorDialect
     * @return
     */
    @Bean
    public ShiroDialect shiroDialect() {
        return new ShiroDialect();
    }


}

非自动配置的shiro必须向IOC容器注入自定义的Rleam,ShiroFilterFactoryBean,以及SecurityManager。才能进行基本的使用,更别说使用一块新功能都需要在配置文件注入一个。
现在我们用shiro-starter如何呢,我们先来看看他jar包中自动配置是怎么注入的。
直接找到jar包的Spring.factories

shiro-spring-boot-starter中的自配配置

public class ShiroBeanAutoConfiguration extends AbstractShiroBeanConfiguration {
    public ShiroBeanAutoConfiguration() {
    }

//生命周期后置处理器
    @Bean
    @ConditionalOnMissingBean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return super.lifecycleBeanPostProcessor();
    }

    @Bean
    @ConditionalOnMissingBean
    protected EventBus eventBus() {
        return super.eventBus();
    }

    @Bean
    @ConditionalOnMissingBean
    public ShiroEventBusBeanPostProcessor shiroEventBusAwareBeanPostProcessor() {
        return super.shiroEventBusAwareBeanPostProcessor();
    }
}
----------------------------------------------
public class ShiroAutoConfiguration extends AbstractShiroConfiguration {
    public ShiroAutoConfiguration() {
    }

    //认证策略
    @Bean
    @ConditionalOnMissingBean
    protected AuthenticationStrategy authenticationStrategy() {
        return super.authenticationStrategy();
    }

    @Bean
    @ConditionalOnMissingBean
    protected Authenticator authenticator() {
        return super.authenticator();
    }


//授权者
    @Bean
    @ConditionalOnMissingBean
    protected Authorizer authorizer() {
        return super.authorizer();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SubjectDAO subjectDAO() {
        return super.subjectDAO();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        return super.sessionStorageEvaluator();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SubjectFactory subjectFactory() {
        return super.subjectFactory();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SessionFactory sessionFactory() {
        return super.sessionFactory();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SessionDAO sessionDAO() {
        return super.sessionDAO();
    }

    @Bean
    @ConditionalOnMissingBean
    protected SessionManager sessionManager() {
        return super.sessionManager();
    }

//SessionsSecurityManager 
    @Bean
    @ConditionalOnMissingBean
    protected SessionsSecurityManager securityManager(List<Realm> realms) {
        return super.securityManager(realms);
    }

    @Bean
    @ConditionalOnResource(
        resources = {"classpath:shiro.ini"}
    )
    protected Realm iniClasspathRealm() {
        return this.iniRealmFromLocation("classpath:shiro.ini");
    }

    @Bean
    @ConditionalOnResource(
        resources = {"classpath:META-INF/shiro.ini"}
    )
    //Realm
    protected Realm iniMetaInfClasspathRealm() {
        return this.iniRealmFromLocation("classpath:META-INF/shiro.ini");
    }

    @Bean
    @ConditionalOnMissingBean({Realm.class})
    protected Realm missingRealm() {
        throw new NoRealmBeanConfiguredException();
    }
}
---------------------------------------
public class ShiroAnnotationProcessorAutoConfiguration extends AbstractShiroAnnotationProcessorConfiguration {
    public ShiroAnnotationProcessorAutoConfiguration() {
    }

    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        return super.defaultAdvisorAutoProxyCreator();
    }

    @Bean
    @ConditionalOnMissingBean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        return super.authorizationAttributeSourceAdvisor(securityManager);
    }
}
------------------------------------
ublic class ShiroNoRealmConfiguredFailureAnalyzer extends AbstractFailureAnalyzer<NoRealmBeanConfiguredException> {
    public ShiroNoRealmConfiguredFailureAnalyzer() {
    }

    protected FailureAnalysis analyze(Throwable rootFailure, NoRealmBeanConfiguredException cause) {
        return new FailureAnalysis("No bean of type 'org.apache.shiro.realm.Realm' found.", "Please create bean of type 'Realm' or add a shiro.ini in the root classpath (src/main/resources/shiro.ini) or in the META-INF folder (src/main/resources/META-INF/shiro.ini).", cause);
    }
}

这里看到大多数属性都进行了自动注入。但是都不是最关键的组件

现在来看看shiro-spring-boot-web-starter

public class ShiroWebAutoConfiguration extends AbstractShiroWebConfiguration {
    public ShiroWebAutoConfiguration() {
    }

    //认证策略
    @Bean
    @ConditionalOnMissingBean
    protected AuthenticationStrategy authenticationStrategy() {
        return super.authenticationStrategy();
    }

   //认证器
    @Bean
    @ConditionalOnMissingBean
    protected Authenticator authenticator() {
        return super.authenticator();
    }
  //授权者,这里注意也是shiro-spring-boot-web-starter的坑点,后面会讲到
    @Bean
    @ConditionalOnMissingBean
    protected Authorizer authorizer() {
        return super.authorizer();
    }

//DAO
    @Bean
    @ConditionalOnMissingBean
    protected SubjectDAO subjectDAO() {
        return super.subjectDAO();
    }
//session存储策略
    @Bean
    @ConditionalOnMissingBean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        return super.sessionStorageEvaluator();
    }
//主角工厂
    @Bean
    @ConditionalOnMissingBean
    protected SubjectFactory subjectFactory() {
        return super.subjectFactory();
    }
//session工厂
    @Bean
    @ConditionalOnMissingBean
    protected SessionFactory sessionFactory() {
        return super.sessionFactory();
    }
//sessionDAO
    @Bean
    @ConditionalOnMissingBean
    protected SessionDAO sessionDAO() {
        return super.sessionDAO();
    }
//SessionManager
    @Bean
    @ConditionalOnMissingBean
    protected SessionManager sessionManager() {
        return super.sessionManager();
    }
//SessionsSecurityManager,这里可以看到自动注入是他已经带着参数将Realm注入进去了,非常重要
    @Bean
    @ConditionalOnMissingBean
    protected SessionsSecurityManager securityManager(List<Realm> realms) {
        return super.securityManager(realms);
    }
//sessionCookieTemplate的cookie
    @Bean
    @ConditionalOnMissingBean(
        name = {"sessionCookieTemplate"}
    )
    protected Cookie sessionCookieTemplate() {
        return super.sessionCookieTemplate();
    }
//rememberMeManager
    @Bean
    @ConditionalOnMissingBean
    protected RememberMeManager rememberMeManager() {
        return super.rememberMeManager();
    }

//rememberMeCookieTemplate的cookie
    @Bean
    @ConditionalOnMissingBean(
        name = {"rememberMeCookieTemplate"}
    )
    protected Cookie rememberMeCookieTemplate() {
        return super.rememberMeCookieTemplate();
    }
//shiro过滤链
    @Bean
    @ConditionalOnMissingBean
    protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
        return super.shiroFilterChainDefinition();
    }
}
=---------------------------------
public class ShiroWebFilterConfiguration extends AbstractShiroWebFilterConfiguration {
    public ShiroWebFilterConfiguration() {
    }

//shiro过滤工厂类
    @Bean
    @ConditionalOnMissingBean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean() {
        return super.shiroFilterFactoryBean();
    }

//过滤器注册Bean
    @Bean(
        name = {"filterShiroFilterRegistrationBean"}
    )
    @ConditionalOnMissingBean
    protected FilterRegistrationBean filterShiroFilterRegistrationBean() throws Exception {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR});
        filterRegistrationBean.setFilter((AbstractShiroFilter)this.shiroFilterFactoryBean().getObject());
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }
}


看我源码我们大概了解到了,shiro-spring-boot-starter给我们自动配置了大多数组件,三大组件也注入了IOC容器中,可是security他给我注入的是SessionSecurityManager,并且已经将Realm待参进行构建,这一点很重要,因为我们必须定义自己的认证授权规则,所以必须创建继承 AuthorizingRealm的Realm类,那么为什么不适用@Compant注解将其注入到IOC容器中,而是@Bean将Rleam加入呢,一点在于必须将Rleam注入到SecurityManager中,所以shiro团队也因为进行了改进。所以我们使用是可以使用@Autowire自动注入SessionSecurityManager到ShiroConfig里,而不需要@Bean再往IOC里面注入,如果你要使用其他类型的SecurityManager,可以自己@Bean配置。而其他的如
ShiroFilterFactoryBean 并没有进行配置,所以需要手动配置。

那么现在看看我自己使用的ShiroConfig

@Configuration
public class ShiroConfigurer {


//直接取出已经注入到IOC容器的SessionsSecurityManager
    @Autowired
    protected SessionsSecurityManager securityManager;


    /**
     * @return realm实现了Authorizer接口,所以autoconfig不好自动注入
     */
    @Bean
    protected Authorizer authorizer() {
        return new ModularRealmAuthorizer();
    }

    /**
     * 拦截规则
     */
    @Bean
    protected LinkedHashMap<String, String> chainDefinitionMap() {

        return new LinkedHashMap<String, String>(25) {{

            //swagger-ui doc放行
            this.put("/swagger-ui.html", "anon");
            this.put("/swagger-resources/**", "anon");
            this.put("/v2/api-docs", "anon");
            this.put("/webjars/**", "anon");
            this.put("/doc.html", "anon");  
            
            this.put("/user/vercode", "anon");//验证码
            this.put("/error", "anon");//错误

            //静态资源
            this.put("/css/**", "anon");
            this.put("/fonts/**", "anon");
            this.put("/img/**", "anon");
            this.put("/static/js/**", "anon");
            this.put("/favicon.ico", "anon");

  

            //自定义的过滤器。
            this.put("/**/*", "jsonRemember"); //其余的资源都必须认证之后才能访问
        }};
    }

    /**
     * 加入自定义的filter
     */
    @Bean
    protected ShiroFilterFactoryBean shiroFilterFactoryBean(
            @Qualifier("userId_loginTime_cache") Cache userId_loginTime_cache,
            @Qualifier("chainDefinitionMap") Map<String, String> chainDefinitionMap) {

        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        factoryBean.setLoginUrl("/user/login");
        //自定义过滤链用于权限验证
        factoryBean.getFilters().put("jsonRemember", new JsonRememberFilter(userId_loginTime_cache));

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setFilterChainDefinitionMap(chainDefinitionMap);
        return factoryBean;
    }


    /**
     * shiro-ehcache(授权信息)
     */
    @Bean
    protected EhCacheManager cacheManager() {
        EhCacheManager ehCacheManager = new EhCacheManager();
        ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
        return ehCacheManager;
    }
}


这里可以看到我们只进行了对于ShiroFilterFactoryBean ,chainDefinitionMap进行了配置,ehcacheManager的配置还是因为我们要使用其业务,至于Authorizer可以看这篇博文

。目前我们就已经进行好了基础配置,接下来我就配置

自定义的Realm认证授权规则

@Component
public class VercodeRealm extends AuthorizingRealm implements Serializable {


    @Resource
    private Cache vercodeCache;


    /**
     * 查看源码发现,不能自动注入CredentialsMatcher,但是能自己设置CacheManager
     */
    public VercodeRealm() {
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("MD5");
        matcher.setHashIterations(4);
        this.setCredentialsMatcher(matcher);
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {

        VercodeToken token = (VercodeToken) authenticationToken;

        Element element = vercodeCache.get(SecurityUtils.getSubject().getSession().getId());

        {
            if (element == null)
                throw new VercodeExpiryException();//验证码过期


            if (!token.getVerCode().equalsIgnoreCase((String) element.getObjectValue()))
                throw new VercodeMismatchedException();//验证码不匹配
        }

        {
            String scNoOrPhone = ((String) token.getPrincipal());

            //查询顺序,学号-电话

            Wrapper<User> wrapper;

            if (scNoOrPhone.startsWith("20")) { //201721456
                wrapper = Wrappers.<User>lambdaQuery().eq(User::getScNo, scNoOrPhone);
            } else if (scNoOrPhone.startsWith("1")) {//13965971234
                wrapper = Wrappers.<User>lambdaQuery().eq(User::getPhone, scNoOrPhone);
            } else {
                throw new UnknownAccountException();//无此用户名
            }

            User user = User.sqlInstance().selectOne(wrapper);

            //账户被锁
//        if (principal.isLocked()) {
//            throw new LockedAccountException();//账户被锁
//        }

            String hashedPassword = user.getPassword();

            ByteSource salt = ByteSource.Util.bytes(user.getSalt());

            return new SimpleAuthenticationInfo(user, hashedPassword, salt, this.getName());
        }
    }


    /**
     * @param principals 授权
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        SimpleAuthorizationInfo authorInfo = new SimpleAuthorizationInfo();

        User user = ((User) principals.getPrimaryPrincipal());

        if (user.getIsAdmin())
            authorInfo.setRoles(Collections.singleton("admin"));
        else
            authorInfo.setRoles(Collections.singleton("user"));

        return authorInfo;
    }


}

如果你不是前后端分离的业务,也对系统效率也没要求,已经可以使用这一套配置进行开发了。
可以shiro-spring-boot-web-starter并没对前后端分离进行支持,它的认证过滤器会在进行认证后进行跳转,这不是我们想要的,前后端分离后,前端接管了跳转,而后端只负责处理数据,返回数据,返回状态等,所以我们必须改造它的过滤链。

Shiro内置的FilterChain

anon(AnonymousFilter.class),
authc(FormAuthenticationFilter.class),
authcBasic(BasicHttpAuthenticationFilter.class),
logout(LogoutFilter.class),
noSessionCreation(NoSessionCreationFilter.class),
perms(PermissionsAuthorizationFilter.class),
port(PortFilter.class),
rest(HttpMethodPermissionFilter.class),
roles(RolesAuthorizationFilter.class),
ssl(SslFilter.class),
user(UserFilter.class);

我们知道shiro执行过滤器的顺序是

  1. 加载 DefaultFilter 中的默认 Filter;
  2. 加载自定义 Filter;
  3. 加载 FFilterChainDefinitionMap;

弄清楚了这 Filter 的加载与注册,那这与我们要解决的问题有何关系呢?首先我们怀疑这里获取的 Filter 是异常的,调试打个断点看看。
所以我会按照FormAuthenticationFilter的基础进行改造,看了FormAuthenticationFilter的源码,它继承AuthenticatingFilter,所以我们也相应的继承AuthenticatingFilter
接下来是我的Filter

public class JsonRememberFilter extends AuthenticatingFilter {


    private final String COOKIE_NAME = "LOGIN_TIME";


    private final Cache userId_loginTime_cache;


    public JsonRememberFilter(Cache userId_loginTime_cache) {
        this.userId_loginTime_cache = userId_loginTime_cache;
    }


    /**
     * 根据jsonText生成token
     */
    @Override
    protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws IOException {

        //一个流最多只能用一次getReader()
        UserLoginDto user = new ObjectMapper().readValue(request.getReader(), UserLoginDto.class);

        return new VercodeToken(user.getAccount(), user.getPassword(),
                user.isRememberMe(), request.getRemoteHost(), user.getVercode());
    }


    /**
     * 是登陆url,并且为post请求判断为登陆请求
     */
    @Override
    protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
        return "POST".equals(((HttpServletRequest) request).getMethod()) && pathsMatch(getLoginUrl(), request);
    }


    /**
     * 保证一个账号只能有一次登陆,如果cookie里面的登录时间和缓存里的登录时间不同,清除cookie,没有cookie,那么就不能生成 principal
     */
    @SneakyThrows
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {

        Subject subject = SecurityUtils.getSubject();

        User presentPrincipal = (User) subject.getPrincipal();

        if (presentPrincipal == null || isLoginRequest(request, response)) { //放行重复的登录请求
            return false;
        } else {
            Element element = userId_loginTime_cache.get(presentPrincipal.getId());

            if (element == null)  //缓存中该用户的登录态已经被剔除
                return false;

            //根据cookie的登录时间判断是否为同一个人登录,若不存在该cookie,不存在登出,进入onAccessDenied()
            Cookie[] cookies = ((HttpServletRequest) request).getCookies();

            for (Cookie cookie : cookies) {

                Object loginTimeInCache = element.getObjectValue();
                String cookieName = cookie.getName();
                String cookieVal = cookie.getValue();

                boolean isValidated = COOKIE_NAME.equals(cookieName) && cookieVal.equals(loginTimeInCache);

                if (isValidated) return true; //相等表示就是当前用户,跳出循环
            }
            return false;   //进入onAccessDenied()
        }
    }

    /**
     * 登陆请求:登陆
     * 非登陆请求: 有principal放行(记住我)
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {

        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse rsp = (HttpServletResponse) response;

        String remoteAddr = req.getRemoteAddr();
        String requestURL = req.getRequestURL().toString();

        //是登陆请求 并且是Post方法 (isLoginSubmission实现post方法的判断)
        if (isLoginRequest(request, response)) {

            log.debug("检测到登录请求,ip = {},正在登陆", remoteAddr);

            return executeLogin(request, response);
        } else {
            //写出401
            rsp.setStatus(401);
            log.warn("检测到非法 ip = {} ,url = {}", remoteAddr, requestURL);
            return false;
        }
    }


    /**
     * 登陆成功返回200
     *
     * @return 是否放行到过滤器链
     */
    @Override
    protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception {

        User user = ((User) subject.getPrincipal());

        //设置登录时间的cookie
        String cookieVal = Long.toString(System.currentTimeMillis());
        String cookieName = COOKIE_NAME;

        Cookie cookie = new Cookie(cookieName, cookieVal);

        cookie.setMaxAge(3600 * 24 * 30); //30天
        cookie.setHttpOnly(true); //安全性
        cookie.setPath("/");
        //写入cookie
        ((HttpServletResponse) response).addCookie(cookie);
        //写入缓存
        userId_loginTime_cache.put(new Element(user.getId(), cookieVal));

        log.debug("ip = {} , userId = {} , name = {} 登陆成功", request.getRemoteAddr(), user.getId(), user.getName());

        //直接写出 1 ,不放行请求
        response.setContentType("application/json;charset=utf-8");

        new ObjectMapper().writeValue(response.getWriter(), RespBody.success());

        return false;
    }


    /**
     * 登陆失败根据 AuthenticationException的类型,返回不同的message
     *
     * @return 是否放行到过滤器链
     */
    @SneakyThrows
    @Override
    protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {

        log.debug("ip = {} 登陆失败", request.getRemoteAddr());

        SecurityUtils.getSubject().logout();

        String message;

        if (e instanceof VercodeMismatchedException)
            message = "验证码错误,请重新输入";
        else if (e instanceof VercodeExpiryException)
            message = "验证码过期,请重新获取";
        else if (e instanceof UnknownAccountException)
            message = "账号不存在";
        else if (e instanceof IncorrectCredentialsException)
            message = "密码错误";
        else if (e instanceof LockedAccountException)
            message = "账户被锁";
        else
            message = "请重新登陆";

        response.setContentType("application/json;charset=utf-8");

        new ObjectMapper().writeValue(response.getWriter(), RespBody.error(null).setMessage(message));

        return false;
    }

值得注意的是,如果将shiro的filter放入spring ioc,shiro将该filter视为spring的filter不会注入path*/
具体参考这篇博文
http://www.hillfly.com/2017/179.html

问题解决

眼下我暂时有两种办法去解决这个问题:

  1. 修改 AccessTokenFilter,在 Filter 内部加入 path match 方法对需要验证 token 的路径进行过滤。
  2. 将咱们的自定义 Filter 注册到 Shiro,不注册到 ApplicationFilterChain。

显然方案一是不可取的,这样修改范围过大,得不偿失了。那我们怎么去实现第二个方法呢?SpringBoot 提供了 FilterRegistrationBean 方便我们对 Filter 进行管理。

    @Bean
    public FilterRegistrationBean registration(AccessTokenFilter filter) {
        FilterRegistrationBean registration = new FilterRegistrationBean(filter);
        registration.setEnabled(false);
        return registration;
    }

将不需要注册的 Filter 注入方法即可。这时候再启动项目进行测试,就可以发现 filters 已经不存在咱们的自定义 Filter 了。

还有个办法不需要使用到 FilterRegistrationBean,因为我们将 AccessTokenFilter 注册为了 Bean 交给 Spring 托管了,所以它会被自动注册到 FilterChain 中,那我们如果不把它注册为 Bean 就可以避免这个问题了。

    /**
     * 不需要显示注册Bean了
    @Bean
    public AccessTokenFilter accessTokenFilter(){}
    **/

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager,
                                              IUrlFilterService urlFilterService) {
        //省略
        filterMap.put("hasToken", new AccessTokenFilter());
        //省略
    }



原文:使用shiro-starter在前后端分离的Springboot项目_Jaymeng8848的博客-CSDN博客_shiro starter
作者: Jaymeng8848