前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Shiro实现多realm方案

Shiro实现多realm方案

原创
作者头像
程序员田同学
发布2022-06-27 11:23:37
8870
发布2022-06-27 11:23:37
举报
文章被收录于专栏:java-spring

大家好,我是程序员田同学。

公司开始了新项目,新项目的认证采用的是Shiro实现。由于涉及到多端登录用户,而且多端用户还是来自不同的表。

这就涉及到了Shiro的多realm,今天的demo主要是介绍Shiro的多realm实现方案,文中包含所有的代码,需要的朋友可以无缝copy。

image-20220627101300075
image-20220627101300075

前后端分离的背景下,在认证的实现中主要是两方面的内容,一个是用户登录获取到token,二是从请求头中拿到token并检验token的有效性和设置缓存。

1、用户登录获取token

登录和以往单realm实现逻辑一样,使用用户和密码生成token返回给前端,前端每次请求接口的时候携带token。

代码语言:txt
复制
  @ApiOperation(value="登录", notes="登录")
    public Result<JSONObject> wxappLogin(String username,String password){
     Result<JSONObject> result = new Result<JSONObject>();
        JSONObject obj = new JSONObject();
	// 生成token
	String password="0";
	String token = JwtUtil.sign(username, password);
        obj.put("token", token);
        result.setResult(obj);
        result.success("登录成功");

        return result;
        }	

生成token的工具类

代码语言:txt
复制
/**
 * 生成签名,5min后过期
 *
 * @param username 用户名
 * @param secret   用户的密码
 * @return 加密的token
 */
public static String sign(String username, String secret) {
   Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
   Algorithm algorithm = Algorithm.HMAC256(secret);
   // 附带username信息
   return JWT.create().withClaim("username", username).withExpiresAt(date).sign(algorithm);
}

以上就实现了简单的登录逻辑,和Shiro的单realm设置和SpringSecurity的登录逻辑都没有什么区别。

2、鉴权登录拦截器(验证token有效性)

使用Shiro登录拦截器的只需要继承Shiro的 BasicHttpAuthenticationFilter 类 重写 isAccessAllowed()方法,在该方法中我们从ServletRequest中获取到token和login_type。

需要特别指出的是,由于是多realm,我们在请求头中加入一个login_type来区分不同的登录类型。

通过token和login_type我们生成一个JwtToken对象提交给getSubject。

JwtFilter过滤器

