最近公司的服務有這么一個需求:有兩套后台服務,在請求后台的的時候,會先由網關層進行權限攔截。根據當前登錄用戶擁有的菜單、角色以及請求的url地址進行攔截能不能請求到另外的服務地址。網關層使用的shiro的權限管理,已經封裝好了角色、權限和賬號。但是在攔截uri的問題上一直沒想明白。然后就看shiro的源碼,從我們現有的登錄看起。最終總算找到了一種解決方案
shiro在登錄時候,使用的是Subject的login方法。
Subject currentLoginUser = SecurityUtils.getSubject();
//A:是否已經登錄
if(currentLoginUser.isAuthenticated()) {
Boolean isAjax = (Boolean) request.getAttribute("X_IS_AJAX");
if( isAjax ) {
return AjaxResponse.success( null );
}else {
response.sendRedirect(homepageUrl);
return null;
}
}
//B:查詢用戶信息
CarAdmUser user = carAdmUserExMapper.queryByAccount(username,null);
if(user==null){
return AjaxResponse.fail(RestErrorCode.USER_NOT_EXIST) ;
}
//C:密碼不正確
String enc_pwd = PasswordUtil.md5(password, user.getAccount());
if(!enc_pwd.equalsIgnoreCase(user.getPassword())) {
return AjaxResponse.fail(RestErrorCode.USER_PASSWORD_WRONG) ;
}
//E: 用戶狀態
if(user.getStatus()!=null && user.getStatus().intValue()==100 ){
return AjaxResponse.fail(RestErrorCode.USER_INVALID) ;
}
//F: 執行登錄
try {
//shiro登錄
UsernamePasswordToken token = new UsernamePasswordToken( username, password.toCharArray() );
currentLoginUser.login(token);
//記錄登錄用戶的所有會話ID,以支持“系統管理”功能中的自動會話清理
String sessionId = (String)currentLoginUser.getSession().getId() ;
redisSessionDAO.saveSessionIdOfLoginUser(username, sessionId);
redisTemplate.delete(redis_login_key);
redisTemplate.delete(redis_getmsgcode_key);
}catch(AuthenticationException aex) {
return AjaxResponse.fail(RestErrorCode.USER_LOGIN_FAILED) ;
}
//返回登錄成功
Boolean isAjax = (Boolean) request.getAttribute("X_IS_AJAX");
if( isAjax ) {
return AjaxResponse.success( null );
}else {
response.sendRedirect(homepageUrl);
return null;
}
currentLoginUser.login(token);是重寫了shiro里面的AuthorizingRealm()
package com.sq.transportmanage.gateway.service.shiro.realm; import com.sq.transportmanage.gateway.dao.entity.driverspark.CarAdmUser; import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasPermissionExMapper; import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasRoleExMapper; import com.sq.transportmanage.gateway.service.auth.MyDataSourceService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.List; import java.util.Set; /**認證 與 權限 **/ /** * 這個就是shiro SSOLogin 的用戶獲取的屬性配置 */ @Component public class UsernamePasswordRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(UsernamePasswordRealm.class); @Autowired private MyDataSourceService myDataSourceService; @Autowired private SaasPermissionExMapper saasPermissionExMapper; @Autowired private SaasRoleExMapper saasRoleExMapper; /**重寫:獲取用戶的身份認證信息**/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{ logger.info( "[獲取用戶的身份認證信息開始]authenticationToken="+authenticationToken); try { UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; CarAdmUser adMUser = myDataSourceService.queryByAccount(token.getUsername()); SSOLoginUser loginUser = new SSOLoginUser(); //當前登錄的用戶 loginUser.setId( adMUser.getUserId() ); //用戶ID loginUser.setLoginName( adMUser.getAccount() );//登錄名 loginUser.setMobile( adMUser.getPhone() ); //手機號碼 loginUser.setName( adMUser.getUserName() ); //真實姓名 loginUser.setEmail(adMUser.getEmail()); //郵箱地址 loginUser.setType( null ); // loginUser.setStatus( adMUser.getStatus() ); //狀態 loginUser.setAccountType( adMUser.getAccountType() ); //自有的帳號類型:[100 普通用戶]、[900 管理員] loginUser.setLevel(adMUser.getLevel()); loginUser.setUuid(adMUser.getUuid()); List<String> menuUrlList = saasPermissionExMapper.queryPermissionCodesOfUser(adMUser.getUserId() ); loginUser.setMenuUrlList(menuUrlList); //---------------------------------------------------------------------------------------------------------數據權限BEGIN logger.info( "[獲取用戶的身份認證信息]="+loginUser); return new SimpleAuthenticationInfo(loginUser, authenticationToken.getCredentials() , this.getName() ); } catch (Exception e) { logger.error("獲取用戶的身份認證信息異常",e); return null; } } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SSOLoginUser loginUser = (SSOLoginUser) principalCollection.getPrimaryPrincipal(); String account = loginUser.getLoginName(); //登錄名 List<String> perms_string = saasPermissionExMapper.queryPermissionCodesOfUser( loginUser.getId() ); List<String> roles_string = saasRoleExMapper.queryRoleCodesOfUser( loginUser.getId() ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles = new HashSet<String>( roles_string ); authorizationInfo.setRoles( roles ); logger.info( "[獲取用戶授權信息(角色)] "+account+"="+roles); Set<String> perms = new HashSet<String>( perms_string ); authorizationInfo.setStringPermissions(perms); logger.info( "[獲取用戶授權信息(權限)] "+account+"="+perms); return authorizationInfo; } @Override public Object getAuthorizationCacheKey(PrincipalCollection principals) { SSOLoginUser loginUser = (SSOLoginUser) principals.getPrimaryPrincipal(); String account = loginUser.getLoginName(); //登錄名 return "-AuthInfo-"+account; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } }
通過這個可以把登錄信息放入shiro里面,下面是具體的菜單權限。正常來說,使用shiro的注解
@RequiresPermissions(value = {"value"})
就可以正常來使用了。@RequiresPermissions的源碼里面使用的是aop的方式,通過判斷value值是否在某個集合里面來判斷是否有權限(參考:https://blog.csdn.net/xiewenfeng520/article/details/89447749)
。但是我們這個項目有些不同,這個是網關層,權限是在網關做好配置,然后進行判斷能否跳轉到指定的url。我白天一直沒想好怎么去攔截這個url,因為我的表關系里面url地址不是一個必傳的參數。然后看了shiro的源碼實現后,
我發現自己的思路沒問題,但是url必傳。 於是方案改成了這樣:
1)寫一個攔截器,攔截url地址
2)登錄后,將用戶含有的菜單權限放到shiro里面
3)在攔截器里面判斷,如果是管理員,直接通過,如果不是,看該用戶是否有該權限。
具體實現代碼:
1) zuul攔截器:
package com.sq.transportmanage.gateway.api.web.filter; import com.alibaba.fastjson.JSONObject; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.sq.transportmanage.gateway.api.common.AuthEnum; import com.sq.transportmanage.gateway.service.shiro.realm.SSOLoginUser; import com.sq.transportmanage.gateway.service.shiro.session.WebSessionUtil; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.util.List; /** * @program: sq-union-manage * @description: AccessFilter * @author: zjw * @create: 2020-02-23 18:57 **/ @Component @RequiresPermissions("/") public class AccessFilter extends ZuulFilter { private static Logger logger = LoggerFactory.getLogger(AccessFilter.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { RequestContext ctx = RequestContext.getCurrentContext(); HttpServletRequest request = ctx.getRequest(); logger.info(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString())); SSOLoginUser loginUser = WebSessionUtil.getCurrentLoginUser(); logger.info(String.format("%s loginUser %s", loginUser.getLoginName(), loginUser.getName())); /**用戶是否有權限**/ boolean bl = false; //如果是管理員 直接通過 if(AuthEnum.MANAGE.getAuthId().equals(loginUser.getAccountType())){ bl = true; }else { String uri = request.getRequestURI().toString(); List<String> menuUrl = loginUser.getMenuUrlList(); if(menuUrl.contains(uri)){ bl = true; } } if(bl){ ctx.addZuulRequestHeader("user_token",JSONObject.toJSONString(loginUser)); }else{ ctx.setSendZuulResponse(false);// 過濾該請求,不對其進行路由 ctx.setResponseStatusCode(401);// 返回錯誤碼 ctx.setResponseBody("{\"code\":0,\"result\":\"網關驗證失敗!驗證方式為2\"}");// 返回錯誤內容 ctx.set("isSuccess", false); } //TODO 在此處增加權限判斷即可 return ctx; } }
2)shiro存放權限:
package com.sq.transportmanage.gateway.service.shiro.realm; import com.sq.transportmanage.gateway.dao.entity.driverspark.CarAdmUser; import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasPermissionExMapper; import com.sq.transportmanage.gateway.dao.mapper.driverspark.ex.SaasRoleExMapper; import com.sq.transportmanage.gateway.service.auth.MyDataSourceService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.HashSet; import java.util.List; import java.util.Set; /**認證 與 權限 **/ /** * 這個就是shiro SSOLogin 的用戶獲取的屬性配置 */ @Component public class UsernamePasswordRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(UsernamePasswordRealm.class); @Autowired private MyDataSourceService myDataSourceService; @Autowired private SaasPermissionExMapper saasPermissionExMapper; @Autowired private SaasRoleExMapper saasRoleExMapper; /**重寫:獲取用戶的身份認證信息**/ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException{ logger.info( "[獲取用戶的身份認證信息開始]authenticationToken="+authenticationToken); try { UsernamePasswordToken token = (UsernamePasswordToken)authenticationToken; CarAdmUser adMUser = myDataSourceService.queryByAccount(token.getUsername()); SSOLoginUser loginUser = new SSOLoginUser(); //當前登錄的用戶 loginUser.setId( adMUser.getUserId() ); //用戶ID loginUser.setLoginName( adMUser.getAccount() );//登錄名 loginUser.setMobile( adMUser.getPhone() ); //手機號碼 loginUser.setName( adMUser.getUserName() ); //真實姓名 loginUser.setEmail(adMUser.getEmail()); //郵箱地址 loginUser.setType( null ); // loginUser.setStatus( adMUser.getStatus() ); //狀態 loginUser.setAccountType( adMUser.getAccountType() ); //自有的帳號類型:[100 普通用戶]、[900 管理員] loginUser.setLevel(adMUser.getLevel()); loginUser.setUuid(adMUser.getUuid()); List<String> menuUrlList = saasPermissionExMapper.queryPermissionCodesOfUser(adMUser.getUserId() ); loginUser.setMenuUrlList(menuUrlList); //---------------------------------------------------------------------------------------------------------數據權限BEGIN logger.info( "[獲取用戶的身份認證信息]="+loginUser); return new SimpleAuthenticationInfo(loginUser, authenticationToken.getCredentials() , this.getName() ); } catch (Exception e) { logger.error("獲取用戶的身份認證信息異常",e); return null; } } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SSOLoginUser loginUser = (SSOLoginUser) principalCollection.getPrimaryPrincipal(); String account = loginUser.getLoginName(); //登錄名 List<String> perms_string = saasPermissionExMapper.queryPermissionCodesOfUser( loginUser.getId() ); List<String> roles_string = saasRoleExMapper.queryRoleCodesOfUser( loginUser.getId() ); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Set<String> roles = new HashSet<String>( roles_string ); authorizationInfo.setRoles( roles ); logger.info( "[獲取用戶授權信息(角色)] "+account+"="+roles); Set<String> perms = new HashSet<String>( perms_string ); authorizationInfo.setStringPermissions(perms); logger.info( "[獲取用戶授權信息(權限)] "+account+"="+perms); return authorizationInfo; } @Override public Object getAuthorizationCacheKey(PrincipalCollection principals) { SSOLoginUser loginUser = (SSOLoginUser) principals.getPrimaryPrincipal(); String account = loginUser.getLoginName(); //登錄名 return "-AuthInfo-"+account; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } }
大概思路是這樣。。。