Spring Security

From Documentation
Revision as of 05:17, 13 July 2023 by Hawk (talk | contribs) (→‎Login Page)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)



Overview

Spring Security is an application framework that provides security services for J2EE-based enterprise software application. It is a popular and widely adopted framework, in this article we will demonstrate how to integrate it to secure a ZK application including securing pages, handling authentication process, securing components, and securing events. Our example is a simple forum-like application. Users can read, create, edit, and delete an article according to his authorities.

Configuration

Maven

We need to add dependencies for Spring Security and Maven's transitive dependency management can include all necessary dependencies of Spring for us.

		<!-- Spring Security -->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
			<version>${springsecurity.version}</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
			<version>${springsecurity.version}</version>
		</dependency>

		<!-- extra -->
		<dependency>
			<groupId>commons-logging</groupId>
			<artifactId>commons-logging</artifactId>
			<version>1.1.1</version>
		</dependency>
		<dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib</artifactId>
			<version>2.2</version>
		</dependency>
  • Line 4: Becuase we use the security namespace in the application context, we need spring-security-config.
  • Line 16: Spring-core depends on commons-logging.
  • Line 21: The cglib is optional. We add it because we use CGLIB-based class proxy.

Icon info.png Note: If you don't use Maven, please refer to Spring Security Reference Documentation to check which JAR files are needed.

Spring

Our example application also integrates Spring framework, the required configuration in web.xml is as follows:

web.xml

	<!-- Loads the Spring application context configuration -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	 <!-- For using web scoped bean -->
	<listener>
		<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
	</listener>

The ContextLoaderListener will load /WEB-INF/applicationContext.xml (Spring configuration file) by default, and we follow this convention so we don't need to add extra configuration in web.xml.

applicationContext.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:aop="http://www.springframework.org/schema/aop"
	xsi:schemaLocation=
	"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	 http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
	
	<context:component-scan base-package="org.zkoss.reference.developer.spring.security.model"/>

	<import resource="applicationContext-security.xml"/>
</beans>
  • Line 9: We can register beans by class-path scanning to reduce XML configuration effort.
  • Line 11: We can import another configuration file for Spring Security.

Security Namespace Configuration

The first configuration you should add to use Spring Security is a filter declaration in web.xml:

	<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>

This filter is a hook into Spring Security's web infrastructure. It intercepts all requests and hands over them to be processed by Spring Security internal filters.

Namespace configuration has been supported by Spring framework since version 2.0 and it is an alternative configuration syntax which is closer to problem domain. It also can reduce configuration's complexity because one element may contain multiple beans and processing steps. To use the security namespace, you should have spring-security-config in your classpath and add the schema declaration to your application context file:

applicationContext-security.xml

<beans:beans xmlns="http://www.springframework.org/schema/security" 
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation=
	"http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
	 http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
	 http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">


 	<!--  HTTP configuration sample -->
	<http auto-config="true">
		<!-- ZK AU reqeust -->
		<intercept-url pattern="/zkau/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- the login page -->
		<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- pages for anonymous access in an application -->
		<intercept-url pattern="/index.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<intercept-url pattern="/articleContent.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- secure pages -->
		<intercept-url pattern="/**" access="ROLE_USER" />
		
		<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> 	

	<!-- omit inactive configurations -->

	<authentication-manager>
		<authentication-provider user-service-ref="myUserDetailsService">
			<password-encoder hash="md5" />
		</authentication-provider>
	</authentication-manager>
	
</beans:beans>

Here we introduce some main elements and will leave the details in the subsequent sections.

  • LIne 12: The <http> element is the parent for all web-related namespace functions and we use auto-config to save configuration efforts. We also create a HTTPS configuration sample in applicationContext-security.xml. Please see source code for details.
  • Line 32: Each Spring Security application which uses the namespace configuration must include <authentication-manager> . It is responsible for registering the AuthenticationManager which provides authentication services to the application.
  • Line 33:We implement our MyUserDetailsService bean to provide authentication service and configure it in <authentication-provider> element.

Secure Pages

