转载

同一个项目中shiro多种登陆方式的一种实现

由于项目中需要同时实现两种访问方式(JWT和普通登陆访问),在网上搜了下使用shiro相关的实现,方法有挺多的,但是有部分方法的代码耦合性太强,因为另一种方式可能以后会拆分出去作为一个独立项目的登陆,因此希望两种登陆方式彼此间的耦合尽量少。

2、登陆流程

由于希望找到一种低入侵性的实现方式,所以整理了一下登陆流程,从 FormAuthenticationFilteronAccessDenied 方法作为查看登陆的入口对代码进行追踪,发现了一个有意思的地方, FormAuthenticationFilterexecuteLogin 方法最后调用了 ModularRealmAuthenticator 这个类的 doAuthenticate 方法

protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
    this.assertRealmsConfigured();
    Collection<Realm> realms = this.getRealms();
    return realms.size() == 1 ? this.doSingleRealmAuthentication((Realm)realms.iterator().next(), authenticationToken) : this.doMultiRealmAuthentication(realms, authenticationToken);
  }

复制代码

可以看到这里的realms实际是个集合,且单realm和多个realm的登陆方式不同,于是我追进多realm的登陆方法中看看

protected AuthenticationInfo doMultiRealmAuthentication(Collection<Realm> realms, AuthenticationToken token) {
    AuthenticationStrategy strategy = this.getAuthenticationStrategy();
    AuthenticationInfo aggregate = strategy.beforeAllAttempts(realms, token);
    if (log.isTraceEnabled()) {
      log.trace("Iterating through {} realms for PAM authentication", realms.size());
    }

    Iterator var5 = realms.iterator();

    while(var5.hasNext()) {
      Realm realm = (Realm)var5.next();
      aggregate = strategy.beforeAttempt(realm, token, aggregate);
      //通过realm判断是否支持这个token来判断当前realm是否进行登陆流程
      if (realm.supports(token)) {
        log.trace("Attempting to authenticate token [{}] using realm [{}]", token, realm);
        AuthenticationInfo info = null;
        Throwable t = null;

        try {
          info = realm.getAuthenticationInfo(token);
        } catch (Throwable var11) {
          t = var11;
          if (log.isWarnEnabled()) {
            String msg = "Realm [" + realm + "] threw an exception during a multi-realm authentication attempt:";
            log.warn(msg, var11);
          }
        }

        aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
      } else {
        log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
      }
    }

    aggregate = strategy.afterAllAttempts(token, aggregate);
    return aggregate;
  }
复制代码

看到 if (realm.supports(token)) 这里,我灵光一现,是否可以通过realm对这个所谓的token支持的判断来实现类似策略模式方式的登陆呢?于是看看supports方法是怎么实现的

public boolean supports(AuthenticationToken token) {
    return token != null && this.getAuthenticationTokenClass().isAssignableFrom(token.getClass());
  }
复制代码

可以看到,这里实际上是通过了获取我们设置的 AuthenticationTokenClass 和传入的token的class做对比来确认是否支持这个realm的登陆方式,于是我有了思路,在filter中设置一个自己实现的 AuthenticationTokenClass ,在登陆的过程中创建不同的token类型是否就可以实现多种不同的登陆方式了呢?

3、实践

首先,实现一个自己类型的token,这里我实现了一个JWTtoken,这个实际上和shiro的 UsernamePasswordToken 一模一样,只是为了做上面所说的realm登陆的区分而已。

ublic class JWTToken  implements HostAuthenticationToken RememberMeAuthenticationToken  {
  UsernamePasswordToken
  private String username;
  private char[] password;
  private boolean rememberMe;
  private String host;
  //省略
  }
复制代码

然后是实现自己的realm来做登陆授权

public class JWTRealm extends AuthorizingRealm {
//设置登陆的realmclass
  public JWTRealm() {
    this.setAuthenticationTokenClass(JWTToken.class);
  }

  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    //这里做角色权限的添加
  }

  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken)
      throws AuthenticationException {
   //这里做登陆的验证
  }
}

复制代码

这里的关键在于调用 setAuthenticationTokenClass 设置自己的校验class,以便在调用登陆的时候可以做区分。然后创建一个自己的验证filter做不同的登陆验证流程

public class AppAuthFilter extends FormAuthenticationFilter {
  public AppAuthFilter(){
    this.setLoginUrl("/api/juejin/login");
  }
  @Override
  protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
      ServletRequest request, ServletResponse response) throws Exception {
    //这里写登陆成功后的回调逻辑

    return true;

  }

  @Override
  protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
    String username = this.getUsername(request);
    String password = this.getPassword(request);
    password = MD5Utils.encrypt(username, password);
    return this.createToken(username, password, request, response);
  }
  @Override
  protected AuthenticationToken createToken(String username, String password, ServletRequest request, ServletResponse response) {
    boolean rememberMe = this.isRememberMe(request);
    String host = this.getHost(request);
    return this.createToken(username, password, rememberMe, host);
  }
  @Override
  protected AuthenticationToken createToken(String username, String password, boolean rememberMe, String host) {
    return new JWTToken(username, password, rememberMe, host);
  }
  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response,
      Object mappedValue) {
    //这里写校验逻辑是否有权限访问
    return super.isAccessAllowed(request, response, mappedValue);
  }
}
复制代码

filter里面的关键点在于

  1. 设置登陆url保证filter在拦截到登陆请求时可以进入登陆流程。
  2. 重写创建token类的方法createToken这里创建成自己写的token类,这样就可以调用相应的realm验证。
  3. 重写isAccessAllowed方法,可以加上自己的校验逻辑,判定是否能够访问该接口。

最后,在shiro config中将两个realm加入进去,并使用自定义的filter来拦截需要不同登陆方法访问的url。

@Configuration
public class ShiroConfig {
@Bean
   ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
       ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
       LinkedHashMap<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
       Map<String, Filter> filtersMap = new LinkedHashMap<>();
       filtersMap.put("appauthc",new AppAuthFilter());
       filterChainDefinitionMap.put("/api/juejin/*","appauthc");
       shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
       return shiroFilterFactoryBean;
   }
  @Bean
   public SecurityManager securityManager() {
       DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
       //设置realm.
       List<Realm> Realms = new ArrayList<>();
       Realms.add(userRealm());
       Realms.add(jwtRealm());
       securityManager.setRealms(Realms);
       }
   }
复制代码

这样就完成了对于不同的url配置不同的登陆方式。

原文  https://juejin.im/post/5cd97afd51882569677f7bc5
正文到此结束
Loading...