好久沒有寫了,之前只寫了一半,我是一邊開發一邊寫Blog一邊上班,所以真心沒有那麼多時間來維護Blog,項目已經開發到編寫邏輯及頁面部分了,框架基本上已經搭建好不會再修改了,數據庫也擴充了好多了。目前前端的技術框架使用的是BootStrap,集成了幾個不錯的插件這邊列舉一下,給大家做一個參考:
好了,現在還要繼續講解Security的集成工作。
目錄:resource/config/spring,文件名:applicationContext-security.xml
<sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/>
繼續上一篇文章,接下來要講的就是這個登出的配置了。
1 <!--session管理及單點登錄--> 2 <sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/> 3 <!--session管理器 start--> 4 <bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> 5 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 6 <constructor-arg name="expiredUrl" value="/user/timeout"/> 7 </bean> 8 9 <bean id="concurrentSessionControlStrategy" 10 class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy"> 11 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 12 <property name="maximumSessions" value="1"/> 13 </bean> 14 15 <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/> 16 <!--session管理器 end-->
這個是單點登錄的管理配置,這個簡單說一下吧,expiredUrl這個參數呢,是當session失效之後,頁面的跳轉地址。maximunSessions指的是最大的session數,如果是限制賬號只能單點登錄的話,自然要配置為“1”。而sessionRegistry這個是Spring自帶實現,我就不多解釋了,大家可以自己去看SessionRegistryImpl這個實現類。
1 <!--資源攔截器配置--> 2 <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> 3 <sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/> 4 5 <!--資源攔截器 start--> 6 <bean id="filterSecurityInterceptor" 7 class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> 8 <property name="accessDecisionManager" ref="accessDecisionManager"/> 9 <property name="authenticationManager" ref="myAuthenticationManager"/> 10 <property name="securityMetadataSource" ref="resourceSecurityMetadataSource"/> 11 </bean>
第一個是資源攔截器,可以看得見,這是一個Filter。第二個是剛才設置的單點登錄Filter。順帶講一下,我不知道是為什麼,配置的第一個Filter點擊ref名字的時候,可以自動鏈接跳轉,但是後面添加的Filter都統統會提示找不到,但實際上是生效的就是了。
然後來講講資源攔截器中的三個屬性:
然後來說說,認證管理器的配置:
<!--認證管理器--> <sec:authentication-manager alias="myAuthenticationManager"> <sec:authentication-provider ref="daoAuthenticationProvider"/> </sec:authentication-manager>
<bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> <property name="messageSource" ref="messageSource"/> <property name="passwordEncoder" ref="messageDigestPasswordEncoder"/> <property name="userDetailsService" ref="cachingUserDetailsService"/> <property name="saltSource" ref="saltSource"/> <property name="hideUserNotFoundExceptions" value="false"/> </bean>
<!--認證處理服務--> <bean id="cachingUserDetailsService" class="org.springframework.security.config.authentication.CachingUserDetailsService"> <constructor-arg name="delegate" ref="webUserDetailsService"/> <property name="userCache"> <bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache"> <property name="cache" ref="userEhCacheFactory"/> </bean> </property> </bean>
這個是認證管理器的配置,其中daoAuthenticationProvider主要用作與認證時查詢數據庫獲取數據庫存儲的認證信息,比如用戶名對應的密碼。
messageSource是用於國際化的,這個你們看著配,非必要功能。
passwordEncoder,主要是用於密碼加密的,
userDetailsService,這個是用於查找用戶信息的類,
saltSource,這個是加密鹽值,這個情況是這樣子,我們存在數據庫中的密碼,向來不是明文,都是密文存儲,所以在訪問密碼的時候,都是將用戶的密碼進一步的加密後再跟系統數據庫中的值進行比較,鹽值的概念,我不知道怎麼解釋,給我的理解就是有它進行加密的話會更安全。
hideUserNotFoundException,這個就跟字面意思一樣,因此找不到用戶的異常,實際上這個異常不應該被隱藏,而是需要拋出,然後錯誤信息直接反饋到前端頁面上,提示用戶找不到用戶名。
接下來我們來講解一下這幾個屬性對應的類,然而要涉及到另一個配置文件,因為這些東西,不僅僅屬於SpringSecurity,而更廣泛的適用於整個框架中,作為一種Service的角色來使用。
目錄:resource/config/spring,文件名:applicationContext-service.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> 6 7 <!--掃描service--> 8 <context:component-scan base-package="com.magic.rent.service"/> 9 <!--注冊統一異常控制--> 10 <bean id="exception" class="com.magic.rent.exception.exhandler.CustomExceptionHandler"/> 11 <!--MD5加密--> 12 <bean id="messageDigestPasswordEncoder" 13 class="org.springframework.security.authentication.encoding.MessageDigestPasswordEncoder"> 14 <constructor-arg name="algorithm" value="MD5"/> 15 </bean> 16 <!--國際化配置--> 17 <bean id="messageSource" 18 class="org.springframework.context.support.ResourceBundleMessageSource"> 19 <property name="basename" value="messages"/> 20 </bean> 21 <bean id="messageSourceAccessor" class="org.springframework.context.support.MessageSourceAccessor"> 22 <constructor-arg ref="messageSource"/> 23 </bean> 24 </beans>
所有的中文,都要轉換成UTF-8的編碼,這個文件,在SpringSecurity中又自帶的,可以直接拿來用,地址是:
org/springframework/security/spring-security-core/4.1.3.RELEASE/spring-security-core-4.1.3.RELEASE.jar!/org/springframework/security/messages_zh_CN.properties
然後這邊我寫了一個轉換的工具類:
package com.magic.rent.util; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; /** * 創建者: wuxinzhe 創建時間: 16/10/6 * 類說明: UTF-8的中文轉換類 */ public class UTF8Util { /** * "/"分隔符 * * @param str * @return */ public static String GBK2Unicode(String str) { StringBuffer result = new StringBuffer(); for (int i = 0; i < str.length(); i++) { char chr = str.charAt(i); if (!isNeedConvert(chr)) { result.append(chr); continue; } result.append("\\u" + Integer.toHexString((int) chr)); } return result.toString(); } public static boolean isNeedConvert(char para) { return ((para & (0x00FF)) != para); } /** * &#分隔符 * * @param str * @return */ public static String GBK2Unicode2(String str) { StringBuffer result = new StringBuffer(); for (int i = 0; i < str.length(); i++) { char chr = str.charAt(i); result.append("&#" + Integer.toString((int) chr) + ";"); } return result.toString(); } public static void main(String[] args) throws IOException { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); String str = br.readLine(); System.out.println("UTF-8:" + GBK2Unicode(str)); System.out.println("UTF-82:" + GBK2Unicode2(str)); } }
運行後在控制台輸入你要轉換的中文或英文或標點,然後回車後會自動轉換成兩種不同格式的UTF-8編碼。還算挺方便的。
然後大家還能看到我配置了一個MessageSourceAccess,這個是做什麼的呢?這是一個國際化的工具類,非常好用,我隨便拿我項目中的一個例子給大家演示:
可以看到,這個對象有兩個參數(這個對象我是寫在BaseController當中,通過繼承獲取,因為這個算是通用的屬性。),第一個參數就是像message的配置文件中查找,看是否有配置這個對應的文字,如果沒有的話,就采用第二個參數中的值,即默認值,進行返回。到此,我們再回到SpringSecurity的配置文件中,繼續講解:
1 <!--MD5加密鹽值--> 2 <bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource"> 3 <property name="userPropertyToUse" value="username"/> 4 </bean>
這就是鹽值的配置了,這個配置的意思,就是說,將用戶的用戶名,作為加密時的混入MD5的加密中,增強密碼的加密強度。當然你也可以不一定用用戶名而是其他的什麼值。
然後貼出這個securityMetadataSource的類代碼,這個沒有什麼特殊的,就從數據庫中獲取數據而已,我留了一個手動刷新的方法,主要是用於後續如果有更新權限的情況下,不需要重啟服務器,就可以刷新權限列表,因為我們再啟動項目的時候,將數據庫中的權限數據一次性加載到內存中,而後續對比權限的時候,實際上只跟內存中的數據對比相當於一個緩存的作用。
package com.magic.rent.service.security; import com.magic.rent.mapper.SysResourcesMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.stereotype.Service; import javax.servlet.http.HttpServletRequest; import java.util.*; @Service public class ResourceSecurityMetadataSource implements FilterInvocationSecurityMetadataSource, InitializingBean { private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = Collections.emptyList(); //權限集合 private Map<RequestMatcher, Collection<ConfigAttribute>> requestMap; private static Logger logger = LoggerFactory.getLogger(ResourceSecurityMetadataSource.class); @Autowired private SysResourcesMapper sysResourcesMapper; public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { final HttpServletRequest request = ((FilterInvocation) object).getRequest(); Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE; for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().matches(request)) { attrs = entry.getValue(); break; } } logger.info("請求資源->資源:[{}]->[{}]", request.getRequestURI(), attrs); return attrs; } public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Map.Entry<RequestMatcher, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { allAttributes.addAll(entry.getValue()); } return allAttributes; } public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } private Map<String, String> loadResource() { Map<String, String> resourceLinkMap = new LinkedHashMap<String, String>(); List<Map<String, String>> resourceList = sysResourcesMapper.getURLResourceMapping(); for (Map<String, String> resourceMap : resourceList) { String resourcePath = resourceMap.get("resourcePath"); String authorityMark = resourceMap.get("authorityMark"); if (resourceLinkMap.containsKey(resourcePath)) { String mark = resourceLinkMap.get("resourcePath"); resourceLinkMap.put(resourcePath, mark + "," + authorityMark); } else { resourceLinkMap.put(resourcePath, authorityMark); } } return resourceLinkMap; } protected Map<RequestMatcher, Collection<ConfigAttribute>> bindRequestMap() { Map<RequestMatcher, Collection<ConfigAttribute>> map = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>(); Map<String, String> resMap = this.loadResource(); for (Map.Entry<String, String> entry : resMap.entrySet()) { String key = entry.getKey(); map.put(new AntPathRequestMatcher(key), SecurityConfig.createListFromCommaDelimitedString(entry.getValue())); } return map; } public void afterPropertiesSet() throws Exception { this.requestMap = this.bindRequestMap(); logger.info("資源文件權限參數初始化:資源列表[{}]", requestMap); } /** * 手動刷新資源 */ public void refreshResuorceMap() { this.requestMap = this.bindRequestMap(); } }
接著來講一講cachingUserDetailsService:認證處理服務,這個服務看它的名字就知道,它涉及到了緩存,所以我們加入了緩存的功能,因為登錄這種操作,一般情況下肯定不會只登錄一次就在不登錄了,甚至一天可能會登錄好幾次,那用緩存的方式,減少數據庫的訪問,能提高每次驗證速度。
我們可以看到有一個userCache的屬性,這個屬性就直接連接著userEhCacheFactory這個對象,我們的緩存,用的是EhCache。緩存的配置部分,我還沒講到,下一篇將會作出說明,簡單地說,就是通過這個userEhCacheFactory工廠對象,來獲取緩存對象。
另外可以看到構造器中還有一個參數是:delegate,指向的是一個WebUserDetailService,這個類是要自己自定義的:
1 package com.magic.rent.service.security; 2 3 /** 4 * 5 * 創建者: wu 創建時間: 16/9/23 6 * 類說明: 用於獲取用戶角色下的所有權限 7 */ 8 9 import com.magic.rent.mapper.SysAuthoritiesMapper; 10 import com.magic.rent.mapper.SysRolesMapper; 11 import com.magic.rent.mapper.SysUsersMapper; 12 import com.magic.rent.pojo.SysAuthorities; 13 import com.magic.rent.pojo.SysRoles; 14 import com.magic.rent.pojo.SysUsers; 15 import org.slf4j.Logger; 16 import org.slf4j.LoggerFactory; 17 import org.springframework.beans.factory.annotation.Autowired; 18 import org.springframework.context.MessageSource; 19 import org.springframework.context.support.MessageSourceAccessor; 20 import org.springframework.security.core.GrantedAuthority; 21 import org.springframework.security.core.authority.SimpleGrantedAuthority; 22 import org.springframework.security.core.userdetails.*; 23 import org.springframework.stereotype.Service; 24 25 import java.util.*; 26 27 @Service 28 public class WebUserDetailsService implements UserDetailsService { 29 30 @Autowired 31 private SysUsersMapper sysUsersMapper; 32 33 @Autowired 34 private SysRolesMapper sysRolesMapper; 35 36 @Autowired 37 private SysAuthoritiesMapper sysAuthoritiesMapper; 38 39 @Autowired 40 private MessageSourceAccessor messageSourceAccessor; 41 42 43 private static Logger logger = LoggerFactory.getLogger(WebUserDetailsService.class); 44 45 46 public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { 47 SysUsers sysUsers = null; 48 try { 49 //從數據中查找數據 50 sysUsers = sysUsersMapper.selectByUserName(s); 51 } catch (Exception e) { 52 e.printStackTrace(); 53 } 54 //如果查找不到用戶信息,則拋出異常 55 if (sysUsers == null) { 56 throw new UsernameNotFoundException( 57 messageSourceAccessor.getMessage("UserDetailsService.userNotFount", "用戶未找到!")); 58 } 59 //查詢用戶角色 60 sysUsers.setSysRoles(sysRolesMapper.selectRolesByUserId(sysUsers.getUserId())); 61 62 //查詢並封裝該用戶具有什麼權限 63 Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); 64 //用於過濾重復的權限 65 List<String> preAuthorityMarks = new ArrayList<String>(); 66 if (sysUsers.getSysRoles() != null && !sysUsers.getSysRoles().isEmpty()) { 67 //遍歷用戶所具有的所有角色 68 for (SysRoles role : sysUsers.getSysRoles()) { 69 //根據角色查詢單獨角色所具有的權限 70 List<SysAuthorities> sysAuthoritiesList = sysAuthoritiesMapper.selectByRole(role); 71 //將權限封裝用於後續做判斷 72 for (SysAuthorities sysAuthority : sysAuthoritiesList) { 73 //過濾已經存在的權限 74 if (preAuthorityMarks.contains(sysAuthority.getAuthorityMark())) { 75 //過濾 76 continue; 77 } else { 78 //加入用於過濾的集合中 79 preAuthorityMarks.add(sysAuthority.getAuthorityMark()); 80 //封裝如權限集合中 81 GrantedAuthority ga = new CustomGrantedAuthority(sysAuthority.getAuthorityMark()); 82 authorities.add(ga); 83 } 84 } 85 86 } 87 } 88 //裝載權限列表 89 sysUsers.setAuthorities(authorities); 90 logger.info("讀取用戶角色:賬戶名[{}]-權限[{}]", s, sysUsers.getAuthorities().toString()); 91 //拼裝SysUserLoginDetails對象 92 return sysUsers; 93 } 94 }
其中,我們有涉及到另一個類,就是CustomGrantedAuthority,而這個類也是自定義的,可以這樣寫的:
1 package com.magic.rent.service.security; 2 3 import org.springframework.security.core.GrantedAuthority; 4 import org.springframework.stereotype.Service; 5 import org.springframework.util.Assert; 6 7 import java.io.Serializable; 8 9 /** 10 * 11 * 創建者: wuxinzhe 創建時間: 16/10/6 12 * 類說明:用於封裝權限對象 13 */ 14 public class CustomGrantedAuthority implements GrantedAuthority, Serializable { 15 16 private static final long serialVersionUID = 9188347583387457302L; 17 18 private final String authority; 19 20 public CustomGrantedAuthority(String role) { 21 Assert.hasText(role, "A granted authority textual representation is required"); 22 this.authority = role; 23 } 24 25 public String getAuthority() { 26 return authority; 27 } 28 29 public boolean equals(Object obj) { 30 if (this == obj) { 31 return true; 32 } 33 34 if (obj instanceof CustomGrantedAuthority) { 35 return authority.equals(((CustomGrantedAuthority) obj).authority); 36 } 37 38 return false; 39 } 40 41 public int hashCode() { 42 return this.authority.hashCode(); 43 } 44 45 public String toString() { 46 return this.authority; 47 } 48 }
接著我們講最後一部分,就是方法攔截器的配置了。
<!--方法攔截器 start--> <bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> <property name="accessDecisionManager" ref="accessDecisionManager"/> <property name="authenticationManager" ref="myAuthenticationManager"/> <property name="securityMetadataSource" ref="methodSecurityMetadataSource"/> </bean> <aop:config> <aop:advisor advice-ref="methodSecurityInterceptor" pointcut="execution(* com.magic.rent.service.*.*(..))" order="1"/> </aop:config> <!--方法攔截器 end-->
基本屬性我就不再闡述了。
正如前面所說,方法層級的權限驗證,主要是通過AOP的方式來實現的,所以有了關於AOP的配置。
主要不同在於methodSecurityMetadataSource這個類:
package com.magic.rent.service.security; import com.magic.rent.mapper.SysResourcesMapper; import com.magic.rent.pojo.MethodKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.method.AbstractMethodSecurityMetadataSource; import org.springframework.stereotype.Service; import java.lang.reflect.Method; import java.util.*; /** * Created by wuxinzhe on 16/9/25. */ @Service public class MethodSecurityMetadataSource extends AbstractMethodSecurityMetadataSource implements InitializingBean { private final static List<ConfigAttribute> NULL_CONFIG_ATTRIBUTE = Collections.emptyList(); private final static String RES_KEY = "resourcePath"; private final static String AUTH_KEY = "authorityMark"; private Map<MethodKey, Collection<ConfigAttribute>> requestMap; private static Logger logger = LoggerFactory.getLogger(MethodSecurityMetadataSource.class); @Autowired private SysResourcesMapper sysResourcesMapper; /** * 根據方法獲取到訪問方法所需要的權限 * * @param method 訪問的方法 * @param targetClass 方法所屬的類 */ public Collection<ConfigAttribute> getAttributes(Method method, Class<?> targetClass) { MethodKey key = new MethodKey(method); Collection<ConfigAttribute> attrs = NULL_CONFIG_ATTRIBUTE; for (Map.Entry<MethodKey, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { if (entry.getKey().equals(key)) { attrs = entry.getValue(); break; } } logger.info("獲取Method-資源:[{}]->[{}]", key.getFullMethodName(), attrs); return attrs; } /** * 獲取到所有方法對應的權限集合 */ public Collection<ConfigAttribute> getAllConfigAttributes() { Set<ConfigAttribute> allAttributes = new HashSet<ConfigAttribute>(); for (Map.Entry<MethodKey, Collection<ConfigAttribute>> entry : requestMap.entrySet()) { allAttributes.addAll(entry.getValue()); } return allAttributes; } /** * 初始化方法權限對應集合,綁定方法權限集合 */ public void afterPropertiesSet() throws Exception { this.requestMap = this.bindRequestMap(); } /** * 從數據庫中獲取方法及權限對應信息 * * @return */ private Map<String, String> loadMethod() { Map<String, String> resMap = new LinkedHashMap<String, String>(); List<Map<String, String>> list = this.sysResourcesMapper.getMethodResourceMapping(); for (Map<String, String> map : list) { String resourcePath = map.get(RES_KEY); String authorityMark = map.get(AUTH_KEY); if (resMap.containsKey(resourcePath)) { String mark = resMap.get(resourcePath); resMap.put(resourcePath, mark + "," + authorityMark); } else { resMap.put(resourcePath, authorityMark); } } return resMap; } /** * 封裝從數據庫中獲取的方法權限集合 * * @return */ public Map<MethodKey, Collection<ConfigAttribute>> bindRequestMap() { Map<MethodKey, Collection<ConfigAttribute>> resMap = new LinkedHashMap<MethodKey, Collection<ConfigAttribute>>(); Map<String, String> map = this.loadMethod(); for (Map.Entry<String, String> entry : map.entrySet()) { MethodKey key = new MethodKey(entry.getKey()); resMap.put(key, SecurityConfig.createListFromCommaDelimitedString(entry.getValue())); } return resMap; } }
這個類暫時忘記了增加手動刷新內存中權限列表的方法,以後再弄吧,反正現在還沒有開發管理頁面,寫法跟上面資源權限列表的那個類是一樣的。
好啦,到此,萬惡的SpringSecurity就配置完了。
順帶的,貼幾張,前端截圖,嘻嘻嘻,做網站嗎,忙活了半天,肯定要看到一個直觀的結果撒~