星期四, 6月 28, 2007

Spring Acegi Security 備忘記

Acegi 發音為 "Ah-see-gee", Acegi Security 主要應用於 Spring 的認證及授權服務.
Acegi Security 亦解決企業在 J2EE 應用程式安全方面的問題,
能夠對網站 URL 進行用戶認證及授權, 用戶資料來源可以設定成由 memory, files 或 資料庫提供,
結合 CAS (Central Authentication Service) Server, 更能提供 Single Sign On 的功能,


這次備忘記開始時記載如何設定 Tomcat 使用 SSL 服務, [以備日後整合 CAS server]
然後從實際的 Eclipse project 實作中,
實作如何使用 Acegi Security 對 Spring 的 bean method 進行簡單的認證及授權.
用戶資料由 properties file 提供, 密碼經由 Md5PasswordEncoder 產生.

開始備忘記:
[1] 安裝 Eclipse WTP
[2] 安裝 tomcat 及 SSL
[3] Eclipse 建立 Acegi project


[1] 安裝 Eclipse WTP:
下載 wtp-all-in-one-sdk-R-1.5.4-win32.zip
http://download.eclipse.org/webtools/downloads/drops/R1.5/R-1.5.4-200705021353/
解壓縮至 C:\wtp-all-in-one-sdk-R-1.5.4-win32


[2] 安裝 tomcat 及 SSL:
下載 apache-tomcat-5.5.23.zip
http://tomcat.apache.org/download-55.cgi
解壓縮至 C:\apache-tomcat-5.5.23


設定 Eclipse 使用 tomcat 為 web server:
Eclipse
: Window -> Show View -> Servers
右鍵點擊 Servers panel -> New -> Server ->> Apache -> Tomcat v5.5 Server -> Next
Tomcat installation directory 選擇 C:\apache-tomcat-5.5.23
JRE 選擇 jre1.6.0_01
按 Finish 完成.


建立 KeyStore:
建立及進入目錄 C:\keytool
輸入
C:\keytool>keytool -genkey -alias joeytaProject -keyalg RSA -keystore c:\keytool\keystore.jks
輸入 keystore 密碼: joeytaPassword
你的名字與姓氏為何? localhost
其它按 <Enter> 鍵


CN=localhost, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown 正確嗎?
[否]: y


輸入 <tomcat> 的主密碼 (RETURN 如果各 keystore 密碼相同):
按 <Enter> 鍵

新增 Tomcat SSL 設定:
修改 Eclipse:Server -> Tomcat v5.5 Server @ localhost-config -> server.xml


將內文:
<!-- Define a SSL HTTP/1.1 Connector on port 8443 -->
<!--
<Connector port="8443" maxHttpHeaderSize="8192"
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" disableUploadTimeout="true"
acceptCount="100" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" />
-->


修改成:
<!-- Define a SSL HTTP/1.1 Connector on port 8443 -->
<Connector port="8443" maxHttpHeaderSize="8192"
maxThreads="150" minSpareThreads="25" maxSpareThreads="75"
enableLookups="false" disableUploadTimeout="true"
acceptCount="100" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="c:\keytool\keystore.jks"
keyAlias="joeytaProject"
keystorePass="joeytaPassword" />


[3] Eclipse 建立 Acegi project:
Eclipse: File -> New -> Other ->> Web -> Dynamic Web Project -> Next
Project Name: FirstAcegi
按 Finish 完成.

下載並解壓縮 spring-framework-2.0.6-with-dependencies.zip
http://www.springframework.org/download


下載並解壓縮 acegi-security-1.0.4.zip
http://sourceforge.net/project/showfiles.php?group_id=104215


將以下 jars 複製至 Eclipse:FirstAcegi/WebContent/WEB-INF/lib 裡:
spring-framework-2.0.6\dist\spring.jar
spring-framework-2.0.6\dist\spring-aspects.jar
spring-framework-2.0.6\dist\spring-mock.jar
spring-framework-2.0.6\lib\log4j\log4j-1.2.14.jar
spring-framework-2.0.6\lib\ehcache\ehcache-1.2.4.jar
spring-framework-2.0.6\lib\jakarta-commons\commons-collections.jar
spring-framework-2.0.6\lib\jakarta-commons\commons-codec.jar


acegi-security-1.0.4\acegi-security-1.0.4.jar



<!------------- web.xml ---------------------->
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="
http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="
http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">

<display-name>FirstAcegi</display-name>


<context-param>
<param-name>webAppRootKey</param-name>
<param-value>FirstAcegi.root</param-value>
</context-param>


<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
</param-value>
</context-param>

<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/classes/log4j.properties</param-value>
</context-param>

<context-param>
<param-name>log4jExposeWebAppRoot</param-name>
<param-value>false</param-value>
</context-param>

<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>


<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>


<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>

<listener>
<listener-class>org.acegisecurity.ui.session.HttpSessionEventPublisher</listener-class>
</listener>


