Spring Boot+Shiro+JWT实现多Realm时Filter异常捕捉

近日,大叔接公司需求,搭建前后端分离的后台管理模块接口。
简述登陆模块相关需求:

  • 用户输入用户名和密码获取JWTToken令牌
  • 用户使用JWTToken令牌访问后端业务接口

大叔简单说下Springboot集成Shiro的过程:
1.在pom.xml引入Shiro

<!-- Shiro使用Srping框架 -->
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
</dependency>

2.写自己的Realm:UserRealm.java,JWTRealm.java,

/**
*   因为UserRealm只用于登陆验证故继承AuthenticatingRealm就好了
**/
public class UserRealm extends AuthenticatingRealm {
     /**
     *  该方法用于多Realm认证时识别需要使用哪一个Realm
     */ 
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof UsernamePasswordToken;
    } 
    /**
    *   该方法用于登陆身份验证
    **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //TODO  根据自己的验证需要写登陆验证
     }
}
/**
*   JWTRealm既要验证身份,又要做权限认证,所以继承AuthorizingRealm 
**/
public class JWTRealm extends AuthorizingRealm {
    /**
    *   该方法用于多Realm认证时识别需要使用哪一个Realm
    **/
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }
    
    /**
    *   权限 权限验证时会执行到这里
    **/ 
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //TODO 根据自己的设计写权限
    }
    /**
    *   该方法用于JWTToken验证
    **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        //TODO  根据自己的验证需要写验证
    }
}

3.写JWTFilter.java

public class JwtFilter extends BasicHttpAuthenticationFilter {
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        //TODO
        return true;
    }
    
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        //TODO 
        return false;
    }
    
}

4.写ShiroConfig.java

/**
*   加载权限配置
**/
@Configuration
public class ShiroConfig {
    /**
     * 注册shiro的Filter,拦截请求
     */
    @Bean
    public FilterRegistrationBean<Filter> filterRegistrationBean(DefaultWebSecurityManager securityManager)
            throws Exception {
        FilterRegistrationBean<Filter> filterRegistration = new FilterRegistrationBean<Filter>();
        filterRegistration.setFilter((Filter) shiroFilter(securityManager).getObject());
        filterRegistration.addInitParameter("targetFilterLifecycle", "true");
        filterRegistration.setAsyncSupported(true);
        filterRegistration.setEnabled(true);
        filterRegistration.setDispatcherTypes(DispatcherType.REQUEST);
        return filterRegistration;
    }

    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm,这里不设置的话会报错
        // One or more realms must be present to execute an authentication attempt. One
        // or more realms must be present to execute an authentication attempt.
        securityManager.setAuthenticator(authenticator());
        securityManager.setAuthorizer(authorizer());
        return securityManager;
    }
    
    /**
    * 用于用户名密码登录时认证的realm
    */
    @Bean("userRealm")
    public Realm userRealm() {
        UserRealm userRealm = new UserRealm();
        return userRealm;
    }

    /**
    * 用于JWT token认证的realm
    */
    @Bean("jwtRealm")
    public Realm jwtRealm() {
        JWTRealm jwtRealm= new JWTRealm();
        return jwtRealm;
    }
     /**
     * 初始化Authenticator 认证器 身份认证
     */
    @Bean
    public Authenticator authenticator() {
        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();//留意这一行哟
        // 设置两个Realm,一个用于用户登录验证;一个用于jwt token的认证和访问权限获取
        authenticator.setRealms(Arrays.asList(jwtRealm(), userRealm()));
        // 设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }
    
    /**
     * 初始化authorizer 认证器 权限认证
     * @return
     */
    @Bean
    public Authorizer authorizer() {
        ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();//这里的
        authorizer.setRealms(Arrays.asList(jwtShiroRealm()));
        return authorizer;
    }
    
    /**
    * 禁用session, 不保存用户登录状态。保证每次请求都重新认证。
    * 需要注意的是,如果用户代码里调用Subject.getSession()还是可以用session,如果要完全禁用,要配合下面的noSessionCreation的Filter来实现
    */
    @Bean
    protected SessionStorageEvaluator sessionStorageEvaluator() {
        DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        return sessionStorageEvaluator;
    }
    
    /**
     * 设置过滤器,将自定义的Filter加入
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        // 添加过滤器
        Map<String, Filter> filterMap = new HashMap<String, Filter>();
        // JWT过滤器
        filterMap.put("jwtFilter", jwtFilter());// JwTfilter
        factoryBean.setFilters(filterMap);
        // 拦截器
        factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap()); 
        return factoryBean;
    }

    @Bean
    protected ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); 
        chainDefinition.addPathDefinition("/login", "noSessionCreation,anon"); // login不做认证,noSessionCreation的作用是用户在操作session时会抛异常
        chainDefinition.addPathDefinition("/**", "noSessionCreation,jwtFilter"); // 默认进行用户鉴权
        return chainDefinition;
    }

    // 不要加@Bean注解,不然spring会自动注册成filter,我们这里是手动注入
    protected JwtFilter jwtFilter() {
        return new JwtFilter();
    }
    /**
    * 开启Shiro注解通知器
    */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            @Qualifier("securityManager") SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
}

