Saturday, June 18, 2005

Acegi อีกที

ช่วงนี้ได้ใช้ acegi จริงๆแล้ว
ก็เลยขอบันทึกไว้กันลืมหน่อย

ขอเท้าความนิดหนึ่งก่อน
ตัว acegi เป็นส่วนเสริมของ spring framework
ที่ช่วยจัดการกับเรื่อง security โดยเฉพาะ
คำถามก็คือ ใน spec ของ j2ee ก็มีการกำหนด spec
เรื่อง Container Managed Security ไว้แล้วเช่นกัน
ทำไมเราไม่ใช้ตัวนั้น

ปัญหาของ container managed security ใน j2ee
ก็คือ portable กับ extension
ผมเคยพัฒนาโปรเจคโดยใช้ jboss โดยมีเป้าหมาย
ให้ deploy production บน websphere
ตอนแรกก็กะว่าจะใช้ Container Managed Security
แต่ปรากฎว่าพบปัญหา conflict เรื่อง portable กับ extension นี่แหล่ะ
คือในกรณีถ้าอยาก customize เพิ่ม ก็จะเกิดปัญหาในส่วน portable
สุดท้ายผมก็ต้องเปลี่ยนไปเป็น Application Managed Security แทน

ปัญหาอย่างหนึ่งของ acegi ที่เจอก็คือ
วิธีการ ร้อย process เข้าหากัน
ข้อดี ก็คือมันเปิดโอกาสให้เรา customize ได้เกือบทุกจุด
แต่ก็เกิดข้อเสีย ก็คือ learning curve ก็เลยสูงพอสมควร
เนื่องจากคน config เห็นทุกอย่างปรากฎแก่สายตา
โดยไม่มีการกรอง

คำเตือน
  • สำหรับผู้ที่ไม่คุ้นกับ xml file, spring configuration style
    การอ่านบรรทัดล่างๆต่อไปนี้ เป็นอันตรายต่อสายตาอย่างยิ่ง
  • การอ่านบทความต่อไปนี้ จะทำให้เกิดการง่วงนอนได้
    กรุณาอย่าอ่าน ขณะขับรถ หรือควบคุมเครื่องจักร


เริ่มแรกให้ทำการ add filter เข้าไปใน web.xml ก่อน
    <filter>
<filter-name>securityFilter</filter-name>
<filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>net.sf.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter>

เจ้า FilterToBeanProxy เป็น proxy ที่คอย
delegate request ไปให้ FilterChainProxy
ที่เป็นพระเอกตัวจริงของเรื่องนี้
โดย FilterToBeanProxy จะมองหา TargetClass
จาก Spring bean context (acegi ทำงานร่วมกับ spring เสมอ)

จากนั้นเราก็ทำการ define FilterChainProxy
ผ่านทาง spring configuration file
(ตัว Appfuse ใช้ชื่อ file ว่า
appContext-security.xml)
    <!-- ======================== FILTER CHAIN ======================= -->
