简单介绍下前提,我们现在使用的单点登录的方案是集成微软的ADFS来实现的,此处吐槽下微软的东东真心蛋疼,我们基于SAML协议实现了ADFS的集成,但是在使用过程中,偶尔会出现 request head too long的错误。问题的根因是浏览器中的cookie信息太多导致,目前集成的系统大概有十几个,且都是同一个顶级域名下,所以整改各个系统的cookie信息不是很现实,而微软的ADFS服务器又对认证时带入的cookie大小做了限定,虽然可以调整ADFS服务器的允许的大小,但是因种种原因没有往这条路上走。
目前暂时走的路是在ADFS的Shiro过滤器之前增加一个自定义的Redis认证的过滤器,只要Redis中存在用户数据则直接通过认证。 增加一种Shiro的自定义认证方式需要准备的内容如下:
- RedisUserRealm
- MyRedisFilter
- MyToken
首先准备我们的MyToken和认证域 RedisUserRealm,至于Shiro的多Realm的配置以及过滤器的配置这里就不赘述了。
/**
* @Author: Magic_yuan
* @Date: 2021/3/10 20:29
*/
public class MyToken implements AuthenticationToken {
private String redisToken;
public MyToken(String redisToken) {
this.redisToken = redisToken;
}
@Override
public Object getPrincipal() {
return this.redisToken;
}
@Override
public Object getCredentials() {
return this.redisToken;
}
}
/**
* @Author: Magic_yuan
* @Date: 2021/3/10 20:07
*/
public class RedisUserRealm extends AuthorizingRealm {
@Autowired
private MyRedisClient myRedisClient;
@Autowired
private ISysMenuService menuService;
@Autowired
private ISysRoleService roleService;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof MyToken;
}
public RedisUserRealm() {
this.setAuthenticationTokenClass(MyToken.class);
}
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
String jsonStr = myRedisClient.get(arg0.toString());
if (StringUtils.isBlank(jsonStr)) {
return null;
}
SysUser user = JSONObject.parseObject(jsonStr, SysUser.class);
// 角色列表
Set<String> roles = new HashSet<String>();
// 功能列表
Set<String> menus = new HashSet<String>();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 管理员拥有所有权限
if (user.isAdmin()) {
info.addRole("admin");
info.addStringPermission("*:*:*");
} else {
roles = roleService.selectRoleKeys(user.getUserId());
menus = menuService.selectPermsByUserId(user.getUserId());
// 角色加入AuthorizationInfo认证对象
info.setRoles(roles);
// 权限加入AuthorizationInfo认证对象
info.setStringPermissions(menus);
}
return info;
}
/**
* 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
MyToken token = (MyToken) authenticationToken;
String jsonStr = myRedisClient.get(token.getPrincipal().toString());
if (StringUtils.isBlank(jsonStr)) {
return null;
}
SysUser user = JSONObject.parseObject(jsonStr, SysUser.class);
PrincipalCollection principalCollection = new SimplePrincipalCollection(user, this.getName());
return new SimpleAuthenticationInfo(principalCollection, token.getCredentials());
}
/**
* 清理缓存权限
*/
public void clearCachedAuthorizationInfo() {
this.clearCachedAuthorizationInfo(SecurityUtils.getSubject().getPrincipals());
}
}
最后的重点是我们的过滤器,Shiro在做认证时,会经过Filter中的几个重点方法,首先是AuthenticatingFilter中的isAccessAllowed方法,此方法中会判定是否允许通过如果是false的话则进入onAccessDenied方法,所以我们自定义的Filter中需要实现这个方法,做的事情很简单就是执行我们的executeLogin方法,也就是我们在executeLogin中判定请求中是否带有我们指定的cookie信息,即redisToke值,如果有的话我们根据cookie中的信息从redis中查询数据,如果可以查询到用户数据则执行我们的认证方法,也就是我们在RedisUserRealm中定义的授权和认证两个方法,当然此处我们不管认证是否成功,最终在我们的Filter中都是返回true,因为此Filter只是我们辅助认证的,哪怕我们实际并没有通过Redis的信息认证成功,后续的ADFS过滤器中依然会去做ADFS的单点登陆认证,而在ADFS的认证中,我们只需要在认证成功之后存放用户数据进Redis并且往浏览器的cookie中写入redis的key值,这样就可以在后续的cookie有效期内不管用户是否结束了会话都可以避免再次通过ADFS做认证,从而缓解通过ADFS认证偶发性的request head too long的问题。
此方案抛砖引玉,不知道大家有没有其他更好的解决ADFS出现的request head too long的快捷途径,望大神们指教。
/**
* @Author: Magic_yuan
* @Date: 2021/3/10 20:22
*/
public class MyRedisFilter extends AuthenticatingFilter {
@Override
protected GenscriptToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
Cookie cookie = ServletUtils.getCookie("redisToken");
if (null == cookie) {
return null;
}
return new GenscriptToken(cookie.getValue());
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
AuthenticationToken token = createToken(request, response);
if (token != null) {
MyRedisClient myRedisClient = SpringUtils.getBean(MyRedisClient.class);
String jsonStr = myRedisClient.get(token.getPrincipal().toString());
if (StringUtils.isNotBlank(jsonStr)) {
Subject subject = getSubject(request, response);
subject.login(token);
}
}
return true;
}
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
//todo
return executeLogin(servletRequest, servletResponse);
}
}