概述前提(代码不全,可略过此篇博客)
1、自定义UsernamePasswordToken:UsenamePasswordToken一个简单的用户名/密码身份验证令牌,可支持最广泛的身份验证机制,所以我希望的认证能够围绕token进行
@Data
@AllArgsConstructor
public class CustomUsernamePasswordToken extends UsernamePasswordToken{
private String token;
}
2、自定义AccessControlFilter:任何资源的过滤器,这里需要覆写isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o)方法以及onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse)方法
public class CustomAccessControllerFilter extends AccessControlFilter
3、自定义Realm:此实现将自动执行所有角色和权限检查,需要覆写授权方法doGetAuthorizationInfo(PrincipalCollection principals)和认证方法doGetAuthenticationInfo(AuthenticationToken token)
public class CustomRealm extends AuthorizingRealm
4、自定义HashedCredentialsMatcher:简单验证匹配器
public class CustomHashedCredentialsMatcher extends HashedCredentialsMatcher
认证过程
假设一登录用户希望查看自身的详细信息,在用户向服务器发送请求的过程中,首先,这次请求会被我们自定义的CustomAccessControllerFilter所拦截,其次,会访问方法:isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o),如果该方法返回true,则这次请求会被本次放过,被下一个所捕获或直接通过(过滤器的工作方式是链式的);如果返回false,则这次继续访问自定义CustomAccessControllerFilter的方法onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse),并通过getSubject(servletRequest,servletResponse).login(customUsernamePasswordToken)传入customUsernamePasswordToken表示最终用户的主体和凭据的构造实例
再之后会流转到ModularRealmAuthenticator进行认证:
protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) {
if (!realm.supports(token)) {
String msg = "Realm [" + realm + "] does not support authentication token [" +
token + "]. Please ensure that the appropriate Realm implementation is " +
"configured correctly or that the realm accepts AuthenticationTokens of this type.";
throw new UnsupportedTokenException(msg);
}
AuthenticationInfo info = realm.getAuthenticationInfo(token);
if (info == null) {
String msg = "Realm [" + realm + "] was unable to find account data for the " +
"submitted AuthenticationToken [" + token + "].";
throw new UnknownAccountException(msg);
}
return info;
}
由于我们之前只自定义了一个realm,所以会跳转至doSingleRealmAuthentication(realms.iterator().next(), authenticationToken)方法内部,这里AuthenticationInfo info = realm.getAuthenticationInfo(token);则是真正开始认证:
这里首先根据getCachedAuthenticationInfo(token)方法从缓存中获取info信息,但由于我们之前没有配置,所以这里的info是空的,那么在info = doGetAuthenticationInfo(token)方法中就从我们自定义的realm里面的doGetAuthenticationInfo方法获取:
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CustomUsernamePasswordToken sessionId = (CustomUsernamePasswordToken)token;
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(sessionId.getPrincipal(),sessionId.getCredentials(),getName());
return info;
}
当info!=null时,将拿着用户信息的进入到用户的核心匹配器进行认证匹配,assertCredentialsMatch(token, info)方法或捕获我们自定义的CustomHashedCredentialsMatcher匹配器(这里登陆时我把用户信息缓存到了redis里),匹配成功后返回true,通过方法栈向下完成认证
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
CustomUsernamePasswordToken customUsernamePasswordToken = (CustomUsernamePasswordToken)token;
String sessionId = (String)customUsernamePasswordToken.getPrincipal();
if(!redisService.hasKey(sessionId)){
throw new BusinessException(4001002, "凭证过期,请重新登录");
}
return true;
}
总结: 首先通过过滤器拦截请求从请求头中获取token信息,随后由主体提交并构造实例传递到SecurityManager委托完成authenticate,在此过程中首先验证Realm的个数,由个数选择启用方法,随后在Realm域中真正获取用户信息并进行匹配
授权过程
授权过程需要启用shiro的注解:
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
主体授权(subject.isPermitted()、subject.checkRole(“xxx角色”)、shiro前端标签授权、后端授权注解
(@RequiresPermissions(“sys:log:list”))),会跳转至AuthorizingSecurityManager执行授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String token = (String) principals.getPrimaryPrincipal();
String userId = (String) redisService.get(token);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addRoles(getRoleByUserId(userId));
info.addStringPermissions(getPermissionByUserId(userId));
return info;
}
最后再将用户的权限信息封装并循环匹配注解中的权限信息,匹配成功返回true说明拥有权限,匹配不成功则返回false并抛出:Subject does not have permission[xxxx]异常