In Spring Security, pages are protected by <intercept-url> element under <http>. We can specify in pattern to match against the URLs of incoming requests using an ant path style syntax in <intercept-url> element. The access attribute defines the access permission for requests which match the given pattern. Here we use simple role-based access control.

In most cases, we usually secure all pages with :

<intercept-url pattern="/**" access="ROLE_USER" />

The "ROLE_USER" is an authority string we define and give for each authenticated user in our custom user service, MyUserDetailsService.

Then we can selectively allow some pages for anonymous access like:

<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />

IS_AUTHENTICATED_ANONYMOUSLY is a built-in permission value used to grant access to anonymous users

applicationContext-security.xml

 	<!--  HTTP configuration sample -->
	<http auto-config="true">
		<!-- ZK AU requests -->
		<intercept-url pattern="/zkau/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- the login page -->
		<intercept-url pattern="/login.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- pages for anonymous access in an application -->
		<intercept-url pattern="/index.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<intercept-url pattern="/articleContent.zul" access="IS_AUTHENTICATED_ANONYMOUSLY" />
		<!-- secure pages -->
		<intercept-url pattern="/**" access="ROLE_USER" />
		
		...
	</http>
  • Line 4: ZK AU requests must be available for anonymous access or ZK can't work normally.
  • Line 6: Remember to set login page URL available to anonymous users, otherwise users wont be able to access the log-in page (this is a common configuration error).
  • Line 14: Restrict all page requests with permission ROLE_USER.

Authentication

Setting auto-config enables form-based login process automatically but it uses Spring Security's built-in login page. We usually build our own login page so we can specify our custom login page URL to override the default configuration.

applicationContext-security.xml

 	<!--  HTTP configuration sample -->
	<http auto-config="true">
		...
		
		<form-login login-page="/login.zul" 
			authentication-failure-url="/login.zul?login_error=1" />
			
		<logout logout-success-url="/index.zul" invalidate-session="true" />
	</http>
  • Line 5: Specify the URL used to render the login page at login-page.
  • Line 5: Specify the URL to redirect the browser on login failure,
  • Line 8: Specify the destination URL in which the user will be redirected to after logging out.

In order to let Spring Security handle authentication, we should use HTML's form in a zul.

login.zul

	...
	<html:form 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>
    					<button type="reset" label="Reset" />
    					<button type="submit" label="Submit" />
    				</hbox>
    			</row>
    		</rows>
    	</grid>
    	</html:form>
  • Line 2: The default action URL monitored by Spring Security filter is j_spring_security_check.
  • Line 6,7: The login form should contain j_username and j_password input fields.


In most cases, each application will have its own way to authenticate a user and Spring Security provides various authentication provider to achieve it. We can create a simple MyUserDetailsService which implements Spring Security's UserDetailsService interface to perform our own authentication.

MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {

	private static final Map<String, MyUser> USERS = new HashMap<String,MyUser>();
	private static void add(MyUser mu){
		USERS.put(mu.getUsername(), mu);
	}
	static{
		
		add(new MyUser("rod","81dc9bdb52d04dc20036dbd8313ed055", //password:1234 
			new String[]{"ROLE_USER", "ROLE_EDITOR"} ));
		
		add(new MyUser("dianne","81dc9bdb52d04dc20036dbd8313ed055", 
			new String[]{"ROLE_USER", "ROLE_EDITOR"} ));
		
		add(new MyUser("scott","81dc9bdb52d04dc20036dbd8313ed055", 
			new String[]{"ROLE_USER"} ));
		
		add(new MyUser("peter","81dc9bdb52d04dc20036dbd8313ed055", 
			new String[]{"ROLE_USER"} ));
	}
	
	// must return a value or throw UsernameNotFoundException
	public UserDetails loadUserByUsername(String username)
			throws UsernameNotFoundException {
		//perform authentication
	}
}
  • Line 10: MyUser extends org.springframework.security.core.userdetails.User which implements org.springframework.security.core.userdetails.UserDetails.
  • Line 11: Here we define two authorities, ROLE_USER and ROLE_EDITOR.


Then specify user-service-ref with our MyUserDetailsService.

