Integrate Spring Security with ZK"
(43 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
{{Template:Smalltalk_Author| | {{Template:Smalltalk_Author| | ||
|author=Ian Tsai, Engineer, Potix Corporation | |author=Ian Tsai, Engineer, Potix Corporation | ||
− | |date=March | + | |date=March 13, 2013 |
|version=ZK 6.5.X, Spring Security 3.1.2 | |version=ZK 6.5.X, Spring Security 3.1.2 | ||
}} | }} | ||
− | + | ||
=Introduction= | =Introduction= | ||
− | Spring Security is a common solution for developer to serve security needs in a Java web application, it is widely used and is a proven technology. However, due to its nature to protect resources by pattern matching through URL, it's not | + | Spring Security is a common solution for developer to serve security needs in a Java web application, it is widely used and is a proven technology. However, due to its nature to protect resources by pattern matching through URL, it's not obvious for application developer to realise how Spring Security can be adapted with a specific Ajax request handling mechanism of an Ajax framework. |
+ | |||
So in this article, I'll introduce how to integrate Spring Security with ZK seamlessly by going through the construction of a simple demo application(An article publish and editing system). | So in this article, I'll introduce how to integrate Spring Security with ZK seamlessly by going through the construction of a simple demo application(An article publish and editing system). | ||
Line 13: | Line 14: | ||
=Resources to Download = | =Resources to Download = | ||
I use Git as my source control and has stored my code at Github, you can check out the demo project from [https://github.com/zanyking/smalltalk/tree/master/Spring_Security_Integration here]. | I use Git as my source control and has stored my code at Github, you can check out the demo project from [https://github.com/zanyking/smalltalk/tree/master/Spring_Security_Integration here]. | ||
− | |||
− | |||
The project is based on Maven, if you want to try different version of ZK or Spring, please change the version number in ''pom.xml'' | The project is based on Maven, if you want to try different version of ZK or Spring, please change the version number in ''pom.xml'' | ||
=Demo Application Details= | =Demo Application Details= | ||
− | This demo application is a simple article publish and edit system which allows three | + | This demo application is a simple article publish and edit system which allows three kind of users to access: |
+ | |||
+ | <ol> | ||
+ | <li>user with role ''ROLE_USER''</li> | ||
+ | <li>user with role ''ROLE_EDITOR''</li> | ||
+ | <li>anonymous user with default reserved role ''IS_AUTHENTICATED_ANONYMOUSLY''</li> | ||
+ | </ol> | ||
:[[File:spring_security_integration_use_case_diagram.png]] | :[[File:spring_security_integration_use_case_diagram.png]] | ||
− | * Anonymous user can visit the homepage which contains article list, and they can view an article's content by clicking | + | * Anonymous user can visit the homepage which contains article list, and they can view an article's content by clicking its link in homepage. |
− | * User with role ''ROLE_USER'' is allowed to post new | + | * User with role ''ROLE_USER'' is allowed to post new articles and edit their own articles. |
− | * User with role ''ROLE_EDITOR'' is most powerful user who is able to edit and delete any | + | * User with role ''ROLE_EDITOR'' is most powerful user who is able to edit and delete any articles. |
− | This article will based on the implementation requirements of this application to demonstrate the integration of Spring Security and ZK. | + | This article will be based on the implementation requirements of this application to demonstrate the integration of Spring Security and ZK. |
=Spring Security Configuration= | =Spring Security Configuration= | ||
Line 57: | Line 62: | ||
</filter-mapping> | </filter-mapping> | ||
</source> | </source> | ||
− | As you can see, despite those ordinary Spring Context Listeners(''RequestContextListener'' and ''ContextLoaderListener''), we declared ''HttpSessionEventPublisher'' and ''springSecurityFilterChain'' for Spring Security. Here | + | As you can see, despite those ordinary Spring Context Listeners(''RequestContextListener'' and ''ContextLoaderListener''), we declared ''HttpSessionEventPublisher'' and ''springSecurityFilterChain'' for Spring Security. Here ''HttpSessionEventPublisher'' is optional and it is designed for Spring Security to do detailed concurrent session control, ''springSecurityFilterChain'' is the main hook for all Spring Security's functionality to the application, it's required and must be named '''springSecurityFilterChain'''. |
+ | |||
==applicationContext-security.xml== | ==applicationContext-security.xml== | ||
− | Here in this project, I separated Spring's ''ApplicationContext.xml'' | + | Here in this project, I separated Spring's ''ApplicationContext.xml'' into two files, the original ''ApplicationContext.xml'' is for backend bean and service bean declarations, and the additional ''applicationContext-security.xml'' is for Spring Security's configuration only. |
− | In ''applicationContext-security.xml'', there are two major elements we have to setup, the ''<http>'' element and the ''<authentication-manager>'' element. | + | In ''applicationContext-security.xml'', there are two major elements we have to setup, which are the ''<http>'' element and the ''<authentication-manager>'' element. |
===Http Element Setting=== | ===Http Element Setting=== | ||
Line 88: | Line 94: | ||
===Authentication Manager Setting=== | ===Authentication Manager Setting=== | ||
− | The ''<authentication-manager>'' element declaration is designed to manage ''authentication-provider'' which is the provider of Spring's authentication instance and will do the real work of the authentication. You can declare multiple ''authentication-provider''s in order to leverage different source of users(ex: openidAuthenProvider or LDAPAuthenProvider), in this project we | + | The ''<authentication-manager>'' element declaration is designed to manage ''authentication-provider'' which is the provider of Spring's authentication instance and will do the real work of the authentication. You can declare multiple ''authentication-provider''s in order to leverage different source of users (ex: openidAuthenProvider or LDAPAuthenProvider), in this project we extend UserDetailsService from default authentication-provider: |
<source lang="xml"> | <source lang="xml"> | ||
<authentication-manager> | <authentication-manager> | ||
Line 102: | Line 108: | ||
=Protecting a Page Request= | =Protecting a Page Request= | ||
After the configuration of Spring Security, now, let's see how to use it to protect a zul file request. | After the configuration of Spring Security, now, let's see how to use it to protect a zul file request. | ||
− | === Scenario: When User requesting a restricted resource === | + | === Scenario: When User is requesting a restricted resource === |
− | In our demo project, ''NewArticle.zul'' is a restricted resource that can only be accessed by user who already logged in, it's natural because to post a new article, we have to know who | + | In our demo project, ''NewArticle.zul'' is a restricted resource that can only be accessed by user who are already logged in, it's natural because to post a new article, we have to know who the author is. |
In Spring Security, to restrict a resource for certain user( who has enough permission) to access is very simple, we simply declare ''<intercept-url>'' element under ''<http>'' in ''applicationContext-security.xml'': | In Spring Security, to restrict a resource for certain user( who has enough permission) to access is very simple, we simply declare ''<intercept-url>'' element under ''<http>'' in ''applicationContext-security.xml'': | ||
<source lang="xml"> | <source lang="xml"> | ||
<intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" /> | <intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" /> | ||
</source> | </source> | ||
− | Here the ''pattern'' attribute is to determine which request this setting will take effect, and if a request's URL matches this pattern, | + | Here the ''pattern'' attribute is used to determine which request this setting will take effect, and if a request's URL matches this pattern, |
the ''access'' attribute will be used to check current user's authorities. | the ''access'' attribute will be used to check current user's authorities. | ||
− | As | + | As we can see, the page request protection under Spring Security is very straightforward and intuitive, it's all based on pattern matching to request path. |
− | = | + | =Log-in Page Implementation in ZK= |
+ | Now, after the ''newArticle.zul'' has been secured, the log-in page(''login.zul'') which is required for authentication process has to be implemented for user to perform log in. Here let's see how to implement a log-in page in ZUL: | ||
+ | <source lang="xml"> | ||
+ | <html:form id="f" name="f" action="j_spring_security_check" method="POST" | ||
+ | xmlns:html="native"> | ||
+ | <grid> | ||
+ | <rows> | ||
+ | <row>User: <textbox id="u" name="j_username"/></row> | ||
+ | <row>Password: <textbox id="p" type="password" name="j_password"/></row> | ||
+ | <row spans="2"> | ||
+ | <hbox> | ||
+ | <html:input type="reset" value="Reset"/> | ||
+ | <html:input type="submit" value="Submit Query"/> | ||
+ | <button type="submit" label="Accedi_trendy" mold="trendy"/> | ||
+ | <button type="submit" label="Accedi_os" /> | ||
+ | |||
+ | </hbox> | ||
+ | </row> | ||
+ | </rows> | ||
+ | </grid> | ||
+ | </html:form> | ||
+ | </source> | ||
+ | |||
+ | # An html ''<form>'' element has to be declared outside the username and password inputs. To use native html ''<form>'' element in ZUL, please refer to [[ZK_Developer's_Reference/UI_Patterns/HTML_Tags/The_native_Namespace| native namespace]]. | ||
+ | # The form must contains two input elements named ''j_username'' and ''j_passoword''. To apply some ajax effect and features to these inputs, we can use ZK's ''<textbox>'' component instead. | ||
+ | # Form action attribute has to be mapped to ''login-processing-url'' attribute's value of ''<form-login>'' element in ''applicationContext-security.xml'' | ||
+ | If an anonymous user click the ''New Article'' button in homepage, he will be redirect to our ''login.zul''. | ||
=Securing Partial View By Using EL in ZUL= | =Securing Partial View By Using EL in ZUL= | ||
+ | In order to secure partial view part in web page, Spring Security has its own taglib which provides basic support for accessing security information and applying security constraints in JSPs(You can find the implementation in the source code of ''spring-security-taglibs-3.1.2.RELEASE.jar''). And here for security in ZULs, ZK framework also provides approaches to define new tag library. In our demo application, for using Spring Security through EL we made our own tag library. | ||
+ | |||
+ | ===Zul Tag Library for Spring Security=== | ||
+ | Though [[ZUML_Reference/ZUML/Processing_Instructions/taglib/Custom_Taglib|ZK Developer Reference]] provides very detailed information on what we can do in a custom taglib of ZK, here we only focus on what we want to have for Spring Security. | ||
+ | |||
+ | First, let's create a ''security.tld'' under ''/WEB-INF/''. However, for more serious projects, you can do the ''classpath:metainfo/tld/config.xml'' method as mentioned in the Developer Reference. | ||
+ | |||
+ | Second, define the EL functions in ''/WEB-INF/security.tld'', here's the example: | ||
+ | <source lang="xml"> | ||
+ | <taglib> | ||
+ | <uri>http://www.zkoss.org/demo/integration/security</uri> | ||
+ | <description> | ||
+ | Methods and actions for ZK + Spring Security | ||
+ | </description> | ||
+ | |||
+ | <function> | ||
+ | <name>isAllGranted</name> | ||
+ | <function-class>org.zkoss.demo.springsec.SecurityUtil</function-class> | ||
+ | <function-signature> | ||
+ | boolean isAllGranted(java.lang.String authorities) { | ||
+ | </function-signature> | ||
+ | <description> | ||
+ | Return true if the authenticated principal is granted authorities | ||
+ | of ALL the specified roles. | ||
+ | </description> | ||
+ | </function> | ||
+ | ... | ||
+ | </source> | ||
+ | As we can see, the document needs to start with a root element: ''<taglib>'' with an ''<uri>'' element in it. Then, you can declare functions. | ||
+ | In the function declaration, we mapped a method(''isAllGranted()'') of ''org.zkoss.demo.springsec.SecurityUtil'' to our EL function ''isAllGranted'', about the ''SecurityUtil'', the implementation of it is based on Spring Security's [http://static.springsource.org/spring-security/site/docs/3.1.x/apidocs/org/springframework/security/core/context/SecurityContextHolder.html SecurityContextHolder] | ||
+ | |||
+ | Now let's see how to use the custom taglib of Spring Security in ZUL. | ||
+ | ===Usage: Protect Action=== | ||
− | = | + | In our demo project, according to the use cases above, we have a <nowiki>"delete article"</nowiki> function that only user with role: ''ROLE_EDITOR'' can access. Now, with our custom taglibs for Spring Security, we are able to set up some constraints for it like this: |
− | === | + | <source lang="xml"> |
− | === ZK | + | <?taglib uri="/WEB-INF/security.tld" prefix="sec"?> |
+ | ... | ||
+ | <button id="deleteBtn" label="Delete" | ||
+ | if="${sec:isAllGranted('ROLE_USER')}" | ||
+ | disabled="${not sec:isAllGranted('ROLE_EDITOR')}"/> | ||
+ | ... | ||
+ | </source> | ||
+ | |||
+ | === Special ZK Component Attributes for Security === | ||
+ | |||
+ | In the sample code above, I used custom EL + ''disabled'' attribute of ZK button to protect the click from user, ZK also provides other attributes that may be applicable for security constraint such as ''visible'' and ''readonly''(for combobox). These attributes which are based on html effects are very useful to fulfill the requirements of the escalation of permission to access UI. | ||
+ | |||
+ | To be more secured and to protect the server-side access of a ZK component, please refer to this article: [[ZK_Developer's_Reference/Security_Tips/Block_Request_for_Inaccessible_Widgets|Block Request for Inaccessible Widgets]] | ||
+ | |||
+ | =Protecting ZK Ajax Request= | ||
+ | In an Ajax framework like ZK, there are two kind of requests. | ||
+ | Now, let's talk about the most difficult part of this integration i.e. how to deal with ZK's Ajax request by using Spring Security? | ||
+ | |||
+ | First, we have to take a look at how to protect a page request in a more programmatic way. | ||
+ | |||
+ | === Do Security Check in ZK's Listener === | ||
+ | In some situation, user might need to do security check in java code not in ZUL, for example, in our ''accessDeniedExTest.zul'', we have an ''Initiator'' that will do security check in ''public void doInit(Page page, Map<String, Object> args) throws Exception'' method: | ||
+ | |||
+ | <source lang="java"> | ||
+ | public class AccessDeniedExInit extends GenericInitiator { | ||
+ | public void doInit(Page page, Map<String, Object> args) throws Exception { | ||
+ | if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){ | ||
+ | throw new AccessDeniedException("this is a test of AccessDeniedException!"); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | The ''AccessDeniedException'' is a key to Spring Security's filter chain to handle, and everything works fine during a page request situation, but what if we throw the exception in an action say ZK's ''EventListener''? | ||
+ | |||
+ | == Security Check in ZK's Action == | ||
+ | |||
+ | Without any further adapting code, throwing ''AccessDeniedException'' during an Ajax request won't trigger Spring Security's authentication and authorization process.This is because to the ZK AU Engine's perspective, it's impossible for any other outsider of ''org.zkoss.zk.au.http.DHtmlUpdateServlet'' to know how to compose a meaningful and correctly-structured response to the client engine at the browser. So any thrown exception must be handled inside ZK AU Engine. | ||
+ | |||
+ | To over come this, we have to convert ZK's Ajax request to a normal page request, so then we can let Spring Security to handle the ''AccessDeniedException'' in a normal page request. the implementation of this idea includes 3 steps: | ||
+ | |||
+ | === STEP 1: Convert Ajax Request to Page Request === | ||
+ | First, we have to tell ZK we want to custom the error handling mechanism for ''AccessDeniedException''. To do that is very simple, in ''zk.xml'' we add this section: | ||
+ | <source lang="xml"> | ||
+ | <error-page> | ||
+ | <exception-type>org.springframework.security.access.AccessDeniedException</exception-type> | ||
+ | <location>security_process.zul</location> | ||
+ | </error-page> | ||
+ | </source> | ||
+ | ZK's error handling is very straightforward, you map an error type to a ZUL page, then the ZUL page will be parsed based on current desktop if such error happens. In our ''security_process.zul'' we simply have an initiator inside to handle ''AccessDeniedException'': | ||
+ | <source lang="xml"> | ||
+ | <?init class="org.zkoss.demo.springsec.ui.error.SpringSecurityHandleInit"?> | ||
+ | <zk> | ||
+ | <!-- DO NOTHING! --> | ||
+ | </zk> | ||
+ | </source> | ||
+ | Then in ''SpringSecurityHandleInit'', we can check if this is an Ajax request, store every needed information in session and ask the client engine to do the redirection back to this ''security_process.zul'' again. | ||
+ | <source lang="java"> | ||
+ | if(exec.isAsyncUpdate(null) ){ | ||
+ | //STEP 1: convert Ajax Request to Page Request(Error Handling Page Request) | ||
+ | System.out.println(">>>> Security Process: STEP 1"); | ||
+ | if(ex instanceof AccessDeniedException){ | ||
+ | sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri());// for login-success-url | ||
+ | sess.setAttribute(VAR_SPRING_SECURITY_ERROR, ex); | ||
+ | |||
+ | Executions.sendRedirect(toSecurityProcessUrl((AccessDeniedException) ex));// GOTO STEP 2 by redirection. | ||
+ | }else{ | ||
+ | throw new IllegalArgumentException( | ||
+ | "How come an unexpected Exception type will be mapped to this handler? please correct it in your zk.xml"); | ||
+ | } | ||
+ | |||
+ | </source> | ||
+ | |||
+ | === STEP 2: Throw AccessDeniedException in Redirection to adapt Spring Security Process === | ||
+ | The redirection will be back to this ''security_process.zul'' again, and because this time we are inside a page request, we can get the exception back from ''HttpSession'' and throw it out to let Spring Security filter chain to handle it. | ||
+ | <source lang="java"> | ||
+ | Exception err = (Exception) sess.getAttribute(VAR_SPRING_SECURITY_ERROR); | ||
+ | String dtPath = (String) sess.getAttribute(VAR_DESKTOP_REQ_URI); | ||
+ | if(err!=null){ | ||
+ | //STEP 2: throw Error in Error Handling Page Request. | ||
+ | System.out.println(">>>> Security Process: STEP 2"); | ||
+ | sess.removeAttribute(VAR_SPRING_SECURITY_ERROR); | ||
+ | throw err;// we suppose Spring Security Error Filter Chain will handle this properly. | ||
+ | |||
+ | } | ||
+ | </source> | ||
+ | Now, Spring Security will check current user's principle and do authentication or authorization process. In authentication processing, user will be redirected to the ''login.zul'' as what we configured, and if log-in is success, user will be redirected to the original location where this exception occurred and the location here will be our ''security_process.zul'' which is not the original place where the Ajax request caused the error. So we need the third step to handle this situation. | ||
+ | |||
+ | === STEP 3: Handle the Login Success Redirection === | ||
+ | Let's go back to see the sample code of STEP1, the code | ||
+ | <source lang="java"> | ||
+ | sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri()); | ||
+ | </source> | ||
+ | reserved the original request path of ZUL which generated the desktop, and which the information is prepared for STEP 3, the implementation of how to retrieve original desktop path(impl of ''getOriginalDesktopUri()'') is as follows: | ||
+ | <source lang="java"> | ||
+ | private static String getOriginalDesktopUri(){ | ||
+ | // developer may implement this part to adapt to PushState or any other Page based Framework, that might have interference to request URI. | ||
+ | String str = Executions.getCurrent().getDesktop().getRequestPath(); | ||
+ | String qs = Executions.getCurrent().getDesktop().getQueryString(); | ||
+ | System.out.println(">>>security Process: Desktop path= "+str); | ||
+ | return str+"?"+qs; | ||
+ | } | ||
+ | </source> | ||
+ | And now in STEP 3, we simply redirect user back to our desktop path: | ||
+ | <source lang="java"> | ||
+ | else if(dtPath!=null){ | ||
+ | System.out.println(">>>> Security Process: STEP 3"); | ||
+ | //STEP 3: if Spring Security Authentication was triggered at STEP 2, | ||
+ | //then we need STEP 3 to redirect back to original URI the very first desktop belongs to. | ||
+ | sess.removeAttribute(VAR_DESKTOP_REQ_URI); | ||
+ | exec.sendRedirect(dtPath); | ||
+ | |||
+ | } | ||
+ | </source> | ||
+ | For more details, please take a look at the source code of ''SpringSecurityHandleInit''. | ||
+ | |||
+ | === Usage: Protect Open Editor Button === | ||
+ | After weaving ZK Ajax request to Spring Security, let's see how to apply security constraint to user's action in our demo project, first, in one of our use case scenario user is allowed to edit an article if he is the author or he has '''ROLE_EDITOR'', so the open editor button in ''ArticleContentViewCtrl'' for ''articleContent.zul'' is implemented like this: | ||
+ | <source lang="java"> | ||
+ | @Listen("onClick=#openEditorBtn") | ||
+ | public void edit(){ | ||
+ | //ownership & permission check. | ||
+ | if(!isOwner() && SecurityUtil.isNoneGranted("ROLE_EDITOR")){ | ||
+ | throw new AccessDeniedException( | ||
+ | "The user is neither the author, nor a privileged user."); | ||
+ | } | ||
+ | ArticleEditor editor = new ArticleEditor(); | ||
+ | editor.setParent(container); | ||
+ | editor.doHighlighted(); | ||
+ | } | ||
+ | </source> | ||
+ | As shown, we throw ''AccessDeniedException'' directly in an event listener, and we can do the same thing in ZK View Model's action: | ||
+ | <source lang="java"> | ||
+ | public class TestVModel { | ||
+ | ... | ||
+ | @Command | ||
+ | @NotifyChange("fullName") | ||
+ | public void doChange(){ | ||
+ | if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){ | ||
+ | throw new AccessDeniedException("you are not an editor!"); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | </source> | ||
+ | If you are using a Spring bean with ''@Secured'' annotation tagged on some methods, when user doesn't pass the security check, the thrown ''AccessDeniedException'' will be handled in the same way. | ||
+ | =Summary= | ||
+ | In this article, we discussed how to use Spring Security in a ZK web application, we went though all major topics developers may think about while doing this kind of integration, and also showed a workaround of adapting ZK's Ajax request to Spring Security's filter chain. | ||
− | + | If there's any thing about this topic you'd want to know more or any part that I should discuss in this article, please feel free to leave a comment a below. | |
Latest revision as of 03:33, 15 March 2013
Ian Tsai, Engineer, Potix Corporation
March 13, 2013
ZK 6.5.X, Spring Security 3.1.2
Introduction
Spring Security is a common solution for developer to serve security needs in a Java web application, it is widely used and is a proven technology. However, due to its nature to protect resources by pattern matching through URL, it's not obvious for application developer to realise how Spring Security can be adapted with a specific Ajax request handling mechanism of an Ajax framework.
So in this article, I'll introduce how to integrate Spring Security with ZK seamlessly by going through the construction of a simple demo application(An article publish and editing system).
Resources to Download
I use Git as my source control and has stored my code at Github, you can check out the demo project from here.
The project is based on Maven, if you want to try different version of ZK or Spring, please change the version number in pom.xml
Demo Application Details
This demo application is a simple article publish and edit system which allows three kind of users to access:
- user with role ROLE_USER
- user with role ROLE_EDITOR
- anonymous user with default reserved role IS_AUTHENTICATED_ANONYMOUSLY
- Anonymous user can visit the homepage which contains article list, and they can view an article's content by clicking its link in homepage.
- User with role ROLE_USER is allowed to post new articles and edit their own articles.
- User with role ROLE_EDITOR is most powerful user who is able to edit and delete any articles.
This article will be based on the implementation requirements of this application to demonstrate the integration of Spring Security and ZK.
Spring Security Configuration
First, let's see how to configure our project. To use Spring Security, we have to add some listener and filter declarations in web.xml:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/applicationContext.xml
/WEB-INF/applicationContext-security.xml
</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>
<listener>
<listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>
<filter><!-- the filter-name must be preserved, do not change it! -->
<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>
As you can see, despite those ordinary Spring Context Listeners(RequestContextListener and ContextLoaderListener), we declared HttpSessionEventPublisher and springSecurityFilterChain for Spring Security. Here HttpSessionEventPublisher is optional and it is designed for Spring Security to do detailed concurrent session control, springSecurityFilterChain is the main hook for all Spring Security's functionality to the application, it's required and must be named springSecurityFilterChain.
applicationContext-security.xml
Here in this project, I separated Spring's ApplicationContext.xml into two files, the original ApplicationContext.xml is for backend bean and service bean declarations, and the additional applicationContext-security.xml is for Spring Security's configuration only.
In applicationContext-security.xml, there are two major elements we have to setup, which are the <http> element and the <authentication-manager> element.
Http Element Setting
The <http> element is to tell Spring what kind of resources need to be secured, which port will be used by container for http & https connections, and what kind of log-in solution will be used in this web application.
<http auto-config="true">
<port-mappings>
<port-mapping http="8080" https="8443"/>
</port-mappings>
<intercept-url pattern="/zkau/**" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="any"/>
<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https" />
<intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" />
<intercept-url pattern="/j_spring_security_check" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="https" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" requires-channel="any" />
<session-management session-fixation-protection="none" />
<form-login login-page="/login.zul"
authentication-failure-url="/login.zul?login_error=1"
login-processing-url="/j_spring_security_check"/>
<logout logout-success-url="/index.zul" invalidate-session="true" />
</http>
Authentication Manager Setting
The <authentication-manager> element declaration is designed to manage authentication-provider which is the provider of Spring's authentication instance and will do the real work of the authentication. You can declare multiple authentication-providers in order to leverage different source of users (ex: openidAuthenProvider or LDAPAuthenProvider), in this project we extend UserDetailsService from default authentication-provider:
<authentication-manager>
<authentication-provider user-service-ref="myUserDetailsService">
<password-encoder hash="md5" />
</authentication-provider>
</authentication-manager>
<beans:bean id="myUserDetailsService"
class="org.zkoss.demo.springsec.model.MyUserDetailsService"/>
Protecting a Page Request
After the configuration of Spring Security, now, let's see how to use it to protect a zul file request.
Scenario: When User is requesting a restricted resource
In our demo project, NewArticle.zul is a restricted resource that can only be accessed by user who are already logged in, it's natural because to post a new article, we have to know who the author is. In Spring Security, to restrict a resource for certain user( who has enough permission) to access is very simple, we simply declare <intercept-url> element under <http> in applicationContext-security.xml:
<intercept-url pattern="/newArticle.zul" access="ROLE_USER" requires-channel="https" />
Here the pattern attribute is used to determine which request this setting will take effect, and if a request's URL matches this pattern, the access attribute will be used to check current user's authorities.
As we can see, the page request protection under Spring Security is very straightforward and intuitive, it's all based on pattern matching to request path.
Log-in Page Implementation in ZK
Now, after the newArticle.zul has been secured, the log-in page(login.zul) which is required for authentication process has to be implemented for user to perform log in. Here let's see how to implement a log-in page in ZUL:
<html:form id="f" name="f" action="j_spring_security_check" method="POST"
xmlns:html="native">
<grid>
<rows>
<row>User: <textbox id="u" name="j_username"/></row>
<row>Password: <textbox id="p" type="password" name="j_password"/></row>
<row spans="2">
<hbox>
<html:input type="reset" value="Reset"/>
<html:input type="submit" value="Submit Query"/>
<button type="submit" label="Accedi_trendy" mold="trendy"/>
<button type="submit" label="Accedi_os" />
</hbox>
</row>
</rows>
</grid>
</html:form>
- An html <form> element has to be declared outside the username and password inputs. To use native html <form> element in ZUL, please refer to native namespace.
- The form must contains two input elements named j_username and j_passoword. To apply some ajax effect and features to these inputs, we can use ZK's <textbox> component instead.
- Form action attribute has to be mapped to login-processing-url attribute's value of <form-login> element in applicationContext-security.xml
If an anonymous user click the New Article button in homepage, he will be redirect to our login.zul.
Securing Partial View By Using EL in ZUL
In order to secure partial view part in web page, Spring Security has its own taglib which provides basic support for accessing security information and applying security constraints in JSPs(You can find the implementation in the source code of spring-security-taglibs-3.1.2.RELEASE.jar). And here for security in ZULs, ZK framework also provides approaches to define new tag library. In our demo application, for using Spring Security through EL we made our own tag library.
Zul Tag Library for Spring Security
Though ZK Developer Reference provides very detailed information on what we can do in a custom taglib of ZK, here we only focus on what we want to have for Spring Security.
First, let's create a security.tld under /WEB-INF/. However, for more serious projects, you can do the classpath:metainfo/tld/config.xml method as mentioned in the Developer Reference.
Second, define the EL functions in /WEB-INF/security.tld, here's the example:
<taglib>
<uri>http://www.zkoss.org/demo/integration/security</uri>
<description>
Methods and actions for ZK + Spring Security
</description>
<function>
<name>isAllGranted</name>
<function-class>org.zkoss.demo.springsec.SecurityUtil</function-class>
<function-signature>
boolean isAllGranted(java.lang.String authorities) {
</function-signature>
<description>
Return true if the authenticated principal is granted authorities
of ALL the specified roles.
</description>
</function>
...
As we can see, the document needs to start with a root element: <taglib> with an <uri> element in it. Then, you can declare functions. In the function declaration, we mapped a method(isAllGranted()) of org.zkoss.demo.springsec.SecurityUtil to our EL function isAllGranted, about the SecurityUtil, the implementation of it is based on Spring Security's SecurityContextHolder
Now let's see how to use the custom taglib of Spring Security in ZUL.
Usage: Protect Action
In our demo project, according to the use cases above, we have a "delete article" function that only user with role: ROLE_EDITOR can access. Now, with our custom taglibs for Spring Security, we are able to set up some constraints for it like this:
<?taglib uri="/WEB-INF/security.tld" prefix="sec"?>
...
<button id="deleteBtn" label="Delete"
if="${sec:isAllGranted('ROLE_USER')}"
disabled="${not sec:isAllGranted('ROLE_EDITOR')}"/>
...
Special ZK Component Attributes for Security
In the sample code above, I used custom EL + disabled attribute of ZK button to protect the click from user, ZK also provides other attributes that may be applicable for security constraint such as visible and readonly(for combobox). These attributes which are based on html effects are very useful to fulfill the requirements of the escalation of permission to access UI.
To be more secured and to protect the server-side access of a ZK component, please refer to this article: Block Request for Inaccessible Widgets
Protecting ZK Ajax Request
In an Ajax framework like ZK, there are two kind of requests. Now, let's talk about the most difficult part of this integration i.e. how to deal with ZK's Ajax request by using Spring Security?
First, we have to take a look at how to protect a page request in a more programmatic way.
Do Security Check in ZK's Listener
In some situation, user might need to do security check in java code not in ZUL, for example, in our accessDeniedExTest.zul, we have an Initiator that will do security check in public void doInit(Page page, Map<String, Object> args) throws Exception method:
public class AccessDeniedExInit extends GenericInitiator {
public void doInit(Page page, Map<String, Object> args) throws Exception {
if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException("this is a test of AccessDeniedException!");
}
}
}
The AccessDeniedException is a key to Spring Security's filter chain to handle, and everything works fine during a page request situation, but what if we throw the exception in an action say ZK's EventListener?
Security Check in ZK's Action
Without any further adapting code, throwing AccessDeniedException during an Ajax request won't trigger Spring Security's authentication and authorization process.This is because to the ZK AU Engine's perspective, it's impossible for any other outsider of org.zkoss.zk.au.http.DHtmlUpdateServlet to know how to compose a meaningful and correctly-structured response to the client engine at the browser. So any thrown exception must be handled inside ZK AU Engine.
To over come this, we have to convert ZK's Ajax request to a normal page request, so then we can let Spring Security to handle the AccessDeniedException in a normal page request. the implementation of this idea includes 3 steps:
STEP 1: Convert Ajax Request to Page Request
First, we have to tell ZK we want to custom the error handling mechanism for AccessDeniedException. To do that is very simple, in zk.xml we add this section:
<error-page>
<exception-type>org.springframework.security.access.AccessDeniedException</exception-type>
<location>security_process.zul</location>
</error-page>
ZK's error handling is very straightforward, you map an error type to a ZUL page, then the ZUL page will be parsed based on current desktop if such error happens. In our security_process.zul we simply have an initiator inside to handle AccessDeniedException:
<?init class="org.zkoss.demo.springsec.ui.error.SpringSecurityHandleInit"?>
<zk>
<!-- DO NOTHING! -->
</zk>
Then in SpringSecurityHandleInit, we can check if this is an Ajax request, store every needed information in session and ask the client engine to do the redirection back to this security_process.zul again.
if(exec.isAsyncUpdate(null) ){
//STEP 1: convert Ajax Request to Page Request(Error Handling Page Request)
System.out.println(">>>> Security Process: STEP 1");
if(ex instanceof AccessDeniedException){
sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri());// for login-success-url
sess.setAttribute(VAR_SPRING_SECURITY_ERROR, ex);
Executions.sendRedirect(toSecurityProcessUrl((AccessDeniedException) ex));// GOTO STEP 2 by redirection.
}else{
throw new IllegalArgumentException(
"How come an unexpected Exception type will be mapped to this handler? please correct it in your zk.xml");
}
STEP 2: Throw AccessDeniedException in Redirection to adapt Spring Security Process
The redirection will be back to this security_process.zul again, and because this time we are inside a page request, we can get the exception back from HttpSession and throw it out to let Spring Security filter chain to handle it.
Exception err = (Exception) sess.getAttribute(VAR_SPRING_SECURITY_ERROR);
String dtPath = (String) sess.getAttribute(VAR_DESKTOP_REQ_URI);
if(err!=null){
//STEP 2: throw Error in Error Handling Page Request.
System.out.println(">>>> Security Process: STEP 2");
sess.removeAttribute(VAR_SPRING_SECURITY_ERROR);
throw err;// we suppose Spring Security Error Filter Chain will handle this properly.
}
Now, Spring Security will check current user's principle and do authentication or authorization process. In authentication processing, user will be redirected to the login.zul as what we configured, and if log-in is success, user will be redirected to the original location where this exception occurred and the location here will be our security_process.zul which is not the original place where the Ajax request caused the error. So we need the third step to handle this situation.
STEP 3: Handle the Login Success Redirection
Let's go back to see the sample code of STEP1, the code
sess.setAttribute(VAR_DESKTOP_REQ_URI, getOriginalDesktopUri());
reserved the original request path of ZUL which generated the desktop, and which the information is prepared for STEP 3, the implementation of how to retrieve original desktop path(impl of getOriginalDesktopUri()) is as follows:
private static String getOriginalDesktopUri(){
// developer may implement this part to adapt to PushState or any other Page based Framework, that might have interference to request URI.
String str = Executions.getCurrent().getDesktop().getRequestPath();
String qs = Executions.getCurrent().getDesktop().getQueryString();
System.out.println(">>>security Process: Desktop path= "+str);
return str+"?"+qs;
}
And now in STEP 3, we simply redirect user back to our desktop path:
else if(dtPath!=null){
System.out.println(">>>> Security Process: STEP 3");
//STEP 3: if Spring Security Authentication was triggered at STEP 2,
//then we need STEP 3 to redirect back to original URI the very first desktop belongs to.
sess.removeAttribute(VAR_DESKTOP_REQ_URI);
exec.sendRedirect(dtPath);
}
For more details, please take a look at the source code of SpringSecurityHandleInit.
Usage: Protect Open Editor Button
After weaving ZK Ajax request to Spring Security, let's see how to apply security constraint to user's action in our demo project, first, in one of our use case scenario user is allowed to edit an article if he is the author or he has 'ROLE_EDITOR, so the open editor button in ArticleContentViewCtrl for articleContent.zul is implemented like this:
@Listen("onClick=#openEditorBtn")
public void edit(){
//ownership & permission check.
if(!isOwner() && SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException(
"The user is neither the author, nor a privileged user.");
}
ArticleEditor editor = new ArticleEditor();
editor.setParent(container);
editor.doHighlighted();
}
As shown, we throw AccessDeniedException directly in an event listener, and we can do the same thing in ZK View Model's action:
public class TestVModel {
...
@Command
@NotifyChange("fullName")
public void doChange(){
if(SecurityUtil.isNoneGranted("ROLE_EDITOR")){
throw new AccessDeniedException("you are not an editor!");
}
}
}
If you are using a Spring bean with @Secured annotation tagged on some methods, when user doesn't pass the security check, the thrown AccessDeniedException will be handled in the same way.
Summary
In this article, we discussed how to use Spring Security in a ZK web application, we went though all major topics developers may think about while doing this kind of integration, and also showed a workaround of adapting ZK's Ajax request to Spring Security's filter chain.
If there's any thing about this topic you'd want to know more or any part that I should discuss in this article, please feel free to leave a comment a below.
Comments
Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License. |