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

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

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

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



    org.apache.shiro
    shiro-spring

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 filterRegistrationBean(DefaultWebSecurityManager securityManager)
            throws Exception {
        FilterRegistrationBean filterRegistration = new FilterRegistrationBean();
        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 filterMap = new HashMap();
        // 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 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;
    }

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

你可能感兴趣的:(Spring Boot+Shiro+JWT实现多Realm时Filter异常捕捉)