如何通过Shiro实现支持长尾词查询的多Realm方案?

2026-04-18 00:261阅读0评论SEO问题
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计2665个文字,预计阅读时间需要11分钟。

如何通过Shiro实现支持长尾词查询的多Realm方案?

公司启动了新项目,该项目采用Shiro认证框架。考虑到涉及多端登录用户,且不同端用户来自不同的表,这引入了Shiro的多个realm概念。今天的demo主要介绍Shiro的realm实现方式。

公司开始了新项目,新项目的认证采用的是Shiro实现。由于涉及到多端登录用户,而且多端用户还是来自不同的表。 这就涉及到了Shiro的多realm,今天的demo主要是介绍Shiro的多realm实现方案,文中包含所有的代码,需要的朋友可以无缝copy。

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

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

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

如何通过Shiro实现支持长尾词查询的多Realm方案?

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

1、用户登录获取token

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

@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的工具类

/** * 生成签名,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过滤器

@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 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实现类

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类.

@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类.

@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实现方案到这里就正式结束了。

本文共计2665个文字,预计阅读时间需要11分钟。

如何通过Shiro实现支持长尾词查询的多Realm方案?

公司启动了新项目,该项目采用Shiro认证框架。考虑到涉及多端登录用户,且不同端用户来自不同的表,这引入了Shiro的多个realm概念。今天的demo主要介绍Shiro的realm实现方式。

公司开始了新项目,新项目的认证采用的是Shiro实现。由于涉及到多端登录用户,而且多端用户还是来自不同的表。 这就涉及到了Shiro的多realm,今天的demo主要是介绍Shiro的多realm实现方案,文中包含所有的代码,需要的朋友可以无缝copy。

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

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

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

如何通过Shiro实现支持长尾词查询的多Realm方案?

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

1、用户登录获取token

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

@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的工具类

/** * 生成签名,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过滤器

@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 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实现类

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类.

@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类.

@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实现方案到这里就正式结束了。