好久沒有寫了,之前只寫了一半,我是一邊開發一邊寫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就配置完了。
順帶的,貼幾張,前端截圖,嘻嘻嘻,做網站嗎,忙活了半天,肯定要看到一個直觀的結果撒~






