1、序
由于项目中需要同时实现两种访问方式(JWT和普通登陆访问),在网上搜了下使用shiro相关的实现,方法有挺多的,但是有部分方法的代码耦合性太强,因为另一种方式可能以后会拆分出去作为一个独立项目的登陆,因此希望两种登陆方式彼此间的耦合尽量少。
2、登陆流程
由于希望找到一种低入侵性的实现方式,所以整理了一下登陆流程,从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类型是否就可以实现多种不同的登陆方式了呢?
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里面的关键点在于
- 设置登陆url保证filter在拦截到登陆请求时可以进入登陆流程。
- 重写创建token类的方法createToken这里创建成自己写的token类,这样就可以调用相应的realm验证。
- 重写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配置不同的登陆方式。