好了,到这里我们可以算作是完成了Shiro的集成工作了


随便写一段测试代码

@RestController
public class TestController {
    @GetMapping("/helloWorld")
    @RequiresPermissions("system:test:hello")
    public String helloWorld() {
        return “hello world";
    }
}

启动项目,请求这个接口试试看吧。


在实现了多Realm的登陆之后,发现当JWTRealm身份验证报错时,在JWTFilter获取到的异常类型都是AuthenticationException,而导致不能再JWTRealm中根据不同的异常做不同的处理。
怎么办呢?跟踪异常抛出流程发现多Realm时,异常在ModularRealmAuthenticator中会被处理掉,统一抛出AuthenticationException。所以大叔发现重写ModularRealmAuthenticator中的doMultiRealmAuthentication方法就好了

public class MultiRealmAuthenticator extends ModularRealmAuthenticator {
    private static final Logger log = LoggerFactory.getLogger(MultiRealmAuthenticator.class);
    @Override
    protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token)
            throws AuthenticationException {
        AuthenticationStrategy strategy = getAuthenticationStrategy();
        AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
        if (log.isTraceEnabled()) {
            log.trace("Iterating through {} realms for PAM authentication", realms.size());
        }
        AuthenticationException authenticationException = null;
        for (Realm realm : realms) {
            aggregate = strategy.beforeAttempt(realm, token, aggregate);
            if (realm.supports(token)) {
                log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
                AuthenticationInfo info = null;
                try {
                    info = realm.getAuthenticationInfo(token);
                } catch (AuthenticationException e) {
                    authenticationException = e;
                    if (log.isDebugEnabled()) {
                        String msg = "Realm [" + realm
                                + "] threw an exception during a multi-realm authentication attempt:";
                        log.debug(msg, e);
                    }
                }
                aggregate = strategy.afterAttempt(realm, token, info, aggregate, authenticationException);
            } else {
                log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
            }
        }
        if (authenticationException != null) {
            throw authenticationException;
        }
        aggregate = strategy.afterAllAttempts(token, aggregate);
        return aggregate;
    }
}

重写之后替换掉ShiroConfig.java中的身份认证就好了

        /**
     * 初始化Authenticator 认证器 身份认证
     */
    @Bean
    public Authenticator authenticator() {
        MultiRealmAuthenticator authenticator = new MultiRealmAuthenticator();
        // 设置两个Realm,一个用于用户登录验证和访问权限获取;一个用于jwt token的认证
        authenticator.setRealms(Arrays.asList(jwtShiroRealm(), dbShiroRealm()));
        // 设置多个realm认证策略,一个成功即跳过其它的
        authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
        return authenticator;
    }

大叔说,坑再多,不在怕,爬起来,反正还会掉坑里的

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345