SpringSecurity 可以使用注解對方法進行細顆粒權限控制,但是很不靈活,必須在編碼期間,就已經寫死權限
其實關於SpringSecurity,大部分類都不需要重寫,需要的只是妥善的配置.
每次修改權限以后,需要讓MetaDataSource刷新 資源-權限 的MAP,這里應該需要做一些處理,或者優化.
這里實現,可以在后台隨時開啟關閉權限,不需要硬編碼寫死.而且資源的RequestMapping,可以是有多個地址
可以根據角色分配權限,也可以精確到為每一個用戶分配權限,模塊,或者方法.
這樣比較靈活,但是UI會很復雜,用戶也不好理解
資源注解:注解使用在控制器類,或者方法中.注解在類中,粗顆粒控制,注解在方法中細顆粒
/** * Created by ZhenWeiLai on on 2016-10-16. */ @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface AclResc { int id();//ACLResource 因為特殊原因不使用 id 自動增長,所以必須自定義ID ,並且不能重復 String code(); String name(); String homePage() default ""; boolean isMenu() default true; }
注解在類:
@AclResc(id = 5000,code = "aclRescUser", name = AclRescUserController.MODULE_NAME,homePage = AclRescUserController.HOME_PAGE) public class AclRescUserController extends BaseController<AclRescUser>
注解在方法:
@RequestMapping(value = "/list",method = RequestMethod.GET) @AclResc(id = 5001,code = "list",name = "用戶資源列表") public ResultDataDto list(){ }
系統完全啟動后,更新資源信息:
/** * Created by ZhenWeiLai on on 2016-10-16. * SpringBoot 啟動完畢做些事情 */ @Component public class ApplicationStartup implements CommandLineRunner { @Resource private AclResourceService aclResourceService; @Resource private AclAuthService aclAuthService; @Resource private RequestMappingHandlerMapping requestMappingHandlerMapping; @Resource private MySecurityMetadataSource securityMetadataSource; @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void run(String... strings) throws Exception { /** * 初始化資源,保存到數據庫 */ initModule(); /** * Spring Security 需要的資源-權限 */ securityMetadataSource.doLoadResourceDefine(); } /** * 讀取所有Controller包括以內的方法 */ private void initModule() { /** * 模塊 - 方法map */ Map<AclResource, List<AclResource>> resourcesMap = new HashMap<>(); Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods(); for (RequestMappingInfo info : map.keySet()) { AclResc moduleAclResc = map.get(info).getBeanType().getAnnotation(AclResc.class); if (moduleAclResc != null) { if (StringUtils.isBlank(moduleAclResc.homePage())) throw new RuntimeException("使用:" + AclResc.class.getName() + " 注解類時,請配置 homePage "); Class<?> aclResourceClass = map.get(info).getBeanType(); RequestMapping moduleMapping = aclResourceClass.getAnnotation(RequestMapping.class); AclResource moduleResc = new AclResource(moduleAclResc.id(), moduleAclResc.code(), moduleAclResc.name(), Arrays.toString(moduleMapping.value()), AclResource.Type.MODULE.getCode(), moduleAclResc.homePage(), moduleAclResc.isMenu()); if (moduleMapping != null) { List<AclResource> resources; AclResource methodResc; Method method = map.get(info).getMethod(); AclResc methodAclResc = method.getAnnotation(AclResc.class); if (methodAclResc != null) { methodResc = new AclResource(methodAclResc.id(), methodAclResc.code(), methodAclResc.name(), info.getPatternsCondition().toString().replace("||", Delimiter.COMMA.getDelimiter()), AclResource.Type.METHOD.getCode(), null); if (resourcesMap.get(moduleResc) == null) { resources = new ArrayList<>(); resources.add(methodResc); resourcesMap.put(moduleResc, resources); } else { resourcesMap.get(moduleResc).add(methodResc); } } } } } addModule(resourcesMap); } /** * 檢查新模塊,添加到數據庫,並更新視圖的模塊ID * * @param resourcesMap */ private void addModule(Map<AclResource, List<AclResource>> resourcesMap) { for (Map.Entry<AclResource, List<AclResource>> item : resourcesMap.entrySet()) { AclResource resultResc = aclResourceService.findEntityById(item.getKey().getId()); //如果模塊是新模塊,那么新增到數據庫 if (resultResc == null) { aclResourceService.addEntity(item.getKey()); List<AclResource> resources = item.getValue(); for (AclResource resc : resources) { resc.setModuleId(item.getKey().getId()); } } else { //如果已存在模塊,那么更新需要的字段 aclResourceService.updateEntity(item.getKey()); List<AclResource> resources = item.getValue(); for (AclResource methodResc : resources) { //方法模塊CODE 根據 模塊CODE + 方法CODE 生成 methodResc.setCode(item.getKey().getCode() + "_" + methodResc.getCode()); methodResc.setModuleId(resultResc.getId()); AclResource oringinalMethodResc = aclResourceService.findEntityById(methodResc.getId()); if (oringinalMethodResc != null) { //RequestMapping可能被修改,所以這里要做一次更新 aclResourceService.updateEntity(methodResc); //同時code也可能被更改,所以更新權限code aclAuthService.updateCodeByRescId(methodResc.getCode(), methodResc.getId()); } else { aclResourceService.addEntity(methodResc); } } } } } }
構建權限菜單:
/** * 根據用戶權限構建菜單 */ @Override public Map<AclMenu, List<AclResource>> getAclUserMenus() { //創建完整的菜單,然后刪除沒有權限的菜單 Map<AclMenu, List<AclResource>> userMenuModuleMap = findAclMenuModuleMap(); //獲取資源/權限集 Map<String, Collection<ConfigAttribute>> moduleMap = securityMetadataSource.getModuleMap(); for (String path : moduleMap.keySet()) { //如果沒有權限 if (!SecurityUtil.hastAnyAuth(moduleMap.get(path))) { Iterator<AclMenu> userMenuModuleMapKey = userMenuModuleMap.keySet().iterator(); while (userMenuModuleMapKey.hasNext()) { AclMenu key = userMenuModuleMapKey.next(); List<AclResource> modules = userMenuModuleMap.get(key); if (modules.isEmpty()) { userMenuModuleMapKey.remove(); continue; } Iterator<AclResource> aclResourceIterator = modules.iterator(); while (aclResourceIterator.hasNext()) { String rescPath = aclResourceIterator.next().getPath(); String[] pathArr = rescPath.substring(1, rescPath.length() - 1).split(Delimiter.COMMA.getDelimiter()); for (String item : pathArr) { if (item.equals(path)) { //從菜單模塊中刪除 aclResourceIterator.remove(); //如果模塊為空 if (modules.isEmpty()) { //刪除菜單 userMenuModuleMapKey.remove(); } } } } } } } return userMenuModuleMap; }
FilterInvocationSecurityMetadataSource:
/** * Created by ZhenWeiLai on 2016-10-16. */ @Component("securityMetadataSource") public class MySecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private static Map<String, Collection<ConfigAttribute>> moduleMap = null; private static Map<String, Collection<ConfigAttribute>> methodMap = null; @Resource private AclResourceService aclResourceService; @Resource private AclRescRoleService aclRescRoleService; @Resource private AclRoleService aclRoleService; @Resource private AclAuthService aclAuthService; @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { Collection<ConfigAttribute> collection; collection = getAttributesHandler(methodMap, object); if (collection != null) return collection; collection = getAttributesHandler(moduleMap, object); return collection; } /** * 處理方法 * * @param map * @return */ private Collection<ConfigAttribute> getAttributesHandler(Map<String, Collection<ConfigAttribute>> map, Object object) { HttpServletRequest request = ((FilterInvocation) object).getRequest(); Iterator var3 = map.entrySet().iterator(); Map.Entry entry; do { if (!var3.hasNext()) { return null; } entry = (Map.Entry) var3.next(); } while (!(new AntPathRequestMatcher(entry.getKey().toString())).matches(request)); return (Collection) entry.getValue(); } //4 @Override public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet(); Map<String, Collection<ConfigAttribute>> all = new HashMap<>(this.moduleMap); all.putAll(this.methodMap); Iterator var2 = all.entrySet().iterator(); while (var2.hasNext()) { Map.Entry<String, Collection<ConfigAttribute>> entry = (Map.Entry) var2.next(); allAttributes.addAll(entry.getValue()); } return allAttributes; } //3 @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } @Transactional(readOnly = true) private void loadResourceDefine() { loadModuleResources(); loadMethodResources(); } /** * 提供一個外部使用方法.獲取module權限MAP; * * @return */ public Map<String, Collection<ConfigAttribute>> getModuleMap() { Map<String, Collection<ConfigAttribute>> map = new HashMap<>(moduleMap); return map; } /** * 提供外部方法讓Spring環境啟動完成后調用 */ public void doLoadResourceDefine() { loadResourceDefine(); } /** * 讀取模塊資源 */ private void loadModuleResources() { /** * 查詢模塊資源權限,配置模塊權限驗證 */ List<AclResource> aclResources = aclResourceService.findAllModule(); //模塊資源為KEY,角色為Value 的list moduleMap = new HashMap<>(); for (AclResource module : aclResources) { /** * 加載所有模塊資源 */ List<AclRescRole> aclRescRoles = aclRescRoleService.findByRescId(module.getId()); /** * 無論如何超級管理員擁有所有權限 */ stuff(new SecurityConfig(SecurityUtil.ADMIN), moduleMap, module.getPath()); for (AclRescRole aclRescRole : aclRescRoles) { Integer roleId = aclRescRole.getRoleId();//角色ID String roleCode = aclRoleService.findEntityById(roleId).getCode();//角色編碼 stuff(new SecurityConfig(roleCode.toUpperCase()), moduleMap, module.getPath()); } } } /** * 讀取精確方法權限資源 */ private void loadMethodResources() { /** * 因為只有權限控制的資源才需要被攔截驗證,所以只加載有權限控制的資源 */ //方法資源為key,權限編碼為 methodMap = new HashMap<>(); List<Map<String, String>> pathAuths = aclAuthService.findPathCode(); for (Map pathAuth : pathAuths) { String path = pathAuth.get("path").toString(); ConfigAttribute ca = new SecurityConfig(pathAuth.get("code").toString().toUpperCase()); stuff(ca, methodMap, path); } } private void stuff(ConfigAttribute ca, Map<String, Collection<ConfigAttribute>> map, String path) { String[] pathArr = path.substring(1, path.length() - 1).split(Delimiter.COMMA.getDelimiter()); for (String item : pathArr) { Collection<ConfigAttribute> collection = map.get(item + "/**"); if (collection != null) { collection.add(ca); } else { collection = new ArrayList<>(); collection.add(ca); String pattern = StringUtils.trimToEmpty(item) + "/**"; map.put(pattern, collection); } } } }
最后:
/** * Created by ZhenWeiLai on on 2016-10-16. * <p> * 三種方法級權限控制 * <p> * 1.securedEnabled: Spring Security’s native annotation * 2.jsr250Enabled: standards-based and allow simple role-based constraints * 3.prePostEnabled: expression-based */ @EnableWebSecurity //@EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Resource private UserDetailsService userDetailsService; @Resource private MySecurityMetadataSource securityMetadataSource; @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**"); web.ignoring().antMatchers("/images/**"); web.ignoring().antMatchers("/js/**"); //忽略登錄界面 web.ignoring().antMatchers("/login"); //注冊地址不攔截 // web.ignoring().antMatchers("/reg"); } @Override protected void configure(HttpSecurity http) throws Exception { //解決不允許顯示在iframe的問題 http.headers().frameOptions().disable(); http.addFilterAt(usernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.authorizeRequests().anyRequest().fullyAuthenticated(); //自定義過濾器 MyFilterSecurityInterceptor filterSecurityInterceptor = new MyFilterSecurityInterceptor(securityMetadataSource,accessDecisionManager(),authenticationManagerBean()); //在適當的地方加入 http.addFilterAt(filterSecurityInterceptor,FilterSecurityInterceptor.class); http.exceptionHandling().authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")).and().logout().logoutUrl("/logout").logoutSuccessUrl("/login").and().exceptionHandling().accessDeniedPage("/accessDenied"); // 關閉csrf http.csrf().disable(); //session管理 //session失效后跳轉 http.sessionManagement().invalidSessionUrl("/login"); //只允許一個用戶登錄,如果同一個賬戶兩次登錄,那么第一個賬戶將被踢下線,跳轉到登錄頁面 http.sessionManagement().maximumSessions(1).expiredUrl("/login"); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 自定義UserDetailsService,設置加密算法 auth.userDetailsService(userDetailsService); //.passwordEncoder(passwordEncoder()) //不刪除憑據,以便記住用戶 auth.eraseCredentials(false); } UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter() throws Exception { UsernamePasswordAuthenticationFilter usernamePasswordAuthenticationFilter = new UsernamePasswordAuthenticationFilter(); usernamePasswordAuthenticationFilter.setPostOnly(true); usernamePasswordAuthenticationFilter.setAuthenticationManager(this.authenticationManager()); usernamePasswordAuthenticationFilter.setUsernameParameter("name_key"); usernamePasswordAuthenticationFilter.setPasswordParameter("pwd_key"); usernamePasswordAuthenticationFilter.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/checkLogin", "POST")); usernamePasswordAuthenticationFilter.setAuthenticationFailureHandler(simpleUrlAuthenticationFailureHandler()); usernamePasswordAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler()); return usernamePasswordAuthenticationFilter; } // @Bean // public LoggerListener loggerListener() { // System.out.println("org.springframework.security.authentication.event.LoggerListener"); // return new LoggerListener(); // } // // @Bean // public org.springframework.security.access.event.LoggerListener eventLoggerListener() { // System.out.println("org.springframework.security.access.event.LoggerListener"); // return new org.springframework.security.access.event.LoggerListener(); // } /** * 投票器 */ private AbstractAccessDecisionManager accessDecisionManager() { List<AccessDecisionVoter<? extends Object>> decisionVoters = new ArrayList(); decisionVoters.add(new AuthenticatedVoter()); decisionVoters.add(new RoleVoter());//角色投票器,默認前綴為ROLE_ RoleVoter AuthVoter = new RoleVoter(); AuthVoter.setRolePrefix("AUTH_");//特殊權限投票器,修改前綴為AUTH_ decisionVoters.add(AuthVoter); AbstractAccessDecisionManager accessDecisionManager = new AffirmativeBased(decisionVoters); return accessDecisionManager; } @Override public AuthenticationManager authenticationManagerBean() { AuthenticationManager authenticationManager = null; try { authenticationManager = super.authenticationManagerBean(); } catch (Exception e) { e.printStackTrace(); } return authenticationManager; } /** * 驗證異常處理器 * * @return */ private SimpleUrlAuthenticationFailureHandler simpleUrlAuthenticationFailureHandler() { return new SimpleUrlAuthenticationFailureHandler("/getLoginError"); } // /** // * 表達式控制器 // * // * @return // */ // private DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() { // DefaultWebSecurityExpressionHandler webSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler(); // return webSecurityExpressionHandler; // } // /** // * 表達式投票器 // * // * @return // */ // private WebExpressionVoter webExpressionVoter() { // WebExpressionVoter webExpressionVoter = new WebExpressionVoter(); // webExpressionVoter.setExpressionHandler(webSecurityExpressionHandler()); // return webExpressionVoter; // } // Code5 官方推薦加密算法 // @Bean("passwordEncoder") // public BCryptPasswordEncoder passwordEncoder() { // BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); // return bCryptPasswordEncoder; // } // // Code3---------------------------------------------- /** * 登錄成功后跳轉 * 如果需要根據不同的角色做不同的跳轉處理,那么繼承AuthenticationSuccessHandler重寫方法 * * @return */ private SimpleUrlAuthenticationSuccessHandler authenticationSuccessHandler() { return new SimpleUrlAuthenticationSuccessHandler("/loginSuccess"); } /** * Created by ZhenWeiLai on on 2016-10-16. */ public static class MyFilterSecurityInterceptor extends FilterSecurityInterceptor { public MyFilterSecurityInterceptor(FilterInvocationSecurityMetadataSource securityMetadataSource, AccessDecisionManager accessDecisionManager, AuthenticationManager authenticationManager){ this.setSecurityMetadataSource(securityMetadataSource); this.setAccessDecisionManager(accessDecisionManager); this.setAuthenticationManager(authenticationManager); } } }