由于项目中需要同时实现两种访问方式(JWT和普通登陆访问),在网上搜了下使用shiro相关的实现,方法有挺多的,但是有部分方法的代码耦合性太强,因为另一种方式可能以后会拆分出去作为一个独立项目的登陆,因此希望两种登陆方式彼此间的耦合尽量少。
由于希望找到一种低入侵性的实现方式,所以整理了一下登陆流程,从 FormAuthenticationFilter
的 onAccessDenied
方法作为查看登陆的入口对代码进行追踪,发现了一个有意思的地方, FormAuthenticationFilter
的 executeLogin
方法最后调用了 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类型是否就可以实现多种不同的登陆方式了呢?
首先,实现一个自己类型的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里面的关键点在于
最后,在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配置不同的登陆方式。