轉載地址: http://yzxqml.iteye.com/blog/1756106
現在很多企業和開發團隊都使用了SSH2(Struts 2 +Spring 2.5 +Hibernate)框架來進行開發, 我們或許已經習慣了強大的Spring Framework 全局配置管理,不可否認,Sping是一個很優秀的開源框架,但是由於Spring3.0版本後強大的的註解式bean的誕生,Spring MVC框架這匹黑馬正悄然殺起,但今天Spring MVC不是主角,今天我和大家分享一個同樣隸屬於SpringSource 的安全框架——Spring Security, 下面的基於Spring MVC給大家分享一下Spring Security 的使用。雖然對它的接觸時間不長,參考了一些網上朋友的做法,但也按照我的理解把這個框架介紹介紹,不是很專業,還請大家不要介意 。
我們知道,Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分。用戶認證指的是驗證某個用戶是否爲系統中的合法主體,也就是說用戶能否訪問該系統。用戶授權指的是驗證某個用戶是否有權限執行某個操作。在一個系統中,不同用戶所具有的權限是不同的。比如對一個資源來說,有的用戶只能進行讀取,而有的用戶可以進行修改。一般來說,系統會爲不同的用戶分配不同的角色,而每個角色則對應一系列的權限。
首先,我們看web.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
- http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
- <!-- 編碼統一最好放最上面,最先加載,防止亂碼-->
- <filter>
- <filter-name>Set Character Encoding</filter-name>
- <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
- <init-param>
- <param-name>encoding</param-name>
- <param-value>UTF-8</param-value>
- </init-param>
- <init-param>
- <param-name>forceEncoding</param-name>
- <param-value>true</param-value><!-- 強制進行轉碼 -->
- </init-param>
- </filter>
- <filter-mapping>
- <filter-name>Set Character Encoding</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- <!-- 然後接着是SpringSecurity必須的filter 優先配置,讓SpringSecurity先加載,防止SpringSecurity攔截失效-->
- <filter>
- <filter-name>springSecurityFilterChain</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- </filter>
- <filter-mapping>
- <filter-name>springSecurityFilterChain</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
- <welcome-file-list>
- <welcome-file>index.jsp</welcome-file>
- </welcome-file-list>
- <!--
- spring需要加載的配置文件
- -->
- <context-param>
- <param-name>contextConfigLocation</param-name>
- <param-value>
- WEB-INF/classes/applicationContext.xml,
- WEB-INF/spring3-servlet.xml,
- WEB-INF/spring-security.xml
- </param-value>
- </context-param>
- <listener>
- <listener-class>
- <!-- 所以,要在web.xml下面配置好監聽,讓服務器啓動時就初始化改類,可以得到request -->
- org.springframework.web.context.request.RequestContextListener
- </listener-class>
- </listener>
- <listener>
- <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
- </listener>
- <!--
- 默認所對應的配置文件是WEB-INF下的{servlet-name}-servlet.xml,這裏便是:spring3-servlet.xml
- -->
- <servlet>
- <servlet-name>spring3</servlet-name>
- <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
- <load-on-startup>1</load-on-startup>
- </servlet>
- <servlet-mapping>
- <servlet-name>spring3</servlet-name>
- <!--
- 這裏可以用 / 但不能用 /*
- ,攔截了所有請求會導致靜態資源無法訪問,所以要在spring3-servlet.xml中配置mvc:resources
- -->
- <url-pattern>/</url-pattern>
- </servlet-mapping>
- </web-app>
註釋已經寫了挺多,還是稍微解釋一下要注意的地方,一個是UTF-8編碼轉換,這個最好加在最前面,讓它先生效,我在調試的時候就出過這種情況,web.xml裏的其他配置都正常生效了,但是編碼死活不行,一中文就亂碼,鬱悶了老半天,然後突發奇想,是不是web.xml裏先聲明的配置先生效,後聲明的後生效?接着實踐,果然不出我所料,把編碼轉換加在前面,一切正常。。。。我那個暈。。。
關於Spirng MVC的就不說了,那些數據訪問、業務和控制層那些的東東就自個研究去了吧。。這不是今天的重點。
接着是spring-security.xml
- <?xml version="1.0" encoding="UTF-8"?>
- <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xmlns:security="http://www.springframework.org/schema/security"
- xsi:schemaLocation="http://www.springframework.org/schema/beans
- http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
- http://www.springframework.org/schema/security
- http://www.springframework.org/schema/security/spring-security-3.0.xsd">
- <!-- Spring-Security 的配置 -->
- <!--
- 注意use-expressions=true.表示開啓表達式,否則表達式將不可用. see:http://www.family168.com/tutorial/springsecurity3/html/el-access.html
- -->
- <security:http auto-config="true" use-expressions="false" access-denied-page="/user/login_failure.html">
- <!--允許所有人訪問-->
- <!-- <security:intercept-url pattern="/**" access="permitAll" />-->
- <!--允許ROLE_ADMIN權限訪問-->
- <security:intercept-url pattern="/user/findAll.html" access="ROLE_ADMIN" />
- <!--允許ROLE_ADMIN權限訪問-->
- <security:intercept-url pattern="/user/**" access="ROLE_ADMIN" />
- <!--允許ROLE_USER權限訪問-->
- <security:intercept-url pattern="/success.jsp" access="ROLE_USER,ROLE_ADMIN" />
- <!--允許IS_AUTHENTICATED_ANONYMOUSLY匿名訪問-->
- <security:intercept-url pattern="/anonymously.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
- <!-- filters="none" 不過濾這些資源-->
- <security:intercept-url pattern="/js/**" filters="none" />
- <security:intercept-url pattern="/index.jsp" filters="none" />
- <!-- login-page:默認指定的登錄頁面. authentication-failure-url:出錯後跳轉頁面. default-target-url:成功登陸後跳轉頁面 -->
- <security:form-login login-page="/index.jsp" authentication-failure-url="/user/login_failure.html"
- default-target-url="/success.jsp" />
- <!--
- invalidate-session:指定在退出系統時是否要銷燬Session。logout-success-url:退出系統後轉向的URL。logout-url:指定了用於響應退出系統請求的URL。其默認值爲:/j_spring_security_logout。
- -->
- <security:logout invalidate-session="true" logout-success-url="/index.jsp" logout-url="/j_spring_security_logout" />
- <!--
- max-sessions:允許用戶帳號登錄的次數。範例限制用戶只能登錄一次。exception-if-maximum-exceeded:
- 默認爲false,此值表示:用戶第二次登錄時,前一次的登錄信息都被清空。當exception-if-maximum-exceeded="true"時系統會拒絕第二次登錄。
- -->
- <security:session-management>
- <security:concurrency-control error-if-maximum-exceeded="true" max-sessions="1" />
- </security:session-management>
- </security:http>
- <!-- 指定一個自定義的authentication-manager :customUserDetailsService -->
- <security:authentication-manager>
- <security:authentication-provider user-service-ref="customUserDetailsService">
- <security:password-encoder ref="passwordEncoder" />
- </security:authentication-provider>
- </security:authentication-manager>
- <!-- 對密碼進行MD5編碼 -->
- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
- <!--
- 通過 customUserDetailsService,Spring會控制用戶的訪問級別.
- 也可以理解成:以後我們和數據庫操作就是通過customUserDetailsService來進行關聯.
- -->
- <bean id="customUserDetailsService" class="org.yzsoft.springmvcdemo.util.CustomUserDetailsService" />
- <!-- 自定義登陸錯誤提示,可以取出mymessages.properties的國際化消息-->
- <bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
- <property name="basename" value="classpath:org/yzsoft/springmvcdemo/mymessages" />
- </bean>
- <bean id="localeResolver" class="org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver" />
- </beans>
這個多解釋一下,首先
- <security:intercept-url pattern="/findAll.html" access="hasRole('ROLE_ADMIN')" />
- <security:intercept-url pattern="/user/**" access="hasRole('ROLE_ADMIN')" />
- <security:intercept-url pattern="/anonymously.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
這個是權限控制,聲明瞭擁有什麼權限可以訪問哪些資源,這個配置的是有ROLE_ADMIN權限的纔可以訪問/findAll.html,至於這個ROLE_ADMIN從哪來,呆會再解釋。或者像第二句一樣配置也可以:擁有ROLE_ADMIN權限的纔可以訪問/user/下的所有資源,否則都會拋出AccessDeniedException 。
IS_AUTHENTICATED_ANONYMOUSLY就是匿名訪問的意思,這個相信都懂的。。。
然後是登陸和安全退出
- <security:form-login login-page="/index.jsp" authentication-failure-url="/user/login_failure.html"default-target-url="/user/findAll.html" />
- <security:logout invalidate-session="true" logout-success-url="/index.jsp" logout-url="/user/login_failure.html" />
解釋下上面一句,相信看也能看出來了的,login-page:默認指定的登錄頁面. authentication-failure-url:出錯後跳轉頁面(包括那些個啥用戶名密碼錯誤吖。。啥啥啥的。). default-target-url:成功登陸後跳轉頁面 (這裏我直接跳到的控制器去查數據列表)。
接着:invalidate-session:指定在退出系統時是否要銷燬Session。logout-success-url:退出系統後轉向的URL。logout-url:指定了用於響應退出系統請求的URL。其默認值爲:/j_spring_security_logout。
接下來是一個比較不錯的功能:是否允許同一用戶多處登陸
- <security:session-management>
- <security:concurrency-control error-if-maximum-exceeded="true" max-sessions="1" />
- </security:session-management>
exception-if-maximum-exceeded:
默認爲false,此值表示:用戶第二次登錄時,前一次的登錄信息都被清空。當error-if-maximum-exceeded="true"時系統會拒絕第二次登錄。
max-sessions:允許用戶帳號登錄的次數,這裏我們允許一次登陸。這裏我們做個實驗吧,看看是不是真的生效了,請看圖
這裏我們看到,當同一個賬號多處登陸時,就會報出Maximum sessions of 1 for this principal exceeded 的錯誤,當然,正式使用我們換成mymessages.properties裏的我們自定義的國際化消息,這樣人性化一點。
好,我們往下看,接着就是應用我們實際項目裏的自定義用戶權限了
- <security:authentication-manager>
- <security:authentication-provider user-service-ref="customUserDetailsService">
- <security:password-encoder ref="passwordEncoder" />
- </security:authentication-provider>
- </security:authentication-manager>
- <!-- 對密碼進行MD5編碼 -->
- <bean id="passwordEncoder" class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
- <bean id="customUserDetailsService" class="org.yzsoft.springmvcdemo.util.CustomUserDetailsService" />
首先是<security:authentication-manager>是指定我們自定義的身份驗證策略,這裏我們用customUserDetailsService這個bean,就是指向我們CustomUserDetailsService.java這個類。然後<security:password-encoder>指定我們密碼使用MD5進行編碼,調用Spring Security自帶的MD5加密類。當然,還有加鹽MD5或我們自己寫的加密算法等安全性更加高的密碼策略。這個按項目實際使用配置吧。
然後看到我們的CustomUserDetailsService.java
- package org.yzsoft.springmvcdemo.util;
- import java.util.ArrayList;
- import java.util.Collection;
- import java.util.List;
- import org.apache.log4j.Logger;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.dao.DataAccessException;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.authority.GrantedAuthorityImpl;
- import org.springframework.security.core.userdetails.User;
- import org.springframework.security.core.userdetails.UserDetails;
- import org.springframework.security.core.userdetails.UserDetailsService;
- import org.springframework.security.core.userdetails.UsernameNotFoundException;
- import org.yzsoft.springmvcdemo.serviceimpl.UsersServiceImpl;
- import org.yzsoft.springmvcdemo.vo.TUsers;
- /**
- * 一個自定義的類用來和數據庫進行操作. 即以後我們要通過數據庫保存權限.則需要我們繼承UserDetailsService
- *
- * @author
- *
- */
- public class CustomUserDetailsService implements UserDetailsService {
- protected static Logger logger = Logger.getLogger("service");//log4j,不用解釋了吧。。
- @Autowired
- private UsersServiceImpl usersService;
- public UsersServiceImpl getUsersService() {
- return usersService;
- }
- public void setUsersService(UsersServiceImpl usersService) {
- this.usersService = usersService;
- }
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException {
- UserDetails user = null;
- try {
- // 搜索數據庫以匹配用戶登錄名.
- // 我們可以通過dao使用Hibernate來訪問數據庫
- System.out.println(username + " 用戶頁面輸入的用戶名");
- TUsers tusers = this.usersService.findByUsername(username);
- System.out.println(tusers.getUsername() + " 數據庫取出的用戶名");
- // Populate the Spring User object with details from the dbUser
- // Here we just pass the username, password, and access level
- // getAuthorities() will translate the access level to the correct
- // role type
- // 用戶名、密碼、是否啓用、是否被鎖定、是否過期、權限
- user = new User(tusers.getUsername(), tusers.getPassword().toLowerCase(), true, true, true, true, getAuthorities(Integer.parseInt(tusers.getRole())));
- } catch (Exception e) {
- logger.error("用戶信息錯誤!");
- throw new UsernameNotFoundException("異常處理:檢索用戶信息未通過!");
- }
- return user;
- }
- /**
- * 獲得訪問角色權限列表
- *
- * @param access
- * @return
- */
- public Collection<GrantedAuthority> getAuthorities(Integer role) {
- System.out.println("取得的權限是 :" + role);
- List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
- // 所有的用戶默認擁有ROLE_USER權限
- if (role == 0) {
- System.out.println("普通用戶");
- logger.debug("取得普通用戶權限-->");
- authList.add(new GrantedAuthorityImpl("ROLE_USERS"));
- }
- // 如果參數role爲1.則擁有ROLE_ADMIN權限
- if (role == 1) {
- logger.debug("取得ADMIN用戶權限-->");
- authList.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
- }
- System.out.println(authList.size()+" 權限列表長度");
- return authList;
- }
- }
這裏就是把我們從數據庫裏面取得的用戶權限和Spring Security的配置進行橋接,還記得上面配置文件裏的ROLE_ADMIN吧,就是從這裏來的,很奇怪的是,這個必須設置成ROLE_ 開頭纔有效。。鬱悶。。。
這裏我剛開始有一個疑惑,我們看這2句
- TUsers tusers = this.usersService.findByUsername(username);
- user = new User(tusers.getUsername(), tusers.getPassword().toLowerCase(), true, true, true, true, getAuthorities(Integer.parseInt(tusers.getRole())));
這裏根本不需要用戶輸入的密碼,只要了用戶名,然後直接根據用戶名去取權限,就直接設置進Spring Security的User對象裏面去,我不禁一身冷汗,這不相當於說有了用戶名就直接去查數據庫麼,而且是不用密碼的。。。。
但經過查看官方文檔和網上的解釋,這才放心,原來是這樣的,Spring Security的確是直接根據用戶名去查,但是查得出來的Spring Security User對象之後,它會根據這個對象的屬性值去數據庫查詢與這個對象匹配的數據,我們這裏設置的是(用戶名,密碼,是否啓用、是否被鎖定、是否過期、權限。。。),那麼如果數據庫存在這個對象,就返回真,否則返回假,這樣也就不用擔心了,完全可靠。就是我們在前臺要做好限制,不能給用戶不輸密碼就訪問, 不然擠爆你數據庫連接。。。。。
最後登陸頁面index.jsp
- <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
- <%
- String path = request.getContextPath();
- String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
- %>
- <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
- <html>
- <head>
- <base href="<%=basePath%>">
- <title>My JSP 'index.jsp' starting page</title>
- <meta http-equiv="pragma" content="no-cache">
- <meta http-equiv="cache-control" content="no-cache">
- <meta http-equiv="expires" content="0">
- <meta http-equiv="keywords" content="keyword1,keyword2,keyword3">
- <meta http-equiv="description" content="This is my page">
- </head>
- <body>
- 用戶登陸 <br>
- ${SPRING_SECURITY_LAST_EXCEPTION.message}
- <form action="j_spring_security_check" method="post">
- USERNAME:<input type="text" name="j_username" value="${sessionScope['SPRING_SECURITY_LAST_USERNAME']}" /><br/>
- PASSWORD:<input type="password" name="j_password" value="" /><br/>
- <input type="checkbox" name="_spring_security_remember_me" />兩週之內不必登陸(這個功能沒有做的)<br/>
- <input type="submit">
- </form>
- </body>
- </html>
這裏我還是使用Spring Security默認的j_username和j_password,表單目標也用默認的j_spring_security_check,會默認跳到Spring Security進行攔截。其他的應該不用解釋了吧。。。。
最後 ,我們整理一下Spring Security的整個控制過程:
——>用戶登陸
——> <security:authentication-manager> 攔截
——>交給customUserDetailsService處理,並且聲明密碼採用MD5策略
——>根據輸入的用戶名去數據庫查這條記錄,驗證身份
——>取出該條記錄的用戶權限(鎖定、禁用、過期和實際權限等)
——>根據取得的權限列表去security:intercept-url匹配、授權,然後判斷是否放行。
這就完成了一整個的權限控制流程。
接下來我們來測試一下看是否真的生效了:
1、測試匿名訪問頁面,直接地址欄訪問:
2、普通用戶登陸
然後訪問後臺管理頁面,跳回了登陸頁
3、管理員用戶登陸
退出後成功跳轉回登陸頁,點擊後退再執行其他操作,這時候session已經註銷了的,不能執行,又跳回了登陸頁。是我們想要的效果,OK,成功了。
好了,Spring Security的簡單使用就講到這裏,其實這只是Spring Security的一小部分,而且這裏我還沒有用權限表對用戶權限進行專門的管理,很多東西還是用Spring Security 默認的,還有Spring Security CAS (單點登陸)以及更加高級的權限控制和更完善的Spring Security 配置,以後我們再慢慢去研究吧。發現Spring Security 這個技術不僅簡化了我們的用戶權限管理,要知道我們做管理系統的時候這是個大問題,也差不多顛覆了我一貫以來用戶權限管理的觀念,但是掌握了這種思維之後,又發現,其實,程序並不是只有一種實現方式,它激發了我寫程序時要去尋找多種解決方案的想法。學習的路上,就是要不斷推翻自己固有的思維,去見識多種新事物,纔能有進步。