<bean id="filterChainProxy" class="net.sf.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/j_security_check*=httpSessionContextIntegrationFilter,
authenticationProcessingFilter
/*.html*=httpSessionContextIntegrationFilter,
remoteUserFilter,
anonymousProcessingFilter,
securityEnforcementFilter
/*.jsp=httpSessionContextIntegrationFilter,
remoteUserFilter
</value>
</property>
</bean>

ในตัวอย่างนี้จะเห็นได้ว่า เรากำหนด chain ของ filter
ให้แต่ละกลุ่ม url pattern
โดยแต่ละกลุ่ม url ต้องการ processing ที่ไม่เหมือนกัน
อย่าง url /j_security_check เป็น request
ที่เกิดจากการ submit หน้าจอ login
ดังนั้นก็เลยต้องใส่ filtler ที่ชื่อ authenticationProcessingFilter
เข้าไป เพื่อที่จะรับหน้าที่ทำ authenticate

ที่นี้ก็มาว่าต่อว่า การ authenticate นั้นทำอย่างไร
<bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref local="daoAuthenticationProvider"/>
<ref local="anonymousAuthenticationProvider"/>
</list>
</property>
</bean>

หลักการก็คือเจ้า ProviderManager จะทำการ
iterate providers list ที่เราใส่เข้าไป
โดยถ้าตัวไหนมี response ว่า authenticate ได้แล้ว ก็จะหยุด iterate
และใช้ผลลัพท์ที่ได้นั้น
กรณีของเรา ก็คือ เราใช้ DaoAuthenticationProvider
ซึ่งกำหนดจะมีการใช้ pattern dao ในการหาข้อมูล user,password มาทำการ authenticate
โดยเราต้อง add bean เพิ่มขึ้นอีกดังนี้
<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="authenticationDao"><ref local="jdbcAuthenticationDao"/></property>
<property name="userCache"><ref local="userCache"/></property>
</bean>

<!-- Read users from database -->
<bean id="jdbcAuthenticationDao" class="net.sf.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">
<property name="dataSource"><ref bean="dataSource"/></property>
<property name="usersByUsernameQuery">
<value>SELECT username,password,enabled FROM app_user WHERE username = ?</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>SELECT username,role_name FROM user_role WHERE username = ?</value>
</property>
</bean>

จะเห็นว่า เจ้า DaoAuthenticationProvider จะใช้
JdbcDaoImpl เป็นตัว query ข้อมูลจาก database ตรงๆ
โดยมี query อยู่ 2 แบบก็คือ
authenticate query อันนี้ใช้เพื่อตรวจ login, password
กับ authorities query ซึ่งใช้เพื่อตรวจสิทธิการทำงาน

Note: จะเห็นว่ามีการ config userCache ด้วย
อันนี้ก็คือ cache ที่ช่วยลด query ที่เกิดกับ database ลง

ย้อนกลับไปที่ ProviderManager ข้างบนนิดหนึ่ง
จะเห็นได้ว่ามีการ config AnonymousAuthenticationProvider ด้วย
ตัวนี้ก็คือ feature ที่ช่วยให้คนที่ไม่ได้ login นั้นได้รับการ assign
บทบาทเป็น Anonymous

เอาหล่ะหลังจากที่ได้กำหนด filterchain กับ วิธีการ query ข้อมูลไปแล้ว
ก็มาถึงขั้นตอนการกำหนดว่า url ไหนต้องใช้ role อะไรในการเข้าถึง

<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
<bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"><ref local="authenticationManager"/></property>
<property name="accessDecisionManager"><ref local="accessDecisionManager"/></property>
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/signup.html=ROLE_ANONYMOUS,admin,tomcat
/passwordhint.html*=ROLE_ANONYMOUS,admin,tomcat
/*.html*=admin,tomcat
</value>
</property>
</bean>

จะเห็นได้ว่าตัว /*.html* นั้นเราอนุญาติให้ใช้ได้เฉพาะ
บทบาท admin และ tomcat เท่านั้น
ส่วนหน้าจอ signup (register เพื่อขอใช้บริการ) นั้น
อนุญาติให้ บทบาท anonymous (พวกที่ยังไม่ได้ login) เรียกใช้ได้

อธิบายเพิ่มเติมอีกหน่อย เนื่องจาก ObjectDifinitionSource นั้นเป็นเพียงแค่ชุด string
ธรรมดา จำเป็นต้องมี parser ที่เข้ามาอ่านและทำความเข้าใจอีก ก็เลยต้องมี
การ config AccessDecisionManager เข้ามาจัดการ
โดยในที่นี้ ก็คือใช้ role เป็นตัวตัดสินว่าจะ อนุญาติให้ใช้หรือไม่ให้ใช้
    <bean id="accessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions"><value>false</value></property>
<property name="decisionVoters">
<list>
<ref local="roleVoter"/>
</list>
</property>
</bean>

<bean id="roleVoter" class="net.sf.acegisecurity.vote.RoleVoter"/>


เฮ้อมาถึงตรงนี้ก็เริ่มเหนื่อยแล้ว
แต่ยังเหลือ bean อีก 3 ตัวที่ config ไว้ใน filterChain
ที่ยังไม่ได้พูดถึง

....
/*.html*=httpSessionContextIntegrationFilter,
remoteUserFilter,
anonymousProcessingFilter,
securityEnforcementFilter
....

ตัวแรกก็คือ SecurityEnforcementFilter
ทำหน้าที่ดัก check สิทธิ ว่าใครทำอะไรได้ไม่ได้ รวมทั้ง check ด้วยว่า
มีการ login มาแล้วหรือยัง ถ้ายัง ก็จะ redirect ไปยังหน้าจอ login ให้ด้วย
(โดยการอ้างอิงผ่าน bean authenticationEntryPoint)
    <bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter">
<property name="filterSecurityInterceptor"><ref local="filterInvocationInterceptor"/></property>
<property name="authenticationEntryPoint"><ref local="authenticationProcessingFilterEntryPoint"/></property>
</bean>

<bean id="authenticationProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl"><value>/login.jsp</value></property>
<property name="forceHttps"><value>false</value></property>
</bean>


ตัวที่ 2 ก็คือ RemoteUserFilter
    <bean id="remoteUserFilter" class="net.sf.acegisecurity.wrapper.ContextHolderAwareRequestFilter"/>

ตัวนี้อ้างถึง ContextHolderAwareRequestFilter ซึ่งไม่ได้ทำหน้าที่อะไรมาก
หน้าที่หลักก็แค่ สร้าง HttpServletRequestWrapper เพื่อห่อ HttpServletRequest ที่ได้จาก servlet container เท่านั้น

ตัวที่3 ก็คือ HttpSessionContextIntegrationFilter
    <bean id="httpSessionContextIntegrationFilter" class="net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter">
<property name="context"><value>net.sf.acegisecurity.context.security.SecureContextImpl</value></property>
</bean>

ทำหน้าที่เป็น context หลักของ acegi framework
(context ของ acegi จะ sync ลง HttpSession หลังจากจบ request)

พอก่อนดีกว่า
ใครที่อ่านมาได้ถึงตรงนี้ ขอชมว่ามีความอดทนดีมาก

Related link from Roti

2 comments:

Anonymous said...

หลงเข้ามาอ่าน กำลังจะใช้ acegi อยู่พอดี ขอบคุณมากครับ

Anonymous said...

ขอบคุณสำหรับบทความดีๆ ครับ กำลังติดปัญหาเช่กันครับ
กรณีนี้คือ ถ้าเราเป็น User ที่ไม่มี permission ในหน้า page นั้น เราจะเข้าไม่ได้ใช่มั้ยครับ
และถ้าเรา ไม่มี permission และอยากซ่อน link นั้นๆ ด้วย ต้องทำไงครับ มีตัวอย่างมั้ยครับ
เช่น ถ้าเราเป็น sale อยากจะซ่อน เมนู admin เป็นต้นครับ
กำลังเจอปัญหานี้พอดีเลยครับ