<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
<!------------- web.xml ---------------------->

<!-------------- applicationContext.xml ------------->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "
http://www.springframework.org/dtd/spring-beans.dtd">
<beans>


<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,rememberMeProcessingFilter,exceptionTranslationFilter
</value>
</property>
</bean>
<!-- 這裡的 /** 為 URL, 可分別對不同的 URL 加入不同的 filter -->


<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider" />
<ref local="rememberMeAuthenticationProvider"/>
</list>
</property>
</bean>

<bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.Md5PasswordEncoder"/>

<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService" ref="inMemoryDaoImpl" />
<property name="passwordEncoder"><ref local="passwordEncoder"/></property>
</bean>

<bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener"/>

<bean id="inMemoryDaoImpl" class="org.acegisecurity.userdetails.memory.InMemoryDaoImpl">
<property name="userProperties">
<bean class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="location" value="/WEB-INF/users.properties" />
</bean>
</property>
<!--
<property name="userMap">
<value>
joeyta=joeytaPassword,ROLE_ADMIN
joey=joeyPassword,ROLE_USER
joe=joePassword,ROLE_USER
jane=janePassword,disabled,ROLE_USER
</value>
</property>
-->
</bean>
<!--
由於上面設定 passwordEncoder 由 Md5PasswordEncoder 做 Hashing,
故 /WEB-INF/users.properties 由 GenUsers.java 產生.
-->


<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<bean class="org.acegisecurity.vote.RoleVoter" />
</list>
</property>
</bean>


<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter" />


<bean id="rememberMeProcessingFilter" class="org.acegisecurity.ui.rememberme.RememberMeProcessingFilter">
<property name="authenticationManager"><ref local="authenticationManager"/></property>
<property name="rememberMeServices"><ref local="rememberMeServices"/></property>
</bean>
<!-- 在 acegilogin.jsp 裡的 checkbox value 加入 _acegi_security_remember_me, 選擇後就可以使用 cookie 自動登入 -->

<bean id="rememberMeServices" class="org.acegisecurity.ui.rememberme.TokenBasedRememberMeServices">
<property name="userDetailsService"><ref local="inMemoryDaoImpl"/></property>
<property name="key"><value>springRocks</value></property>
</bean>

<bean id="rememberMeAuthenticationProvider" class="org.acegisecurity.providers.rememberme.RememberMeAuthenticationProvider">
<property name="key"><value>springRocks</value></property>
</bean>

<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/"/>
<constructor-arg>
<list>
<ref bean="rememberMeServices"/>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler"/>
</list>
</constructor-arg>
</bean>
<!-- 欄截 url 為 /j_acegi_logout, 處理登出動作 -->

<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationFailureUrl" value="/accessDenied.jsp" />
<property name="defaultTargetUrl" value="/secure/loginsuccess.jsp" />
<property name="filterProcessesUrl" value="/j_acegi_security_check" />
<property name="rememberMeServices">
<ref local="rememberMeServices"/>
</property>
</bean>

<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/acegilogin.jsp" />
<property name="forceHttps" value="false" />
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage" value="/accessDenied.jsp" />
</bean>
</property>
</bean>


<!--
<bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/secure/**=ROLE_ADMIN,ROLE_USER
</value>
</property>
</bean>
-->

<bean name="sayHello" class="test.joeyta.SayHello" />


<bean id="methodSecurityInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager"/>
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager"/>
</property>
<property name="objectDefinitionSource">
<value>test.joeyta.ISayHello.say=ROLE_ADMIN</value>
</property>
</bean>

<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>sayHello</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>methodSecurityInterceptor</value>
</list>
</property>
</bean>


</beans>
<!-------------- applicationContext.xml ------------->

############ users.properties ##################
#Generated by GeneUsers.java
#Wed Jun 27 17:13:43 CST 2007
joey=eb7fb487fccce00e33f945d53d6d653b,ROLE_USER
joe=00f5c18c7dbe1424d32b253e695762a2,ROLE_USER
jane=a7ef041dae153f8426b739227fe77427,disabled,ROLE_USER
joeyta=c9cad4b3e956e94c8ded8c14a99c04c3,ROLE_ADMIN
############ users.properties ##################

/************** GenUsers *******************/
import java.io.FileOutputStream;
import java.util.Properties;


import org.acegisecurity.providers.encoding.Md5PasswordEncoder;



public class GenUsers {


public static void main(String[] args) {
Md5PasswordEncoder md5 = new Md5PasswordEncoder();

Properties property = new Properties();
property.setProperty("joeyta", md5.encodePassword("joeytaPassword", null)+",ROLE_ADMIN");
property.setProperty("joey", md5.encodePassword("joeyPassword", null)+",ROLE_USER");
property.setProperty("joe", md5.encodePassword("joePassword", null)+",ROLE_USER");
property.setProperty("jane", md5.encodePassword("janePassword", null)+",disabled,ROLE_USER");

try {
property.store(new FileOutputStream("WebContent/WEB-INF/users.properties"),"Generated by GeneUsers.java");
System.out.println("WebContent/WEB-INF/users.properties generated");
} catch (Exception e) {
System.err.println("Failure:"+e);
e.printStackTrace();
}

}


}
/************** GenUsers *******************/

