按鈕基本的權限校驗實現
目錄
簡述
哈哈哈哈哈哈,我答應會經常更新的。很好,再一次做不到,打臉!來來來寫寫博客吹吹牛逼來了!
是這樣的目前很多業務系統都希望系統的權限是可以動態可調整的,而且還希望系統的權限可以精確到按鈕級別的。通常會這樣做先做個按鈕和菜單的功能配置表,去控制前端按鈕的顯示與否。然後再做一個接口管理表,管理所有接口在某個角色當中是否有權限調用。而且我們需要通過前端頁面去配置這些接口和按鈕。
今天就說一下我是怎麼去實現,不需要去配置有什麼接口,有什麼按鈕的動態權限,同時我也會基於AOP去實現權限認證。
核心類
一、Action 類
主要是記錄系統裏面有多少個操作,每個接口就算一個操作,我們將功能按鈕和對應的接口抽象成一個Action 每一個Action都需要定義一個全局唯一的ActionId,Action還關聯着對應的菜單 menuId(主要生成權限樹)。
@Entity
@Table(name = "T_ACTION")
public class Action {
public static final int STATUS_ENABLE = 1;
public static final int STATUS_DISABLE = 0;
/**
* 行爲ID 全局唯一 英文標識
*/
@Id
@Column(nullable = false)
private String actionId;
/**
* 行爲名稱 主要用作顯示
*/
@Column(nullable = false)
private String actionName;
/**
* 行爲對應的接口URI
*/
@Column(nullable = false, length = 256)
private String actionUri;
/**
* 行爲所屬的菜單ID
*/
@Column(nullable = false)
private String menuId;
@Column(nullable = false, length = 6)
private int status;
}
二、Menu 類
菜單對象,主要用於是否需要顯示前端菜單項的。判斷可以通過菜單以及子孫菜單是否有相應的action權限,如果當前角色 一個action的權限都沒有那就直接將菜單隱藏,這樣在返回給前端的權限樹當中,就不會有相應的菜單。
Entity
@Table(name = "T_MENU")
public class Menu {
public static final int STATUS_ENABLE = 1;
public static final int STATUS_DISABLE = 0;
public Menu() {
}
public Menu(String menuId, String menuName, String parentMenuId) {
this.menuId = menuId;
this.menuName = menuName;
this.parentMenuId = parentMenuId;
this.status = STATUS_ENABLE;
}
/**
* 菜單ID 全局唯一標識
*/
@Id
@Column(nullable = false)
private String menuId;
/**
* 菜單名稱 用於顯示
*/
@Column(nullable = false)
private String menuName;
/**
* 父菜單ID 如果沒有父級菜單ID 即爲頂級菜單
*/
private String parentMenuId;
/**
* 權限狀態如果爲0則功能已經不存在 或者 已經被取消權限判斷
*/
@Column(nullable = false, length = 6)
private int status;
}
三、Role 類
Role 主要是配置和管理有那些求權限,同時就是關聯着相應的管理員用戶。
@Entity
@Table(name = "T_ROLE")
public class Role {
public static final int STATUS_DISABLE = 0;
public static final int STATUS_ENABLE = 1;
@Id
@GeneratedValue
@Column(nullable = false)
private Long roleId;
/**
* 角色
*/
@Column(nullable = false)
private String name;
/**
* 角色描述
*/
@Column(nullable = false,length = 300)
private String description;
/**
* 角色狀態
*/
@Column(nullable = false)
private int status;
/**
* 角色所對應的權限
*/
@ManyToMany(targetEntity = Action.class)
@JoinTable(name = "T_ACTION_ROLE", joinColumns = {@JoinColumn(name = "ROLE_ID")}, inverseJoinColumns = {@JoinColumn(name = "ACTION_ID")})
private Set<Action> actions;
@OneToMany(mappedBy = "role")
private Set<AdminUser> adminUsers;
/**
* 創建角色的時間
*/
@Column(nullable = false)
private Date createTime;
}
分析
目前我們要做到動態權限要做到幾個地方:
- 做到接口即Action,定義了接口就等於定義了相關的Action功能,之後我們會通過這個Action去控制權限;
- 需要配置菜單,動態關聯上對應的Action;
- 每次啓動服務,可以檢查整理出最新的action列表(與你的接口變更一致)和 menu列表;
- 用戶登錄的時候,將用戶對應角色擁有的菜單和對應的action 組織成一個權限樹,返回給前端渲染菜單和按鈕;
- 用戶請求接口時候,將會通過AOP讀取接口定義的ActionId,通過用戶對應關係獲得該用戶的角色是否有權限訪問該接口;
- 提供接口配置相應的角色權限;
權限配置定義和讀取
一、Action配置
我們會定義一個annotation,通過annotation配置到對應的接口上,用作actionId定義和AOP校驗的切入點。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
String actionId();
String menuId();
/**
* 功能名稱
* @return
*/
String name();
}
另外還有一個是不檢查權限,只是需要當前用戶一定是已經登錄並且可以獲得其用戶信息。我將會定義另外一個annotation進行判斷
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authorization {
}
定義完成這兩個annotation之後,我將使用這兩個annotation定義在ResController對應的方法當中。使用方法如下:
@RestController
@RequestMapping("/decorate")
public class DecorateController {
@Autowired
private IDecorateService decorateService;
@GetMapping("/view")
@Permission(actionId = "decorateManage_viewBtn", menuId = "decorateManage", name = "查看小程序裝修")
public ResponseBasic<DecorateRsp> view() {
return ResponseBasic.ok(decorateService.view());
}
}
可以看到我在/view 這個接口上面,定義了一個名稱爲 查看小程序裝修 actionId 爲 decorateManage_viewBtn 屬於 decorateManage 菜單的權限,這個權限將會是全局唯一的。同時我們這個actionId 在用戶登錄的時候用戶擁有什麼權限actionId 會全部給前端渲染頁面。
二、菜單配置
我們需要定義個菜單配置的類,其實只是配置一個bean將所有配置的菜單保存起來
/**
* Created by TONY YAN
*/
public class PermissionConfig {
private Set<Menu> menus = new HashSet<>();
public Set<Menu> getMenus() {
return this.menus;
}
public PermissionConfig addMenu(Menu menu) {
menus.add(menu);
return this;
}
}
然後就開始配置菜單,在其中一個配置類裏面配置定義好的PermissionConfig,定義好各個菜單的名稱和menuId,還有就是就是父級的menuId
@Bean
public PermissionConfig menuDefinition() {
PermissionConfig permissionConfig = new PermissionConfig();
permissionConfig.addMenu(new Menu("staffManageIndex", "員工管理", null));
permissionConfig.addMenu(new Menu("roleManage", "角色管理", "staffManageIndex"));
permissionConfig.addMenu(new Menu("staffManage", "員工管理", "staffManageIndex"));
permissionConfig.addMenu(new Menu("storeManage", "門店管理", "staffManageIndex"));
permissionConfig.addMenu(new Menu("orderManageIndex", "訂單管理", null));
permissionConfig.addMenu(new Menu("orderSurvey", "訂單概況", "orderManageIndex"));
permissionConfig.addMenu(new Menu("orderManage", "所有訂單", "orderManageIndex"));
permissionConfig.addMenu(new Menu("goodsManage", "商品管理", null));
permissionConfig.addMenu(new Menu("memberManage", "會員管理", null));
permissionConfig.addMenu(new Menu("decorateManage", "小程序裝修", null));
permissionConfig.addMenu(new Menu("cardManage", "禮品卡管理", null));
return permissionConfig;
}
三、配置讀取生成權限表
OK,那我們還需要提供一個藉口給前端用戶,選擇某個角色應該擁有什麼權限。所以我們需要每次啓動SpringBoot應用的時候,將所有其本身定義的所有權限action給記錄起來。需要完成這一步的思路是這樣的,我們需要在SpringBoot啓動後,然後掃描Spring容器裏面所有的Bean找到對應的@Permission的配置。上代碼:
1、我們需要實現initializingBean接口,在afterPropertiesSet 方法裏面獲得Spring的context,然後通過獲得所有的RestController類的Bean。
/**
* Created by TONY YAN
*/
@Component
public class PermissionInitializing implements InitializingBean {
@Autowired
private ApplicationContext context;
@Autowired
private PermissionService permissionService;
@Override
public void afterPropertiesSet() throws Exception {
Map<String, Object> beans = this.context.getBeansWithAnnotation(RestController.class);
this.permissionService.permissionInit(beans.values());
}
}
2、permissionInit這個方法就是讀取配置的關鍵
/**
* Created by TONY YAN
*/
@Service
@Transactional
public class PermissionService {
@Autowired
private ActionRepository actionRepository;
@Autowired
private MenuRepository menuRepository;
@Autowired
private PermissionConfig permissionConfig;
@Autowired
private JdbcTemplate jdbcTemplate;
private Logger logger = LoggerFactory.getLogger(PermissionService.class);
/**
* 權限表自動化生成
* @param beans
* @throws Exception
*/
public void permissionInit(Collection<Object> beans) throws Exception {
List<Action> actions = new LinkedList<>();
this.menuRepository.setAllAMenuDisable();
this.actionRepository.setAllActionDisable();
for (Object obj : beans) {
Object targetObj = ProxyUtils.getTarget(obj);
List<Action> actionListByClz = this.ingestActionList(targetObj.getClass());
actions.addAll(actionListByClz);
}
ambiguityActionCheck(actions);
this.actionRepository.save(actions);
Set<Menu> menus = this.permissionConfig.getMenus();
checkActionOutOfMenu(actions, menus);
this.menuRepository.save(menus);
checkAndRemoveDisableMenu();
checkAndRemoveDisableAction();
logger.info("Lotus permission Initialized");
}
/**
* 檢查action權限對應的菜單是否存在
* @param actions
* @param menus
*/
public void checkActionOutOfMenu(List<Action> actions, Set<Menu> menus) {
Set<String> menuIdSet = menus.stream().map(menu -> menu.getMenuId()).collect(Collectors.toSet());
for (Action action : actions) {
if (!menuIdSet.contains(action.getMenuId())) {
logger.warn("Lotus permission Initializing warning : Action id -> "
+ action.getActionId() + " is out of any menus by configuration. " +
" Cause menuID -> " + action.getMenuId() + " undefined ." +
" ActionInfo = "
+ action.getActionName() + "[" + action.getActionUri() + "] ");
}
}
}
/**
* 將所有已經被移除掉的menu全部刪掉
*/
public void checkAndRemoveDisableMenu() {
this.menuRepository.deleteByStatus(0);
}
/**
* 刪除所有已經沒有用的action、action和角色多對多的關聯。
*/
public void checkAndRemoveDisableAction() {
List<Action> actions = this.actionRepository.findByStatus(0);
for (Action action : actions) {
jdbcTemplate.execute("delete from t_action_role where ACTION_ID = '" + action.getActionId() + "'");
}
this.actionRepository.deleteByStatus(0);
}
/**
* 所有actionID的全局唯一性檢查,如果有衝突馬上拋異常阻止SpringBoot初始化啓動
* @param actions
*/
private void ambiguityActionCheck(List<Action> actions) {
Map<String, Integer> checkMap = new HashMap<>();
for (Action action : actions) {
Integer counter = checkMap.get(action.getActionId());
if (counter == null) {
checkMap.put(action.getActionId(), 1);
} else {
if (++counter > 1) {
throw new PermssionConfigException("Permission config error actionId ambiguity , more then one " + action.getActionId());
}
}
}
}
/**
* 通過一個RESTController 類獲得所有的action對象列表
* @param clz
* @return
*/
private List<Action> ingestActionList(Class clz) {
Method[] methodArray = clz.getDeclaredMethods();
List<Action> actions = new ArrayList<>();
for (Method method : methodArray) {
if (method.getAnnotation(Permission.class) != null) {
Action action = this.ingestAction(method);
actions.add(action);
}
}
return actions;
}
/**
* 通過一個RESTController的方法,獲得一個Action對象
* @param method
* @return
*/
private Action ingestAction(Method method) {
Permission permission = method.getAnnotation(Permission.class);
if (permission == null) {
return null;
}
Action action = new Action();
action.setActionUri(ingestActionUri(method));
action.setActionName(permission.name());
action.setMenuId(permission.menuId());
action.setActionId(permission.actionId());
action.setStatus(Action.STATUS_ENABLE);
return action;
}
/**
* 獲得URI
* @param method
* @return
*/
private String ingestActionUri(Method method) {
String methodUriPart = "";
GetMapping getMapAn = method.getAnnotation(GetMapping.class);
if (getMapAn != null) {
String[] paths = getMapAn.path() == null || getMapAn.path().length <= 0 ? getMapAn.value() : getMapAn.path();
methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
}
RequestMapping reqMapAn = method.getAnnotation(RequestMapping.class);
if (reqMapAn != null) {
String[] paths = reqMapAn.path() == null || reqMapAn.path().length <= 0 ? reqMapAn.value() : reqMapAn.path();
methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
}
PostMapping postMapAn = method.getAnnotation(PostMapping.class);
if (postMapAn != null) {
String[] paths = postMapAn.path() == null || postMapAn.path().length <= 0 ? postMapAn.value() : postMapAn.path();
methodUriPart = paths != null && paths.length > 0 ? paths[0] : "";
}
String classUriPart = "";
RequestMapping clzReqMapAn = method.getDeclaringClass().getAnnotation(RequestMapping.class);
if (clzReqMapAn != null) {
String[] paths = clzReqMapAn.path() == null || clzReqMapAn.path().length <= 0 ? clzReqMapAn.value() : clzReqMapAn.path();
classUriPart = paths != null && paths.length > 0 ? paths[0] : "";
}
boolean hasSeparator = false;
boolean moreThenOneSeparator = false;
if (!StringUtils.isEmpty(methodUriPart)) {
hasSeparator = methodUriPart.indexOf("/") == 0;
}
if (!StringUtils.isEmpty(classUriPart)) {
if (hasSeparator && classUriPart.indexOf("/") == classUriPart.length() - 1) {
moreThenOneSeparator = true;
} else if (!hasSeparator) {
hasSeparator = classUriPart.indexOf("/") == classUriPart.length() - 1;
}
}
String uri = "";
if (!StringUtils.isEmpty(methodUriPart) || !StringUtils.isEmpty(classUriPart)) {
if (moreThenOneSeparator) {
methodUriPart = methodUriPart.substring(1, methodUriPart.length());
uri = classUriPart + methodUriPart;
} else if (hasSeparator) {
uri = classUriPart + methodUriPart;
} else {
uri = classUriPart + "/" + methodUriPart;
}
if (uri.indexOf("/") != 0) {
uri = "/" + uri;
}
}
return uri;
}
}
雖然有點長但是還是算比較簡單,然後說說這個permissionInit方法幹了什麼。首先將以前舊的所有Action和Menu表的數據設置成無效,然後開始迭代所有bean,在這裏有個地方需要注意的Object targetObj = ProxyUtils.getTarget(obj); 。其中ProxyUtils的類後面會貼出來,意思是這樣的,首先我們在Spring容器獲得的所有Bean基本上都是被CGLib 或者 JdkDynamicProxy代理過的,所以我們這邊獲得的所有都是代理對象,導致我們獲得class對象都是一個代理類,導致我們沒有辦法讀取到原始類裏面的@permission配置。所以通過ProxyUtils 獲得真實的原來對象。
然後通過ingestActionList 方法讀取出類裏面的所有@Permission配置,然後通過ambiguityActionCheck方法檢查其權限是否衝突,如果衝突就拋出異常(消息可以看看其代碼),然後將權限保存到表當中,由於生成的權限status都是在生效狀態,等於如果是上次啓動的時候已經存在的權限,在這次保存的時候得到更新從失效改成了生效,而新的權限記錄將會插入一條數據。
通過PermissionConfig 對象(之前配置過的),獲得所有的菜單對象,並且保存。也是同樣道理以前已經存在的菜單對象在這次從新保存的時候得到了更新從一個開始的失效狀態,更新成生效狀態。
檢查@Permission配置的Action定義的所屬菜單是否存在,如果沒有對應menuId的菜單對象,就給出warning的日誌消息。
然後將所有狀態爲失效的Action和Menu記錄刪除,保持最終的權限條目結果。然後完成初始化,下面帖出ProxyUtils的代碼:
public class ProxyUtils {
public static Object getTarget(Object proxy) throws Exception {
if (!AopUtils.isAopProxy(proxy)) {
return proxy;
}
if (AopUtils.isJdkDynamicProxy(proxy)) {
return getJdkDynamicProxyTargetObject(proxy);
} else {
return getCglibProxyTargetObject(proxy);
}
}
private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");
h.setAccessible(true);
Object dynamicAdvisedInterceptor = h.get(proxy);
Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();
return target;
}
private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
Field h = proxy.getClass().getSuperclass().getDeclaredField("h");
h.setAccessible(true);
AopProxy aopProxy = (AopProxy) h.get(proxy);
Field advised = aopProxy.getClass().getDeclaredField("advised");
advised.setAccessible(true);
Object target = ((AdvisedSupport) advised.get(aopProxy)).getTargetSource().getTarget();
return target;
}
}
提供接口配置角色
目前已經擁有菜單和功能的基礎條目了,先就差某個角色擁有什麼功能這樣的一個功能了。首先我們需要提供一個功能讓前端獲得整體一個權限樹構成,就是說有那些菜單,裏面有什麼權限和什麼子菜單這樣一個結構。其中我們需要提供所有全局權限樹,還有就是角色的權限樹,還有就是用戶的權限樹等等。這裏就簡單貼出代碼,不算是這裏的核心思想。只要清楚數據庫結構,就能簡單實現界面所需要的接口。
@Service
public class PermissionTreeService {
@Autowired
private ActionRepository actionRepository;
@Autowired
private MenuRepository menuRepository;
@Autowired
private PermissionConfig permissionConfig;
@Autowired
private JdbcTemplate jdbcTemplate;
private Logger logger = LoggerFactory.getLogger(PermissionTreeService.class);
/**
* 獲得角色所有的權限actionID
* @param role
* @return
*/
public Set<String> getUserActionIdList(Role role) {
if (role == null) {
return new HashSet<>();
}
Set<Action> actions = role.getActions();
Set<String> actionIds = actions.stream().map(action -> action.getActionId()).collect(Collectors.toSet());
return actionIds;
}
/**
* 獲得用戶所有的權限actionID
* @param adminUser
* @return
*/
public Set<String> getUserActionIdList(AdminUser adminUser) {
if (adminUser == null) {
return new HashSet<>();
}
Set<Action> actions = null;
if (adminUser.getSuperAdmin() == 99) {
actions = this.actionRepository.findAll().stream().collect(Collectors.toSet());
} else {
actions = adminUser.getRole().getActions();
}
Set<String> actionIds = actions.stream().map(action -> action.getActionId()).collect(Collectors.toSet());
return actionIds;
}
/**
* 獲得用戶的權限樹
* @param adminUser
* @return
*/
public List<MenuVo> userPermissionTree(AdminUser adminUser) {
List<MenuVo> menuVos = this.getActionTree(adminUser);
return this.menuFilterLayer(menuVos);
}
/**
* 獲得角色用戶的權限樹
* @param role
* @return
*/
public List<MenuVo> userPermissionTree(Role role) {
List<MenuVo> menuVos = this.getActionTree(role);
return this.menuFilterLayer(menuVos);
}
/**
* 菜單過濾器層
* @param menuVos
* @return
*/
public List<MenuVo> menuFilterLayer(List<MenuVo> menuVos) {
List<MenuVo> result = new ArrayList<>();
for (MenuVo menuVo : menuVos) {
if (hasMenusPermission(menuVo)) {
if (menuVo.getSubMenus() != null && menuVo.getSubMenus().size() > 0) {
menuVo.setSubMenus(menuFilterLayer(menuVo.getSubMenus()));
}
menuVo.setActions(actionFilterLayer(menuVo.getActions()));
result.add(menuVo);
}
}
return result;
}
/**
* action過濾器層
* @param actionList
* @return
*/
public List<ActionVo> actionFilterLayer(List<ActionVo> actionList) {
List<ActionVo> result = actionList.stream().filter(actionVo -> actionVo.getHasPermission() == 1).collect(Collectors.toList());
return result;
}
public boolean hasMenusPermission(MenuVo menuVo) {
List<ActionVo> actionVos = menuVo.getActions();
for (ActionVo actionVo : actionVos) {
if (actionVo.getHasPermission() != 0) {
return true;
}
}
if (menuVo.getSubMenus() != null && menuVo.getSubMenus().size() > 0) {
for (MenuVo subVo : menuVo.getSubMenus()) {
if (hasMenusPermission(subVo)) {
return true;
}
}
}
return false;
}
public List<MenuVo> getActionTree(Role role) {
Set<String> userActionSet = this.getUserActionIdList(role);
return getActionTreeCore(userActionSet);
}
public List<MenuVo> getActionTree(AdminUser adminUser) {
Set<String> userActionSet = this.getUserActionIdList(adminUser);
return getActionTreeCore(userActionSet);
}
public List<MenuVo> getActionTreeCore(Set<String> userActionSet) {
List<Menu> menus = this.menuRepository.findAll();
List<Action> actions = this.actionRepository.findAll();
List<MenuVo> vos = new ArrayList<>();
for (Menu menu : menus) {
if (menu.getParentMenuId() == null) {
MenuVo vo = new MenuVo();
CommonUtils.copyProperty(menu, vo);
findSubMenus(vo, menus, actions, userActionSet);
findActionInMenu(vo, actions, userActionSet);
vos.add(vo);
}
}
return vos;
}
public void findActionInMenu(MenuVo menuVo, List<Action> actions, Set<String> userActionIdSet) {
List<ActionVo> vos = new ArrayList<>();
for (Action action : actions) {
if (action.getMenuId().equals(menuVo.getMenuId())) {
ActionVo actionVo = new ActionVo();
CommonUtils.copyProperty(action, actionVo);
actionVo.setHasPermission(userActionIdSet.contains(action.getActionId()) ? 1 : 0);
vos.add(actionVo);
}
}
menuVo.setActions(vos);
}
/**
* 獲得子菜單單
* @param menuVo
* @param menus
* @param actions
* @param userActionIdSet
*/
public void findSubMenus(MenuVo menuVo, List<Menu> menus, List<Action> actions, Set<String> userActionIdSet) {
List<MenuVo> subMenuVoList = new ArrayList<>();
for (Menu menu : menus) {
if (menu.getParentMenuId() == null) {
continue;
}
if (menu.getParentMenuId().equals(menuVo.getMenuId())) {
MenuVo subVo = new MenuVo();
CommonUtils.copyProperty(menu, subVo);
subMenuVoList.add(subVo);
}
}
if (subMenuVoList.size() > 0) {
menuVo.setSubMenus(subMenuVoList);
for (MenuVo vo : subMenuVoList) {
findActionInMenu(vo, actions, userActionIdSet);
findSubMenus(vo, menus, actions, userActionIdSet);
}
}
}
}
提供具體的權限和角色綁定的接口這個也是比較簡單的業務了,下面也簡單貼出來一下。哎喲提醒一下我這裏用了大量的深拷貝。
public ResponseBasic createRole(ReqRoleVo req) {
Role role = new Role();
if (this.roleRepository.findByName(req.getName()) != null) {
return ResponseBasic.error(ResponseBasic.ERROR_CODE, "角色名稱名已經存在");
}
role.setCreateTime(new Date());
req.setRoleId(null);
CommonUtils.copyProperty(req, role);
role.setStatus(Role.STATUS_ENABLE);
Set<Action> actions = this.actionRepository.findByActionIdIn(req.getActionIdList());
role.setActions(actions);
this.roleRepository.save(role);
return ResponseBasic.ok();
}
public ResponseBasic<RoleDetailVo> getRoleDetail(ReqGetRole req) {
Role role = this.roleRepository.findOne(req.getRoleId());
RoleDetailVo detailVo = new RoleDetailVo();
CommonUtils.copyProperty(role, detailVo);
List<MenuVo> permissionTree = this.permissionTreeService.getActionTree(role);
detailVo.setPermissionTree(permissionTree);
return ResponseBasic.ok(detailVo);
}
public ResponseBasic<List<MenuVo>> getPermissionTree() {
List<MenuVo> permissionTree = this.permissionTreeService.getActionTreeCore(new HashSet<>());
return ResponseBasic.ok(permissionTree);
}
public ResponseBasic<AppPage<SimpleRoleVo>> getRoleList(ReqSearchRole req) {
Page<Role> page = this.roleRepository.findAll(new PageRequest(req.getPageNo() - 1, req.getPageSize()));
Page<SimpleRoleVo> vos = page.map(new Converter<Role, SimpleRoleVo>() {
@Override
public SimpleRoleVo convert(Role role) {
SimpleRoleVo vo = new SimpleRoleVo();
CommonUtils.copyProperty(role, vo);
vo.setStatusName(role.getStatus() == 0 ? "禁用" : "正常");
vo.setNumOfUser(userRepository.countByRole(role));
return vo;
}
});
return ResponseBasic.ok(new AppPage<>(vos));
}
public ResponseBasic updateRole(ReqRoleVo req) {
Role role = this.roleRepository.findOne(req.getRoleId());
if (role == null) {
return ResponseBasic.error(ResponseBasic.ERROR_CODE, "角色不存在");
}
CommonUtils.copyProperty(req, role);
Set<Action> actions = this.actionRepository.findByActionIdIn(req.getActionIdList());
role.setActions(actions);
this.roleRepository.save(role);
return ResponseBasic.ok();
}
public ResponseBasic<List<RoleItemVo>> getSimpleRoleList() {
List<Role> roles = this.roleRepository.findAll(new Sort("createTime"));
if (roles != null) {
List<RoleItemVo> roleItemVos = roles.stream().map(role -> {
RoleItemVo itemVo = new RoleItemVo();
itemVo.setRoleId(role.getRoleId());
itemVo.setRoleName(role.getName());
return itemVo;
}).collect(Collectors.toList());
return ResponseBasic.ok(roleItemVos);
}
return ResponseBasic.ok(new ArrayList<>());
}
public ResponseBasic<List<RoleListItemVo>> getRoleListPermissionTree() {
List<Role> roles = this.roleRepository.findAll(new Sort(Sort.Direction.DESC, "createTime"));
List<RoleListItemVo> roleListItemVoList = new ArrayList<>();
for (Role role : roles) {
RoleListItemVo listItemVo = new RoleListItemVo();
CommonUtils.copyProperty(role, listItemVo);
listItemVo.setRoleName(role.getName());
listItemVo.setPermissionTree(this.permissionTreeService.userPermissionTree(role));
roleListItemVoList.add(listItemVo);
}
return ResponseBasic.ok(roleListItemVoList);
}
權限校驗
說了那麼多應該都到權限校驗這一部分了,首選我們是通過AOP實現權限校驗的。同時我們使用redis保存用戶的Token令牌,然而我們前端會通過Http header 的authorization 參數將token帶會來進行校驗。而且我們會分成這幾部分去完成整一個權限校驗。
- 通過實現HandlerInterceptor接口,在preHandle方法當中獲得其令牌信息並轉換成用戶對象,保存在ThreadLocal當中;
- 在HandlerInterceptor接口的 afterCompletion 方法當中刪除ThreadLocal的用戶信息緩存,防止內存泄露;
- 提供統一的封裝方法,從而在同一個請求線程當中獲得其用戶對象;
- 通過@Aspect 配置AOP的@Around環繞織入,將所有的@Permission的方法進行AOP動態代理;
- 檢查其ThreadLocal是否獲得已經有用戶的對象,如果沒有則未登錄,直接返回錯誤或者拋出指定異常;
- 獲得被織入的方法的@Permission annotation配置的ActionId 參數值,同時檢查用戶對象的ActionId SET裏面是否包含其ActionId,如果沒有包含,返回錯誤或者拋出指定異常,否則正常執行處理;
- 同樣道理@Authorization的annotation也會進行AOP代理,但是不會檢查用戶的權限列表,只會去看用戶登錄了沒有,如果沒有則返回錯誤或者拋出指定的異常;
好啦,就是核心的幾個類。我這邊貼出來。。。
第一個就是AuthInterceptor 和 AuthService,其中AuthService支持在任何地方進行注入,通過他可以獲得當前登錄的用戶對象。
/**
* Created by TONY YAN
*/
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private AuthService authService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
this.authService.setAdminUserInCache(httpServletRequest);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
this.authService.removeAdminUserInCahce();
}
}
AuthService類其中在獲得用戶令牌的方法當中,也會調用到PermissionTreeService的方法去獲得當前用戶的角色權限樹和ActionId列表,詳細請看代碼註釋。
public class AuthService {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private PermissionTreeService permissionTreeService;
public static long expireDays = 20;
private static ThreadLocal<AuthToken> USER_THREAD_LOCAL = new ThreadLocal<>();
/**
* 將根據token獲得用戶對象並存儲到threadLocal當中
*
* @param request
*/
public void setAdminUserInCache(HttpServletRequest request) {
String token = request.getParameter("Authorization");
if (StringUtils.isEmpty(token)) {
token = request.getHeader("Authorization");
}
if (StringUtils.isEmpty(token)) {
return;
}
AuthToken user = this.getUserByToken(token);
if (user == null) {
return;
}
user.setToken(token);
USER_THREAD_LOCAL.set(user);
}
public void removeAdminUserInCahce() {
USER_THREAD_LOCAL.remove();
}
/**
* 獲得用戶令牌
*
* @param user
* @return
*/
public AuthToken getUserTokenAndSetUser(AdminUser user) {
ValueOperations<String, AuthToken> valOps = this.redisTemplate.opsForValue();
String token = UUID.randomUUID().toString();
String key = this.getRedisUserKey(token);
AuthToken authToken = new AuthToken();
CommonUtils.copyProperty(user, authToken);
authToken.setActionIds(this.permissionTreeService.getUserActionIdList(user));
authToken.setMenuVos(this.permissionTreeService.userPermissionTree(user));
authToken.setToken(token);
valOps.set(key, authToken, expireDays, TimeUnit.MINUTES);
return authToken;
}
/**
* 獲得緩存用戶
*
* @return
*/
public AuthToken getUserByToken() {
return USER_THREAD_LOCAL.get();
}
/**
* 獲得緩存用戶
*
* @param userToken
* @return
*/
public AuthToken getUserByToken(String userToken) {
ValueOperations<String, AuthToken> valOps = this.redisTemplate.opsForValue();
String key = this.getRedisUserKey(userToken);
AuthToken user = valOps.get(key);
if (user != null) {
this.redisTemplate.expire(key, expireDays, TimeUnit.DAYS);
}
return user;
}
/**
* 從redis當中移除用戶
*/
public void removeAdminUserInRedis() {
AuthToken user = this.getUserByToken();
if (user == null) {
return;
}
String key = this.getRedisUserKey(user.getToken());
this.redisTemplate.delete(key);
}
/**
* 獲得用戶token緩存的key
*
* @param userToken
* @return
*/
public String getRedisUserKey(String userToken) {
return PermissionContacts.ADMIN_USER_TOKEN + userToken;
}
}
然後就是核心的PermissionAspect 類,主要是做相關的攔截權限校驗操作。然後你會看到我偷雞,只要獲得當前用戶的superAdmin字段是99的話,我就會賦予全部權限。這是爲什麼了,主要是系統在初始化的時候沒有用戶,那這樣也會妨礙到角色的配置問題,所以纔在這個位置做出取巧的一個方法,當首次配置後,直接將這個用戶的superAdmin去掉然後綁定到其中一個超級管理員權限的角色即可。
/**
* Created by TONY YAN
*/
@Component
@Aspect
public class PermissionAspect {
@Autowired
private AuthService authService;
/**
* 攔截所有@Permission的方法,檢查其當前用戶是否有權限訪問
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("@annotation(com.lotus.admin.permission.annotation.Permission)")
public Object check(ProceedingJoinPoint joinPoint) throws Throwable {
AuthToken authToken = this.authService.getUserByToken();
if (authToken == null) {
return ResponseBasic.error(ResponseBasic.AUTH_ERROR_CODE, "用戶未登陸");
}
String actionId = this.getPermissionMethod(joinPoint).getAnnotation(Permission.class).actionId();
boolean ok = false;
if (authToken.getSuperAdmin() == 99) {
ok = true;
} else {
if (authToken.getActionIds().contains(actionId)) {
ok = true;
}
}
if (ok) {
return joinPoint.proceed(joinPoint.getArgs());
}
return ResponseBasic.error(ResponseBasic.PERMISSION_ERROR_CODE, "你沒有權限使用該功能");
}
/**
* 攔截所有@Authorization的方法,檢查當前調用方法的線程是否已經獲得其用戶對象,如果沒有則未登錄
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("@annotation(com.lotus.admin.permission.annotation.Authorization)")
public Object authCheck(ProceedingJoinPoint joinPoint) throws Throwable {
AuthToken authToken = this.authService.getUserByToken();
if (authToken == null) {
return ResponseBasic.error(ResponseBasic.AUTH_ERROR_CODE, "用戶未登陸");
}
return joinPoint.proceed(joinPoint.getArgs());
}
/**
* 獲得方法對象
* @param joinPoint
* @return
* @throws NoSuchMethodException
*/
private Method getPermissionMethod(ProceedingJoinPoint joinPoint) throws NoSuchMethodException {
Signature sig = joinPoint.getSignature();
MethodSignature msig = null;
if (!(sig instanceof MethodSignature)) {
throw new IllegalArgumentException("該註解只能用於方法");
}
msig = (MethodSignature) sig;
Object target = joinPoint.getTarget();
Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
return currentMethod;
}
}
登錄
其實我不打算在這裏貼出登錄的代碼的,也沒有什麼好貼出來的。反正AuthService已經有獲得令牌的代碼了,就差用戶名校驗這些邏輯了估計大家都會了,我就不打算貼出了其實登錄這裏的核心代碼就是調用了AuthService的public AuthToken getUserTokenAndSetUser(AdminUser user) 方法而已。
結尾
這個權限框架,也是解決了很多業務上的痛點。也算是寫了這麼多年代碼的一個經驗總結吧。其實還是有很多需要進步的東西的,因爲也沒有寫多久才用了一週不到的時間完成。其中有如下的優化想法:
- 其中可以做一個權限依賴的功能,將一個權限依賴某一個權限,這樣的導致用戶定義了用戶有編輯權限當時沒有查看權限的這種尷尬情況;
- 然後也可以做一下intellij IDE插件將權限的actionID全局檢查在編寫代碼的時候進行動態檢查,進一步防止ID衝突;
- 還可以做一個單獨的微服務業務,通過Spring初始化的階段將本服務的信息傳遞給權限認證服務進行認證檢查和衝突檢查等,統一管理整個系統的權限;
- 除了支持按鈕級別的接口和按鈕的顯示控制之外,還可以支持開放的服務接口的權限配置;
- 再進一步就是可以通過統一攔截repository的訪問,通過統一的基類Entity 通過統一數據權限字段去管理數據級別的權限檢驗,做到業務開發不需要感知判斷當前用戶的權限直接調用repository 進行數據查詢,在repository層中將攔截自動加上權限數據篩選的條件,從而做到數據級別的權限。
- 可以將這個東西進一步封裝成一個通用的微服務權限認證框架,這是一個偉大的夢想。。。。
好啦,有什麼就留言我吧。。。絕對原創轉載請註明出處。。。