applicationContext-security.xml

	<authentication-manager>
		<authentication-provider user-service-ref="myUserDetailsService">
			<password-encoder hash="md5" />
		</authentication-provider>
	</authentication-manager>
  • LIne 2: Configure Spring Security to use our custom user service.

Secure Components

Every authenticated user has his/her own authorities. A common scenario is where we want to control UI's status according to current user's authorities. In our example, an anonymous user can only view an article, and a user with "ROLE_USER" can see a disabled "Delete" button. But a user with "ROLE_EDITOR" can see the "Delete" button and be able to click it.

Spring-security-anonymous.png
What a anonymous user see


Spring-security-user.png
What a user with "ROLE_USER" see


Spring-security-editor.png
What a user with "ROLE_EDITOR" see


How do we achieve this security control in a zul? We can implement a custom tag library to check current user's authorities and apply the tag library on if or disable attribute. Please refer to ZUML_Reference/ZUML/Processing_Instructions/taglib/Custom_Taglib for details.

The tag library's function is implemented in org.zkoss.reference.developer.spring.security.SecurityUtil. Here we just briefly explain how isAllGranted() is implemented and you can read other methods in the source code .

The SecurityContextHolder is the most fundamental object and is where Spring Security stores the present security context of the application, which includes details of the principal currently using the application. We can obtain user information from SecurityContextHolder including his authorities.

SecurityUtil.java

package org.zkoss.reference.developer.spring.security;

//omit import

public class SecurityUtil {

	public static boolean isAllGranted(String authorities) {
		if (null == authorities || "".equals(authorities)) {
			return false;
		}
		final Collection<? extends GrantedAuthority> granted = getPrincipalAuthorities();
		boolean isAllGranted = granted.containsAll(parseAuthorities(authorities)); 
		return isAllGranted;
	}

	private static Collection<? extends GrantedAuthority> getPrincipalAuthorities() {
		Authentication currentUser = SecurityContextHolder.getContext().getAuthentication();
		if (null == currentUser) {
			return Collections.emptyList();
		}
		if ((null == currentUser.getAuthorities()) || (currentUser.getAuthorities().isEmpty())) {
			return Collections.emptyList();
		}
		Collection<? extends GrantedAuthority> granted = currentUser.getAuthorities();
		return granted;
	}

	//omit other methods
}
  • Line 7: Return true if the authenticated principal is granted ALL of the roles specified in authorities. The input parameter is a comma separated list of roles which the user have been granted.

Then we still have to write a description file to describe the functions that we can use in a zul. In our example, it is /WEB-INF/security.tld. You can read the file in the source code.

security.tld

<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.reference.developer.spring.security.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>
	...
</taglib>

Before using a tag library in a zul, we should load its tld file with a directive.

articleContent.zul

<?taglib uri="http://www.zkoss.org/zkspring/security" prefix="sec"?>
...

			<button id="deleteBtn" label="Delete"
			if="${sec:isAllGranted('ROLE_USER')}"
			disabled="${not sec:isAllGranted('ROLE_EDITOR')}"/>
  • Line 1: Use a directive to load a custom tag library's tld file.
  • Line 5: Hide this button for anonymous users.
  • Line 6: Disable the button for those users without ROLE_EDITOR authority.

The function isAllGranted() will return true if the authenticated principal is granted all of the roles specified in authorities. An anonymous user doesn't have authority "ROLE_USER", so the "Delete" button will not be created. If a user have authority "ROLE_EDITOR", he can see an enabled "Delete" button (the disable will be false).


You can use our SecurityUtil and security.tld as reference and write your own one to apply in your application.

Secure Events

If you want to restrict available actions according to a business rule or a dynamic status, this cannot be achieved by tag library. To do this, Spring Security provides a "method security" which can add security to your service layer methods. To use this feature, you should declare as follows:

applicationContext-security.xml

<global-method-security secured-annotations="enabled" />

Then add @Secure to those methods you want to secure with permissions.

ArticleService.java

public interface ArticleService {

	@Secured({"ROLE_USER", "IS_AUTHENTICATED_ANONYMOUSLY"})
	public List<Article> findAll();
	
