Shiro自定义Realm结合Redis实现单点登录

原创
2021/03/24 18:53
阅读数 1.3K

简单介绍下前提,我们现在使用的单点登录的方案是集成微软的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);
    }
}

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部