SpringSecurity(1)
其實啊,這部分我是最不想寫的,因為最麻煩的也是這部分,真的是非常非常的麻煩。關於SpringSecurity的配置,讓我折騰了好半天,網上的配置方式一大把,但總有一些功能不完全,版本不是最新等等的問題在,所以幾乎沒有一個教程,是可以整個貫通的。當然我的意思不是說那些不好,那些也不錯,但就對於我來說,還不夠全面。另外,SpringSecurity的替代品是shiro,據說,兩者的區別在於,前者涵蓋的范圍更廣,但前者也相對學習成本更高。又因為SpringSecurity是Spring家族的成員之一,所以在Spring框架下應用的話,可以做到非常高度的自定義,算是非常靈活的安全框架,就是配置起來,真心復雜。
SpringSecurity的配置文件
目錄:resource/config/spring,文件名:applicationContext-security.xml

1 <?xml version="1.0" encoding="UTF-8" ?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:sec="http://www.springframework.org/schema/security" 4 xmlns:aop="http://www.springframework.org/schema/aop" 5 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 6 xsi:schemaLocation="http://www.springframework.org/schema/beans 7 http://www.springframework.org/schema/beans/spring-beans-4.1.xsd 8 http://www.springframework.org/schema/aop 9 http://www.springframework.org/schema/aop/spring-aop.xsd 10 http://www.springframework.org/schema/security 11 http://www.springframework.org/schema/security/spring-security.xsd"> 12 13 <!--過濾資源 start--> 14 <!--不進行攔截的靜態資源--> 15 <sec:http pattern="/css/*" security="none"/> 16 <sec:http pattern="/images/*" security="none"/> 17 <sec:http pattern="/images/**" security="none"/> 18 <sec:http pattern="/js/*" security="none"/> 19 <sec:http pattern="/fonts/*" security="none"/> 20 <!--不進行攔截的頁面--> 21 <sec:http pattern="/WEB-INF/views/index.jsp" security="none"/> 22 <!--<sec:http pattern="WEB-INF/views/login.jsp" security="none"/>--> 23 <!--過濾資源 end--> 24 25 <!--權限配置及自定義登錄界面 start--> 26 <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> 27 <sec:form-login 28 login-page="/user/login" 29 login-processing-url="/login.do" 30 authentication-success-handler-ref="loginController" 31 authentication-failure-handler-ref="loginController"/> 32 <!--登出--> 33 <sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/> 34 <!--session管理及單點登錄--> 35 <sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/> 36 <!--資源攔截器配置--> 37 <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> 38 <sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/> 39 </sec:http> 40 41 <!--自定義驗證結果控制器--> 42 <bean id="loginController" class="com.magic.rent.controller.LoginAuthenticationController"> 43 <property name="successURL" value="/user/home"/> 44 <property name="failURL" value="/user/login"/> 45 <property name="attrName" value="loginResult"/> 46 <property name="byForward" value="false"/> 47 <property name="userInfo" value="userInfo"/> 48 </bean> 49 50 <sec:authentication-manager alias="myAuthenticationManager"> 51 <sec:authentication-provider ref="daoAuthenticationProvider"/> 52 </sec:authentication-manager> 53 54 55 <!--權限查詢服務--> 56 <bean id="cachingUserDetailsService" 57 class="org.springframework.security.config.authentication.CachingUserDetailsService"> 58 <constructor-arg name="delegate" ref="webUserDetailsService"/> 59 <property name="userCache"> 60 <bean class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache"> 61 <property name="cache" ref="userEhCacheFactory"/> 62 </bean> 63 </property> 64 </bean> 65 66 <bean id="daoAuthenticationProvider" 67 class="org.springframework.security.authentication.dao.DaoAuthenticationProvider"> 68 <property name="messageSource" ref="messageSource"/> 69 <property name="passwordEncoder" ref="messageDigestPasswordEncoder"/> 70 <property name="userDetailsService" ref="cachingUserDetailsService"/> 71 <property name="saltSource" ref="saltSource"/> 72 <property name="hideUserNotFoundExceptions" value="false"/> 73 </bean> 74 75 <!--MD5加密鹽值--> 76 <bean id="saltSource" class="org.springframework.security.authentication.dao.ReflectionSaltSource"> 77 <property name="userPropertyToUse" value="username"/> 78 </bean> 79 80 <!--決策管理器 start--> 81 <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> 82 <constructor-arg name="decisionVoters"> 83 <list> 84 <ref bean="roleVoter"/> 85 <ref bean="authenticatedVoter"/> 86 </list> 87 </constructor-arg> 88 <property name="messageSource" ref="messageSource"/> 89 </bean> 90 <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"> 91 <property name="rolePrefix" value="ROLE_"/> 92 </bean> 93 <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter"/> 94 <!--決策管理器 end--> 95 96 <!--資源攔截器 start--> 97 <bean id="filterSecurityInterceptor" 98 class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor"> 99 <property name="accessDecisionManager" ref="accessDecisionManager"/> 100 <property name="authenticationManager" ref="myAuthenticationManager"/> 101 <property name="securityMetadataSource" ref="resourceSecurityMetadataSource"/> 102 </bean> 103 104 <!--方法攔截器 start--> 105 <bean id="methodSecurityInterceptor" 106 class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor"> 107 <property name="accessDecisionManager" ref="accessDecisionManager"/> 108 <property name="authenticationManager" ref="myAuthenticationManager"/> 109 <property name="securityMetadataSource" ref="methodSecurityMetadataSource"/> 110 </bean> 111 <aop:config> 112 <aop:advisor advice-ref="methodSecurityInterceptor" pointcut="execution(* com.magic.rent.service.*.*(..))" 113 order="1"/> 114 </aop:config> 115 <!--方法攔截器 end--> 116 117 <!--session管理器 start--> 118 <bean id="concurrencyFilter" class="org.springframework.security.web.session.ConcurrentSessionFilter"> 119 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 120 <constructor-arg name="expiredUrl" value="/user/timeout"/> 121 </bean> 122 123 <bean id="concurrentSessionControlStrategy" 124 class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy"> 125 <constructor-arg name="sessionRegistry" ref="sessionRegistry"/> 126 <property name="maximumSessions" value="1"/> 127 </bean> 128 129 <bean id="sessionRegistry" class="org.springframework.security.core.session.SessionRegistryImpl"/> 130 <!--session管理器 end--> 131 </beans>
來吧,簡單的,從頭到尾的解釋一下。
首先呢,最先看到的應該是過濾資源的配置:
<!--過濾資源 start--> <!--不進行攔截的靜態資源--> <sec:http pattern="/css/*" security="none"/> <sec:http pattern="/images/*" security="none"/> <sec:http pattern="/images/**" security="none"/> <sec:http pattern="/js/*" security="none"/> <sec:http pattern="/fonts/*" security="none"/> <!--不進行攔截的頁面--> <sec:http pattern="/WEB-INF/views/index.jsp" security="none"/> <!--過濾資源 end-->
這些pattern意味着這些資源,不進行安全過濾,即在訪問這些資源的時候,不需要進行Security的權限驗證,舉一個例子:在以“webapp”為根目錄的情況下,css文件夾下的任何文件被訪問將不進行安全驗證,即任何用戶都可以毫無顧忌的直接訪問這些資源。
接下來的配置,相當重要,是整個框架的核心部分,如果不理解這部分,將無法好好使用這個框架。
<!--權限配置及自定義登錄界面 start--> <sec:http auto-config="true" access-decision-manager-ref="accessDecisionManager"> <sec:form-login login-page="/user/login" login-processing-url="/login.do" authentication-success-handler-ref="loginController" authentication-failure-handler-ref="loginController"/> <!--登出--> <sec:logout invalidate-session="true" logout-url="/logout.do" logout-success-url="/"/> <!--session管理及單點登錄--> <sec:session-management session-authentication-strategy-ref="concurrentSessionControlStrategy"/> <!--資源攔截器配置--> <sec:custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/> <sec:custom-filter ref="concurrencyFilter" position="CONCURRENT_SESSION_FILTER"/> </sec:http>
首先可以看到,“access-decision-manager-ref”是自定義框架的決策管理器(1),這個決策管理器是比如,當一個資源,被配置給3個不同的權限可以訪問的時候,你可以決定,是只要擁有三個中的一個權限,就能訪問資源,還是至少擁有2個權限,還是必須滿足三個權限都擁有的情況下,才能訪問資源。這就是決策管理器,就是制定放行規則。所以我們緊接着就要配置它了,這個決策管理器的配置,是這樣的:
<!--決策管理器 start--> <bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> <constructor-arg name="decisionVoters"> <list> <ref bean="roleVoter"/> <ref bean="authenticatedVoter"/> </list> </constructor-arg> <property name="messageSource" ref="messageSource"/> </bean> <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"> <property name="rolePrefix" value="ROLE_"/> </bean> <bean id="authenticatedVoter" class="org.springframework.security.access.vote.AuthenticatedVoter"/> <!--決策管理器 end-->
值得一提的是,bean:roleVoter中,有一個屬性是”rolePrefix“,這個是用於設置角色前綴的。什么是角色前綴呢?先要解釋什么是角色。SpringSecurity這個框架,默認的規則是以角色來判斷是否有訪問權限的,當然這並不符合我們的實際情況,我們使用的時候,更喜歡的是把角色更細化一層,比如,一個角色,具有多個“權限”,然后根據“權限”來判斷是否有訪問資源的資格。如果有資格,則訪問,沒資格,則返回403無權訪問錯誤頁面(當然默認的403有點丑,大部分情況我們都會對404、500、403這些常見的錯誤頁面來去替換成我們自己編寫的頁面,這個回頭再說。)。而角色權限,就是說,當系統讀取到的一個字符串,判斷它是否為一個用於表示角色的字符串,就是根據這個前綴來判斷的,如果有心得朋友,可以查看“RoleVoter”這個類,可以發現,其實系統對rolePrefix設置了一個默認值,就是“ROLE_”,而我們在這里配置,只是我為了說明這個問題,當然我們可以通過配置Bean來修改這個前綴,不過我個人覺得這個“ROLE_”挺好的,就采用原有的了。那這邊設置了前綴,就意味着,我們以后將角色存在數據庫當中的時候,就必須給我們的角色定義這個前綴,比如我在數據庫中存一個角色為管理員:ROLE_ADMIN。如果我們沒有以約定好的前綴來定義角色,系統就會不識別,然后直接報無權限訪問。這個也可以在RoleVoter這個類中的“supports”方法中得到查證。順便說一下,框架會先調用這個supports方法,來校驗是否是符合角色前綴的定義規則,如果不符合,根本都不進入后面的對比階段,直接返回false,然后就被判定為無權訪問了。可能就有朋友會想知道從哪里看出,先執行supports這個方法的,我在測試的時候,Debug了整個流程,但是現在已經不記得了,如果有想弄清楚的朋友,可以自行Debug,反正IDEA的Debug有記錄整個執行過程,所以只需要在這個supports方法上打一個斷點,然后查看上一個步驟就能找到調用的地方。
接着我們繼續往下配置文件的下面看,
<sec:form-login login-page="/user/login" login-processing-url="/login.do" authentication-success-handler-ref="loginController" authentication-failure-handler-ref="loginController"/>
這里呢,定義了前台頁面中,登錄表單的一些規則,
- login-page:這個參數,配置的是登錄頁面的訪問地址,因為我們是使用了SpringMVC,所以我自定義了一個Controller用於訪問登錄頁面,而地址就是“/user/login”:
其實就是很簡單的指向了login.jsp這個頁面,也沒有做什么其他的處理。
- login-processing-url:這個參數呢,是當你在jsp或者html頁面中,設計登錄的表單<form>標簽時,其中action元素的地址,就是你配置的這個參數,比如:
- authentication-success-handler-ref:這個參數,是定義一個當登錄驗證成功時要執行操作的控制器。
- authentication-failure-handler-ref:這個參數,是定一個,當登錄驗證失敗時,要執行操作的控制器。
這兩個參數,所對應的控制器,我為了簡略,就把它們合並成為一個,這個控制器怎么寫呢?實際很簡單,登錄驗證成功的控制器呢,就是一個普通的java類,去實現AuthenticationSuccessHandler這個接口的方法“onAuthenticationSuccess”,而登錄驗證失敗呢,就是實現AuthenticationFailureHandler的接口“anAuthenticationFailure”。我的實現類:
1 package com.magic.rent.controller; 2 3 import com.magic.rent.pojo.SysUsers; 4 import com.magic.rent.service.IUserService; 5 import com.magic.rent.util.HttpUtil; 6 import com.magic.rent.util.JsonResult; 7 import org.slf4j.Logger; 8 import org.slf4j.LoggerFactory; 9 import org.springframework.beans.factory.InitializingBean; 10 import org.springframework.beans.factory.annotation.Autowired; 11 import org.springframework.context.MessageSource; 12 import org.springframework.context.support.MessageSourceAccessor; 13 import org.springframework.dao.DataAccessException; 14 import org.springframework.security.core.Authentication; 15 import org.springframework.security.core.AuthenticationException; 16 import org.springframework.security.web.DefaultRedirectStrategy; 17 import org.springframework.security.web.RedirectStrategy; 18 import org.springframework.security.web.authentication.AuthenticationFailureHandler; 19 import org.springframework.security.web.authentication.AuthenticationSuccessHandler; 20 import org.springframework.transaction.annotation.Propagation; 21 import org.springframework.transaction.annotation.Transactional; 22 import org.springframework.util.StringUtils; 23 24 import javax.servlet.ServletException; 25 import javax.servlet.http.HttpServletRequest; 26 import javax.servlet.http.HttpServletResponse; 27 import java.io.IOException; 28 import java.util.Date; 29 import java.util.Locale; 30 31 public class LoginAuthenticationController implements AuthenticationSuccessHandler, AuthenticationFailureHandler, InitializingBean { 32 33 @Autowired 34 private IUserService iUserService; 35 36 private String successURL; 37 38 private String failURL; 39 40 private boolean byForward = false; 41 42 private String AttrName; 43 44 private String userInfo; 45 46 private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); 47 48 private static Logger logger = LoggerFactory.getLogger(LoginAuthenticationController.class); 49 50 public void setSuccessURL(String successURL) { 51 this.successURL = successURL; 52 } 53 54 public void setFailURL(String failURL) { 55 this.failURL = failURL; 56 } 57 58 public void setByForward(boolean byForward) { 59 this.byForward = byForward; 60 } 61 62 public void setAttrName(String attrName) { 63 AttrName = attrName; 64 } 65 66 public void setUserInfo(String userInfo) { 67 this.userInfo = userInfo; 68 } 69 70 @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class}) 71 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { 72 SysUsers users; 73 JsonResult jsonResult; 74 try { 75 users = (SysUsers) authentication.getPrincipal(); 76 Date date = new Date(); 77 users.setLastLogin(date); 78 users.setLoginIp(HttpUtil.getIP(request)); 79 try { 80 iUserService.updateUserLoginInfo(users); 81 } catch (DataAccessException e) { 82 logger.error("登錄異常:保存登錄數據失敗!", e); 83 } 84 } catch (Exception e) { 85 jsonResult = JsonResult.error("用戶登錄信息保存失敗!"); 86 logger.error("登錄異常:用戶登錄信息保存失敗!", e); 87 request.getSession().setAttribute(AttrName, jsonResult); 88 return; 89 } 90 jsonResult = JsonResult.success("登錄驗證成功!", users); 91 request.getSession().setAttribute(userInfo, jsonResult); 92 httpReturn(request, response, true); 93 } 94 95 @Transactional(readOnly = false, propagation = Propagation.REQUIRED, rollbackFor = {Exception.class}) 96 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { 97 JsonResult jsonResult; 98 logger.info("登錄失敗:請求IP地址[{}];失敗原因:{};", HttpUtil.getIP(request), exception.getMessage()); 99 jsonResult = JsonResult.error(exception.getMessage()); 100 request.getSession().setAttribute(AttrName, jsonResult); 101 httpReturn(request, response, false); 102 } 103 104 public void afterPropertiesSet() throws Exception { 105 if (StringUtils.isEmpty(successURL)) 106 throw new ExceptionInInitializerError("成功后跳轉的地址未設置!"); 107 if (StringUtils.isEmpty(failURL)) 108 throw new ExceptionInInitializerError("失敗后跳轉的地址未設置!"); 109 if (StringUtils.isEmpty(AttrName)) 110 throw new ExceptionInInitializerError("Attr的Key值未設置!"); 111 } 112 113 private void httpReturn(HttpServletRequest request, HttpServletResponse response, boolean success) throws IOException, ServletException { 114 if (success) { 115 if (this.byForward) { 116 logger.info("登錄成功:Forwarding to [{}]", successURL); 117 request.getRequestDispatcher(this.successURL).forward(request, response); 118 } else { 119 logger.info("登錄成功:Redirecting to [{}]", successURL); 120 this.redirectStrategy.sendRedirect(request, response, this.successURL); 121 } 122 } else { 123 if (this.byForward) { 124 logger.info("登錄失敗:Forwarding to [{}]", failURL); 125 request.getRequestDispatcher(this.failURL).forward(request, response); 126 } else { 127 logger.info("登錄失敗:Redirecting to [{}]", failURL); 128 this.redirectStrategy.sendRedirect(request, response, this.failURL); 129 } 130 } 131 132 } 133 }
估計還是需要簡單解釋一下,因為這個類我最終也是在Spring中裝配的,所以一些字段我也就沒有定義,只是做了get和set方法,等待配置。為了防止漏了這些字段的配置,所以我把這個類又另外實現了InitializingBean接口的afterPropertiesSet方法,這個方法可以在Spring框架啟動,生產Bean對象對其屬性進行裝配的時候執行,然后我在這個方法中,對所有需要配置的屬性,進行了非空驗證。其實這個類的作用很簡單,就是登陸成功后,保存登陸信息,然后跳轉到登陸后的界面。對了,不能忘了這個LoginAuthenticationController的配置文件了:
1 <!--自定義驗證結果控制器--> 2 <bean id="loginController"class="com.magic.rent.controller.LoginAuthenticationController"> 3 <property name="successURL" value="/user/home"/> 4 <property name="failURL" value="/user/login"/> 5 <property name="attrName" value="loginResult"/> 6 <property name="byForward" value="false"/> 7 <property name="userInfo" value="userInfo"/> 8 </bean>
這配置應該算淺顯易懂把,因為使用SpringMVC,所以每個地址其實都是SpringMVC的映射地址。
哦讀了!上面那個類,有一個對象,就是JsonResult,這是我用於傳輸到前端的一個包裝工具。
1 package com.magic.rent.util; 2 3 import org.slf4j.Logger; 4 import org.slf4j.LoggerFactory; 5 6 import java.io.Serializable; 7 8 /** 9 * Created by wuxinzhe on 16/9/20. 10 */ 11 public class JsonResult implements Serializable { 12 13 private static final long serialVersionUID = 8134245754393400511L; 14 15 private boolean status = true; 16 private String message; 17 private Object data; 18 private static Logger logger = LoggerFactory.getLogger(JsonResult.class); 19 20 public JsonResult() { 21 } 22 23 public JsonResult(Object data) { 24 this.data = data; 25 } 26 27 public boolean getStatus() { 28 return status; 29 } 30 31 public JsonResult setStatus(boolean status) { 32 this.status = status; 33 return this; 34 } 35 36 public String getMessage() { 37 return message; 38 } 39 40 public JsonResult setMessage(String message) { 41 this.message = message; 42 return this; 43 } 44 45 public Object getData() { 46 return data; 47 } 48 49 public JsonResult setData(Object data) { 50 this.data = data; 51 return this; 52 } 53 54 public static JsonResult success() { 55 return new JsonResult().setStatus(true); 56 } 57 58 public static JsonResult success(Object data) { 59 JsonResult jsonResult = success().setData(data); 60 logger.info(jsonResult.toString()); 61 return jsonResult; 62 } 63 64 public static JsonResult success(String message, Object data) { 65 JsonResult jsonResult = success().setData(data).setMessage(message); 66 logger.info(jsonResult.toString()); 67 return jsonResult; 68 } 69 70 public static JsonResult error() { 71 return new JsonResult().setStatus(false); 72 } 73 74 public static JsonResult error(String message) { 75 JsonResult jsonResult = error().setMessage(message); 76 logger.info(jsonResult.toString()); 77 return jsonResult; 78 } 79 80 @Override 81 public String toString() { 82 return "JsonResult{" + 83 "status=" + status + 84 ", message='" + message + '\'' + 85 ", data=" + data + 86 '}'; 87 } 88 }
這個類還是跟朋友借鑒的呢,之前我也沒有做過這種,不過這個說實話,真的很有用。