/************** ISayHello *******************/
package test.joeyta;


public interface ISayHello {
public String say(String name);
}
/************** ISayHello *******************/

/************** SayHello *******************/
package test.joeyta;


public class SayHello implements ISayHello {
public String say(String name){
return "Hello, " + name;
}
}
/************** SayHello *******************/

<%!---------------- index.jsp ------------------%>
<%@ page session="false"%>


<%
response.sendRedirect(request.getContextPath()+"/sayHello.jsp");
%>
<%!---------------- index.jsp ------------------%>

<%!---------------- sayHello.jsp ------------------%>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
import="test.joeyta.*,org.springframework.web.context.support.WebApplicationContextUtils"
%>


<a href="<%=request.getContextPath()%>/j_acegi_logout">Logout</a> <br />


<%
ISayHello sayHello = (ISayHello)WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()).getBean("sayHello");
out.println(sayHello.say("Joeyta"));
%>
<%!---------------- sayHello.jsp ------------------%>

<%!---------------- acegilogin.jsp ------------------%>
<%@ page import="org.acegisecurity.ui.AbstractProcessingFilter" %>
<%@ page import="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter" %>
<%@ page import="org.acegisecurity.AuthenticationException" %>


<html>
<head>
<title>Login</title>
</head>


<body>
<h1>Login</h1>


<P>Valid users:
<P>
<P>username <b>joeyta</b>, password <b>joeytaPassword</b>
<P>username <b>joey</b>, password <b>joeyPassword</b>
<P>username <b>joe</b>, password <b>joePassword</b>
<p>username <b>jane</b>, password <b>janePassword</b> (user disabled)
<p>


<form action="<%=request.getContextPath()%>/j_acegi_security_check" method="POST">
<table>
<tr>
<td>User:</td>
<td><input type='text' name='j_username' /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type='password' name='j_password' /></td>
</tr>
<tr>
<td><input type="checkbox" name="_acegi_security_remember_me"></td>
<td>Don't ask for my password for next time</td>
</tr>


<tr><td colspan='2'><input name="submit" type="submit"></td></tr>
<tr><td colspan='2'><input name="reset" type="reset"></td></tr>
</table>


</form>


</body>
</html>
<%!---------------- acegilogin.jsp ------------------%>

<%!---------------- accessDenied.jsp ------------------%>
<%@ page import="org.acegisecurity.context.SecurityContextHolder" %>
<%@ page import="org.acegisecurity.Authentication" %>
<%@ page import="org.acegisecurity.ui.AccessDeniedHandlerImpl" %>


<h1>Sorry, access is denied</h1>


<a href="<%=request.getContextPath()%>/j_acegi_logout">Logout</a>
<p>
<%= request.getAttribute(AccessDeniedHandlerImpl.ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY)%>


<p>


<% Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null) { %>
Authentication object as a String: <%= auth.toString() %><BR><BR>
<% } %>
<%!---------------- accessDenied.jsp ------------------%>



項目結構如下圖所示:


開啟 IE 輸入 https://localhost:8443/FirstAcegi/index.jsp
出現如下圖所示:


按<是>後如下圖所示:
輸入用戶名 joeyta , 密碼 joeytaPassword


由於在 applicationContext.xml 裡定義如下:
<bean name="sayHello" class="test.joeyta.SayHello" />


<bean id="methodSecurityInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="authenticationManager">
<ref bean="authenticationManager"/>
</property>
<property name="accessDecisionManager">
<ref bean="accessDecisionManager"/>
</property>
<property name="objectDefinitionSource">
<value>test.joeyta.ISayHello.say=ROLE_ADMIN</value>
</property>
</bean>

<bean id="autoProxyCreator" class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames">
<list>
<value>sayHello</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>methodSecurityInterceptor</value>
</list>
</property>
</bean>

並在 sayHello.jsp 源碼裡可以了解到,
當執行下面這兩句時要求進行確認, 進入登入頁面進行認證.
ISayHello sayHello = (ISayHello)WebApplicationContextUtils.getRequiredWebApplicationContext(getServletContext()).getBean("sayHello");
out.println(sayHello.say("Joeyta"));



按送出查詢後如下圖所示:


請由以下連結下載項目:
http://blog.matrix.org.cn/joeyta/resource/FirstAcegi.zip
由於 spring.jar 太大了, 請自行加入.


參考資料:
http://acegisecurity.org/docbook/acegi.html


http://caterpillar.onlyfun.net/Gossip/AcegiGossip/AcegiGossip.html


沒有留言: