有沒(méi)有發(fā)現(xiàn)一個(gè)問(wèn)題,我們之前做的所有練習(xí),都沒(méi)的權(quán)限管理這個(gè)模塊。我們的WEB應(yīng)用中的同一個(gè)帳戶可以在多臺(tái)機(jī)器上同時(shí)登陸,每一個(gè)用戶可以操作所有功能模塊。這樣在以后的應(yīng)用開(kāi)發(fā)中是結(jié)對(duì)不可行的!
今天的重點(diǎn)內(nèi)容就是權(quán)限管理,如果使用傳統(tǒng)的方式進(jìn)行權(quán)限管理,在實(shí)現(xiàn)上多少有點(diǎn)麻煩。我在進(jìn)行桌面開(kāi)發(fā)時(shí),涉及到的權(quán)限管理是向用戶表中添加用戶具有的權(quán)限ID串,每次用戶登陸時(shí)都拿這個(gè)串去查詢角色表,并根據(jù)用戶的角色隱藏相應(yīng)的功能。那我們?cè)?/span>WEB應(yīng)用中應(yīng)該如何做呢?方法都一樣,但是Spring為我們提供了一個(gè)框架專門用于權(quán)限管理,用起來(lái)比較方便——security!
一、Spring-security
SpringSecurity是SpringFramework的一個(gè)子項(xiàng)目,之前也叫做AcegiSecruty。
SpringSecurity能用于保護(hù)各種Java應(yīng)用程序(權(quán)限管理框架),但在基于Web的應(yīng)用程序中使用得最為廣泛。
SpringSecurity能以聲明的方式來(lái)保護(hù)Web應(yīng)用程序的URL訪問(wèn),只需簡(jiǎn)單的配置即可實(shí)現(xiàn)。
SpringSecurity通過(guò)一系列Servlet過(guò)濾器為Web應(yīng)用程序提供了多種安全服務(wù)。
SpringSecurity2.x顯著簡(jiǎn)化了配置,使用基于XMLSchema和基于注解的配置。
首先讓我們來(lái)回想一下,以前老方在講JavaWEB基礎(chǔ)時(shí)向大家介紹使用Filter進(jìn)行權(quán)限管理,這是一個(gè)十分好的方法。當(dāng)然他也有說(shuō),以后大家學(xué)習(xí)Spring時(shí),只要簡(jiǎn)單的配置幾下就可以進(jìn)行權(quán)限管理了,十分方便。(注意:過(guò)濾器)
Security也正是使用過(guò)濾器來(lái)實(shí)現(xiàn)上面的功能的!能想通嗎?讓我們一點(diǎn)點(diǎn)往下看。
二、HelloWorld!
我們依然從這個(gè)經(jīng)典的程序開(kāi)始,其實(shí)HelloWorld!程序只是學(xué)習(xí)者直觀簡(jiǎn)單的進(jìn)入一門語(yǔ)言或工具。我們的這SecurityHelloWrld程序比較一般的HelloWorld程序內(nèi)容要多一些。我們需要建立兩個(gè)頁(yè)面(index.jsp和admin.jsp),index.jsp用于登陸,admin.jsp只有具有ROLE_ADMIN權(quán)限的用戶才能查看。
我們先想一下需求:
· 防止一個(gè)帳戶同時(shí)在多個(gè)機(jī)器上登陸
· 帳戶只能根據(jù)自己的權(quán)限操作相應(yīng)的功能模塊
· 帳戶只能在頁(yè)面上看到具有對(duì)應(yīng)權(quán)限的數(shù)據(jù)內(nèi)容
1.搭建環(huán)境
因?yàn)槲覀兪褂玫氖?/span>Spring-security,所以需要搭建Spring環(huán)境。創(chuàng)建一個(gè)動(dòng)態(tài)WEB工程(EclipseJavaEE版)。
1).添加jar包: spring.jar、commons-loggin.jar、spring-security-core-2.0.5.jar。
2).WEB文件:
<!-- 搭建Spring -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/classes/applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 添加Spring-Security過(guò)濾器 -->
<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>
|
看見(jiàn)沒(méi),在這用到過(guò)濾器代理了!過(guò)濾器名稱必須指定為”springSecurityFilterChain“,Spring-secrutiy默認(rèn)為我們提供了一個(gè)過(guò)濾器。我們也可以編寫(xiě)一個(gè)自己的過(guò)濾器,然后將它配置為Spring的Bean,Bean的名稱必須為 springSecurityFilterChain。
3).applicationContext.xml文件:
<!-- 配置SpringSecurity的http安全服務(wù) -->
<sec:http auto-config="true">
<!-- 只有ROLE_ADMIN或ROLE_USER權(quán)限用戶才能訪問(wèn)index.jsp -->
<sec:intercept-url pattern="/index.jsp" access="ROLE_ADMIN,ROLE_USER"/>
<!-- 只有ROLE_ADMIN權(quán)限用戶才能訪問(wèn)admin.jsp -->
<sec:intercept-url pattern="/admin.jsp" access="ROLE_ADMIN"/>
</sec:http>
<!-- 配置SpringSecutiry的權(quán)限信息 -->
<sec:authentication-provider>
<sec:user-service>
<!-- 帳戶信息 -->
<sec:user password="admin" name="admin" authorities="ROLE_ADMIN"/>
<sec:user password="user" name="user" authorities="ROLE_USER"/>
</sec:user-service>
</sec:authentication-provider>
|
需要為這個(gè)spring的配置文件添加:xmlns:sec="http://www.springframework.org/schema/security",具體操作步驟不細(xì)說(shuō)了。
在Spring的配置文件中描述帳戶、權(quán)限信息,但把這么重要的信息放在這里是不安全的!
2.添加JSP頁(yè)面
index.jsp,body部分的內(nèi)容:
<body>
<h1>*****INDEX*****</h1>
<security:authorize ifAllGranted="ROLE_ADMIN">
<br>
<a href="admin.jsp">ADMIN</a>
</security:authorize>
<br>
<a href="logout">LOGOUT</a>
</body>
|
需要為這個(gè)JSP文件導(dǎo)入:<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags"%>。
admin.jsp,body部分的內(nèi)容:
<body>
<h1>*****ADMIN*****</h1>
<a href="index.jsp">RETURN</a>
</body>
|
OK,我們的Spring-securityHelloWorld!程序已經(jīng)完成了!跑一下,看到這個(gè)頁(yè)面沒(méi)有?
我們沒(méi)有為工程添加這們的頁(yè)面??!先不管這些,使用我們配置的兩個(gè)帳戶登陸看看功能如何。
3.登出配置
因?yàn)槲覀冊(cè)陧?yè)面中添加了登出連接,所以我們添加一個(gè)登出配置:
<!-- 配置登出信息 -->
<logout logout-url="/logout" logout-success-url="/bye.jsp"
invalidate-session="true" />
|
我們添加一個(gè)bye.jsp頁(yè)面,點(diǎn)擊logout連接時(shí),就會(huì)轉(zhuǎn)到此頁(yè)面。
4.登陸配置
上面的頁(yè)面是spring-security自動(dòng)為我們添加的登陸頁(yè)面,我們也可以設(shè)置自己的登陸頁(yè)面:
<!-- 配置登陸信息 -->
<form-login login-page="/login.jsp" login-processing-url="/login" />
|
login.jsp
<!-- 配置登陸信息 -->
<form-login login-page="/login.jsp"
login-processing-url="/login"
default-target-url="/index.jsp"
authentication-failure-url="/loginfail.jsp"
always-use-default-target="true"/>
|
login-page:登陸頁(yè)面,這里的login.jsp頁(yè)面中的表單請(qǐng)求和字段名稱必須是spring-security指定的,可以查看spring-security默認(rèn)為我們提供的頁(yè)面代碼。
login-processing-url:登陸請(qǐng)求處理URL。
default-target-url:登陸成功后,轉(zhuǎn)到的頁(yè)面。
authentication-failure-url:登陸失敗后,轉(zhuǎn)到的頁(yè)面。
always-use-default-target:一直使用”default-target-url“,這是什么意思?假如我們現(xiàn)在有兩個(gè)頁(yè)面可以跳到登陸頁(yè)面,默認(rèn)情況下spring-security在登陸成功后會(huì)跳轉(zhuǎn)到——跳轉(zhuǎn)登陸頁(yè)面的頁(yè)面。如果這個(gè)值真,那么登陸成功后會(huì)一直跳轉(zhuǎn)到”default-target-url“指定的頁(yè)面。有些情況下,用戶直接進(jìn)入登陸頁(yè)面進(jìn)行登陸,此時(shí)登陸成功后會(huì)跳轉(zhuǎn)到”default-target-url“指定的頁(yè)面,如果沒(méi)有指定這個(gè)頁(yè)面則跳轉(zhuǎn)到index.jsp頁(yè)面,如果沒(méi)有index.jsp頁(yè)面將出錯(cuò)。
問(wèn)題:
將這么重要的信息放在spring的配置文件中是相當(dāng)不安全的,Spring-security為們提供了安全的方法,將數(shù)據(jù)放到數(shù)據(jù)庫(kù)中。
三、使用數(shù)據(jù)庫(kù)替換配置文件
下面我們就實(shí)現(xiàn)將用戶的帳戶信息存儲(chǔ)到數(shù)據(jù)庫(kù)中,關(guān)于頁(yè)面的訪問(wèn)權(quán)限配置存放到數(shù)據(jù)庫(kù)中我們明天再總結(jié)。在此我們就不大動(dòng)干戈去使用Hibernate了,我們使用JDBC。
1.添加JDBC
向Spring添加數(shù)據(jù)源Bean還記得嗎?我們使用的數(shù)據(jù)源是c3p0。
<context:property-placeholder location="classpath:jdbc.properties" />
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="user" value="${user}" />
<property name="password" value="${password}" />
<property name="driverClass" value="${driverClass}" />
<property name="jdbcUrl" value="${jdbcUrl}" />
<property name="minPoolSize" value="5" />
<property name="initialPoolSize" value="3" />
<property name="maxPoolSize" value="10" />
<property name="acquireIncrement" value="2" />
</bean>
<bean id="jdbcTemplate"
class="org.springframework.jdbc.core.simple.SimpleJdbcTemplate">
<constructor-arg ref="dataSource"/>
</bean>
|
2.建數(shù)據(jù)庫(kù)和數(shù)據(jù)表
用戶表與角色表是什么關(guān)系?當(dāng)前是多對(duì)多嘍!因此我們需要三個(gè)表:
1).user(用戶表)
2).role(角色表)
3).user_role(中間表)
3.從數(shù)據(jù)庫(kù)中讀取權(quán)限信息
我們的數(shù)據(jù)源、jdbcTemplate和數(shù)據(jù)庫(kù)已經(jīng)建好了,接下來(lái)就是讓spring-security可以讀取到數(shù)據(jù)庫(kù)中的信息,根據(jù)這些信息判斷用戶輸入的登陸信息是否正確以及用戶所具有的權(quán)限。
1).要想spring-security可以讀取到數(shù)據(jù)庫(kù)中的信息,我們必須編寫(xiě)一個(gè)實(shí)現(xiàn)了UserDetailsService接口的類:
package cn.itcast.cc.spring.security;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.GrantedAuthorityImpl;
import org.springframework.security.userdetails.User;
import org.springframework.security.userdetails.UserDetails;
import org.springframework.security.userdetails.UserDetailsService;
import org.springframework.security.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private SimpleJdbcTemplate jdbcTemplate = null;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException, DataAccessException {
// 根據(jù)用戶名獲取帳戶和權(quán)限信息
String sql = "SELECT username,password,status,name rname"
+ " FROM user u,role r,user_role ur"
+ " WHERE u.id=ur.user_id AND r.id=ur.role_id AND username=?";
// 如果一個(gè)用戶具有多個(gè)權(quán)限,連接查詢會(huì)返回一個(gè)List
List list = this.jdbcTemplate.queryForList(sql,
new Object[] { username });
// 取出帳戶和權(quán)限信息填充到User中返回
if (list == null || list.size() <= 0)
// spring-security定義的異常
throw new UsernameNotFoundException("用戶不存在!");
// 如果用戶存在
Map<String, Object> map = (Map<String, Object>) list.get(0);
// 密碼
String password = (String) map.get("password");
// 帳戶是否可用
boolean enabled = ((Integer) map.get("status") == 1);
// 帳戶所具有的權(quán)限
GrantedAuthority[] gas = new GrantedAuthority[list.size()];
for (int i = 0; i < gas.length; i++) {
Map<String, Object> temp = (Map<String, Object>) list.get(i);
gas[i] = new GrantedAuthorityImpl((String) temp.get("rname"));
}
// spring-security提供的類
User user = new User(username, password, enabled, true, true, true, gas);
return user;
}
}
|
一定要記住實(shí)現(xiàn)的是UserDetailsService 接口,在spring-security幫助手冊(cè)中應(yīng)該有。佟佟今天領(lǐng)著大家看著源碼,下著斷點(diǎn),找到了這個(gè)接口。
2).將applicationContext.xml文件中的”<!-- 配置SpringSecutiry的權(quán)限信息 -->“部分,全部替換為:
<authentication-provider user-service-ref="userDetailsService" />
|
3).運(yùn)行,跑一個(gè)!
Spring-security的實(shí)現(xiàn)原理是什么?查看它的源文件,東西似乎還不少。我們簡(jiǎn)單的想一下,這些功能在Filter中完全可以實(shí)現(xiàn)。我們可以在Filter中讀取數(shù)據(jù)庫(kù)的記錄,根據(jù)帳戶的權(quán)限決定是否允許用戶繼續(xù)訪問(wèn)。我們也可以在JSP頁(yè)面中添加代碼,判斷用戶的權(quán)限以顯示或隱藏相應(yīng)的功能。
如果我們手動(dòng)去實(shí)現(xiàn)這樣的功能,那還是有些麻煩的。
明天繼續(xù)Spring-sceurity!