	@Secured({"ROLE_USER", "IS_AUTHENTICATED_ANONYMOUSLY"})
	public Article find(long id);
	
	@Secured({"ROLE_USER"})
	public void create(Article a);
	
	@Secured({"ROLE_EDITOR","ROLE_USER"})
	public void update(Article a);
	
	@Secured({"ROLE_EDITOR"})
	public void delete(long id);
}

If a user uses the service and has no permission, Spring Security will throw its AccessDeniedException.


If the security checking is more dynamic and cannot be determined in compile time, we can check a user's permission in an event listener of a controller. In our example, a user with "ROLE_EDITOR" can edit any article but a user with "ROLE_USER" can only edit those articles written by himself/herself. We can check this when a user clicks the "Edit" button:

permission checking in an event listener

@VariableResolver(DelegatingVariableResolver.class)
public class ArticleContentViewCtrl extends SelectorComposer<Component> {

	//omit other methods

	@Listen("onClick=#openEditorBtn")
	public void edit(){
		//ownership & permission check.
		if(!(isOwner() || SecurityUtil.isAllGranted("ROLE_EDITOR"))){
			throw new AccessDeniedException(
				"The user is neither the author, nor a privileged user.");
		}
		ArticleEditor editor = new ArticleEditor();
		editor.setParent(container);
		editor.doHighlighted();
	}
}
  • Line 9~11: If the current login user is neither the owner of the article nor has the authority "ROLE_EDITOR", we will not allow the editing of the article and throw a Spring Security's AccessDeniedException.


If we throw a runtime exception for an access with insufficient permission, ZK will show the error message on the page by default. But for an unauthenticated user (not log in yet), we can even do more: redirect the anonymous user to the login page. We will show how to achieve this in ZK:

First, we have to catch the exception thrown in an event listener by ZK error handling mechanism, configure <error-page> in zk.xml.

zk.xml

	<error-page>
		<exception-type>org.springframework.security.access.AccessDeniedException</exception-type>
		<location>/WEB-INF/errors/handleAccessDenied.zul</location>
	</error-page>


Then, create the error handling page. To avoid users visiting the page directly, we put it under /WEB-INF. This error handling page displays nothing but a page initiator to redirect an unauthenticated user to the login page.

handleAccessDenied.zul

<?init class="org.zkoss.reference.developer.spring.security.ui.error.AjaxAccessDeniedHandler"?>
<zk>
<!-- Forward a unauthenticated user to login page. -->
</zk>


The page initiator redirects the browser to login page if current user principle doesn't exist, otherwise, it displays exception's detail in a custom page.

AjaxAccessDeniedHandler.java

public class AjaxAccessDeniedHandler extends GenericInitiator {

	public void doInit(Page page, Map<String, Object> args) throws Exception {
		// when this initiator has been executed that means users encounter access denied problem.
		
		Execution exec = Executions.getCurrent();
		
		if (null == SecurityUtil.getUser()){ //unauthenticated user
			exec.sendRedirect("/login.zul");
		}else{
			//display error's detail
			Executions.createComponents("/WEB-INF/errors/displayAccessDeniedException.zul", null, args);
		}
	}
}

Login Page

If you implement a login page with zul, you need to configure Spring Security filter to ignore all AU requests (starting with /zkau). Because ZK framework communicates with a server via that channel for various things including firing events and getting resources. If the security filter blocks AU requests, your login page cannot work.

But not intercepting ZK AU requests is equivalent to opening an unchecked channel, which may result in security vulnerabilities.

Non-ZK login entrypoint

Hence, to ensure all zk components are under the security filter's protection, a good practice is to implement authentication without relying on ZK.

This can be done with form-based authentication by implementing the login form in HTML or JSP. And this can also be achieved with other authentication methods which don't require a ZK page to be served, such as redirecting to an external provider, using a SSO provider, etc.

With these options, the security filter doesn't have to be configured to allow anonymous access to ZK AU requests. After a user logs in, all requests including ZK AU can be intercepted by the security filter and rejected if the user is unauthorized or unauthenticated.

Example Source Code

All source code of examples used in this chapter can be found in here.




Last Update : 2023/07/13

Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.