代码语言:txt
复制
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    /**
     * 默认开启跨域设置(使用单体)
     */
    private boolean allowOrigin = true;

    public JwtFilter(){}
    public JwtFilter(boolean allowOrigin){
        this.allowOrigin = allowOrigin;
    }

    /**
     * 执行登录认证
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        try {
            executeLogin(request, response);
            return true;
        } catch (Exception e) {
            JwtUtil.responseError(response,401,CommonConstant.TOKEN_IS_INVALID_MSG);
            return false;
            //throw new AuthenticationException("Token失效,请重新登录", e);
        }
    }

    /**
     *
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(CommonConstant.X_ACCESS_TOKEN);
        String loginType = httpServletRequest.getHeader(CommonConstant.LOGIN_TYPE);
        // update-begin--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数
        if (oConvertUtils.isEmpty(token)) {
            token = httpServletRequest.getParameter("token");
        }
        // update-end--Author:lvdandan Date:20210105 for:JT-355 OA聊天添加token验证,获取token参数

        JwtToken jwtToken = new JwtToken(token,loginType);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwtToken);
        // 如果没有抛出异常则代表登入成功,返回true
        return true;
    }
  
}

JwtToken类

代码语言:txt
复制
public class JwtToken implements AuthenticationToken {
   
   private static final long serialVersionUID = 1L;
   private String token;

    private String loginType;
//    public JwtToken(String token) {
//        this.token = token;
//    }
    public JwtToken(String token,String loginType) {
        this.token = token;
        this.loginType=loginType;
    }
    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }
 
    @Override
    public Object getCredentials() {
        return token;
    }
}

再往下的逻辑肯定会先根据我们的login_type来走不同的realm了,然后在各自的realm中去检查token的有效性了,那Shiro怎么知道我们的Realm都是哪些呢?

接下来就该引出使用Shiro的核心配置文件了——ShiroConfig.java类

shiro的配置文件中会注入名字为securityManager的Bean。

在该bean中首先注入ModularRealmAuthenticator,ModularRealmAuthenticator会根据配置的AuthenticationStrategy(身份验证策略)进行多Realm认证过程。

由于是多realm我们需要重写ModularRealmAuthenticator类,ModularRealmAuthenticator类中用于判断逻辑走不同的realm,接着注入我们的两个realm,分别是myRealm和clientShiroRealm。

重新注入 ModularRealm类

代码语言:txt
复制
 @Bean
    public ModularRealm ModularRealm(){
        //自己重写的ModularRealmAuthenticator
        ModularRealm modularRealm = new ModularRealm();
//        modularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());//这里为默认策略:如果有一个或多个Realm验证成功,所有的尝试都被认为是成功的,如果没有一个验证成功,则该次尝试失败
        return modularRealm;
    }

securityManager-bean。

代码语言:txt
复制
 @Bean("securityManager")
    public DefaultWebSecurityManager securityManager(ShiroRealm myRealm,
    ClientShiroRealm clientShiroRealm,ModularRealm modularRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//        securityManager.setRealm(myRealm);

        securityManager.setAuthenticator(modularRealm);
        List<Realm> realms = new ArrayList<>();
        //添加多个Realm
        realms.add(myRealm);
        realms.add(clientShiroRealm);
        securityManager.setRealms(realms);

        /*
         * 关闭shiro自带的session,详情见文档
         * http://shiro.apache.org/session-management.html#SessionManagement-
         * StatelessApplications%28Sessionless%29
         */
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        //自定义缓存实现,使用redis
        securityManager.setCacheManager(redisCacheManager());
        return securityManager;
    }

ModularRealm实现类

代码语言:txt
复制
public class ModularRealm  extends ModularRealmAuthenticator {

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());

        for (Realm realm : realms) {
            // 这里使用的realm中定义的Name属性来进行区分,注意realm中要加上
            realmHashMap.put(realm.getName(), realm);
        }

        JwtToken token = (JwtToken) authenticationToken;

        if (StrUtil.isEmpty(token.getLoginType())){
            return  doSingleRealmAuthentication(realmHashMap.get(LoginType.DEFAULT.getType()),token);
        } else {
            return  doSingleRealmAuthentication(realmHashMap.get(token.getLoginType()),token);
        }

//        return super.doAuthenticate(authenticationToken);
    }
}

然后会根据不同的login_type到不同的realm,下面为我的Shiro认证realm。

myrealm类.

代码语言:txt
复制
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {
   @Lazy
    @Resource
    private CommonAPI commonApi;

    @Lazy
    @Resource
    private RedisUtil redisUtil;

    @Override
    public String getName() {
        return LoginType.DEFAULT.getType();
    }
    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
//        return token instanceof JwtToken;
        if (token instanceof JwtToken){
            return StrUtil.isEmpty(((JwtToken) token).getLoginType()) || LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());
        } else {
            return false;
        }
    }

    /**
     * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
     * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
        String username = null;
        if (principals != null) {
            LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
            username = sysUser.getUsername();
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        // 设置用户拥有的角色集合,比如“admin,test”
        Set<String> roleSet = commonApi.queryUserRoles(username);
        System.out.println(roleSet.toString());
        info.setRoles(roleSet);

        // 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = commonApi.queryUserAuths(username);
        info.addStringPermissions(permissionSet);
        System.out.println(permissionSet);
        log.info("===============Shiro权限认证成功==============");
        return info;
    }

    /**
     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
     * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
     *
     * @param auth 用户登录的账号密码信息
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
        String token = (String) auth.getCredentials();
        if (token == null) {
            HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
            log.info("————————身份认证失败——————————IP地址:  "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        LoginUser loginUser = null;
        try {
            loginUser = this.checkUserTokenIsEffect(token);
        } catch (AuthenticationException e) {
            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
            e.printStackTrace();
            return null;
        }
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     *
     * @param token
     */
    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username,用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }

        // 查询用户信息
        log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
        LoginUser loginUser = TokenUtils.getLoginUser(username,commonApi,redisUtil);
        //LoginUser loginUser = commonApi.getUserByName(username);
        if (loginUser == null) {
            throw new AuthenticationException("用户不存在!");
        }
        // 判断用户状态
        if (loginUser.getStatus() != 1) {
            throw new AuthenticationException("账号已被锁定,请联系管理员!");
        }
        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
        }
        //update-begin-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
        String userTenantIds = loginUser.getRelTenantIds();
        if(oConvertUtils.isNotEmpty(userTenantIds)){
            String contextTenantId = TenantContext.getTenant();
            String str ="0";
            if(oConvertUtils.isNotEmpty(contextTenantId) && !str.equals(contextTenantId)){
                //update-begin-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
                String[] arr = userTenantIds.split(",");
                if(!oConvertUtils.isIn(contextTenantId, arr)){
                    throw new AuthenticationException("用户租户信息变更,请重新登陆!");
                }
                //update-end-author:taoyan date:20211227 for: /issues/I4O14W 用户租户信息变更判断漏洞
            }
        }
        //update-end-author:taoyan date:20210609 for:校验用户的tenant_id和前端传过来的是否一致
        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
     * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
     *       用户过期时间 = Jwt有效时间 * 2。
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (oConvertUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                //生成token
                String newAuthorization = JwtUtil.sign(userName, passWord);
                // 设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
                log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
            }
            //update-begin--Author:scott  Date:20191005  for:解决每次请求,都重写redis中 token缓存问题
//       else {
//          // 设置超时时间
//          redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, cacheToken);
//          redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME / 1000);
//       }
            //update-end--Author:scott  Date:20191005   for:解决每次请求,都重写redis中 token缓存问题
            return true;
        }

        //redis中不存在此TOEKN,说明token非法返回false
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

}

ClientShiroRealm类.

代码语言:txt
复制
@Component
@Slf4j
public class ClientShiroRealm extends AuthorizingRealm {
   @Lazy
    @Resource
    private ClientAPI clientAPI;

    @Lazy
    @Resource
    private RedisUtil redisUtil;
    @Override
    public String getName() {
        return LoginType.CLIENT.getType();
    }
    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
//        return token instanceof JwtToken;
        if (token instanceof JwtToken){
            return  LoginType.CLIENT.getType().equals(((JwtToken) token).getLoginType());
        } else {
            return false;
        }
    }

    /**
     * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
     * 触发检测用户权限时才会调用此方法,例如checkRole,checkPermission
     *
     * @param principals 身份信息
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.debug("===============Shiro权限认证开始============ [ roles、permissions]==========");
        //String username = null;
        //if (principals != null) {
        //    LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
        //    username = sysUser.getUsername();
        //}
        //SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //// 设置用户拥有的角色集合,比如“admin,test”
        //Set<String> roleSet = commonApi.queryUserRoles(username);
        //System.out.println(roleSet.toString());
        //info.setRoles(roleSet);
        //
        //// 设置用户拥有的权限集合,比如“sys:role:add,sys:user:add”
        //Set<String> permissionSet = commonApi.queryUserAuths(username);
        //info.addStringPermissions(permissionSet);
        //System.out.println(permissionSet);
        log.info("===============Shiro权限认证成功==============");
        return null;
    }

    /**
     * 用户信息认证是在用户进行登录的时候进行验证(不存redis)
     * 也就是说验证用户输入的账号和密码是否正确,错误抛出异常
     *
     * @param auth 用户登录的账号密码信息
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.debug("===============Shiro身份认证开始============doGetAuthenticationInfo==========");
        String token = (String) auth.getCredentials();
        if (token == null) {
            HttpServletRequest req = SpringContextUtils.getHttpServletRequest();
            log.info("————————身份认证失败——————————IP地址:  "+ oConvertUtils.getIpAddrByRequest(req) +",URL:"+req.getRequestURI());
            throw new AuthenticationException("token为空!");
        }
        // 校验token有效性
        LoginUser loginUser = null;
        try {
            loginUser = this.checkUserTokenIsEffect(token);
        } catch (AuthenticationException e) {
            JwtUtil.responseError(SpringContextUtils.getHttpServletResponse(),401,e.getMessage());
            e.printStackTrace();
            return null;
        }
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     *
     * @param token
     */
    public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
        // 解密获得username,用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token非法无效!");
        }

        // 查询用户信息
        log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
        LoginUser loginUser = TokenUtils.getClientLoginUser(username,clientAPI,redisUtil);
        //LoginUser loginUser = commonApi.getUserByName(username);
        if (loginUser == null) {
            throw new AuthenticationException("用户不存在!");
        }

        // 校验token是否超时失效 & 或者账号密码是否错误
        if (!jwtTokenRefresh(token, username, loginUser.getPassword())) {
            throw new AuthenticationException(CommonConstant.TOKEN_IS_INVALID_MSG);
        }
        return loginUser;
    }

    /**
     * JWTToken刷新生命周期 (实现: 用户在线操作不掉线功能)
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样),缓存有效期设置为Jwt有效时间的2倍
     * 2、当该用户再次请求时,通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求jwt生成的token值已经超时,但该token对应cache中的k还是存在,则表示该用户一直在操作只是JWT的token失效了,程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值,该缓存生命周期重新计算
     * 4、当该用户这次请求jwt在生成的token值已经超时,并在cache中不存在对应的k,则表示该用户账户空闲超时,返回用户信息已失效,请重新登录。
     * 注意: 前端请求Header中设置Authorization保持不变,校验有效性以缓存中的token为准。
     *       用户过期时间 = Jwt有效时间 * 2。
     *
     * @param userName
     * @param passWord
     * @return
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        String cacheToken = String.valueOf(redisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
        if (oConvertUtils.isNotEmpty(cacheToken)) {
            // 校验token有效性
            if (!JwtUtil.verify(cacheToken, userName, passWord)) {
                //生成token
                String newAuthorization = JwtUtil.sign(userName, passWord);
                // 设置超时时间
                redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newAuthorization);
                redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME *2 / 1000);
                log.debug("——————————用户在线操作,更新token保证不掉线—————————jwtTokenRefresh——————— "+ token);
            }

            return true;
        }

        //redis中不存在此TOEKN,说明token非法返回false
        return false;
    }

    /**
     * 清除当前用户的权限认证缓存
     *
     * @param principals 权限信息
     */
    @Override
    public void clearCache(PrincipalCollection principals) {
        super.clearCache(principals);
    }

}

这两个realm更多的是需要实现我们自身的realm,我把我的全部代码贴上,读者可根据自己的需要进行修改,两个方法大致的作用都是检验token的有效性,只是查询的用户从不同的用户表中查出来的。

至此,Shiro的多Realm实现方案到这里就正式结束了。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、用户登录获取token
  • 2、鉴权登录拦截器(验证token有效性)
相关产品与服务
数字身份管控平台
数字身份管控平台(Identity and Access Management)为您提供集中式的数字身份管控服务。在企业 IT 应用开发时,数字身份管控平台可为您集中管理用户账号、分配访问权限以及配置身份认证规则,避免因员工账号、授权分配不当导致的安全事故。在互联网应用开发时,数字身份管控平台可为您打通应用的身份数据,更好地实现用户画像,也可为用户提供便捷的身份认证体验,提升用户留存。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档