Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Building a SpringMVC Project with Apache Shiro Integration

Tech May 18 2

This tutorial demonstrates how to integrate Apache Shiro security framework with a SpringMVC application in IntelliJ IDEA. The setup includes authentication, authorization, and role-based access control.

Project Structure

After creating and configuring the Maven project, the directory structure is as follows:

pom.xml Configuration

The Maven configuration file defines all necessary dependencies for SpringMVC and Shiro:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>secure-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <spring.version>4.3.7.RELEASE</spring.version>
        <shiro.version>1.4.0</shiro.version>
        <javax.servlet-api.version>4.0.0-b07</javax.servlet-api.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- Spring Framework Dependencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- Servlet and JSP Dependencies -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>${javax.servlet-api.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>javax.servlet.jsp-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- Apache Shiro Dependencies -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>${shiro.version}</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-ehcache</artifactId>
            <version>${shiro.version}</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>secure-app</finalName>
    </build>
</project>

web.xml Configuration

The deployment descriptor configures the Spring DispatcherServlet and Shiro filter:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
         http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <display-name>secure-app</display-name>

    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/mvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

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

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring/mvc-config.xml,classpath:spring/shiro-config.xml</param-value>
    </context-param>

    <!-- Shiro Filter Configuration -->
    <filter>
        <filter-name>securityFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>securityFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

MVC Configuration

The Spring MVC configuration file enables annotation-driven controllers and configures view resolution:

<?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:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/mvc 
       http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
        http://www.springframework.org/schema/beans 
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/context 
        http://www.springframework.org/schema/context/spring-context-3.1.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">

    <!-- Enable Spring MVC Annotations -->
    <mvc:annotation-driven enable-matrix-variables="true"/>

    <!-- Static Resource Handling -->
    <mvc:default-servlet-handler/>

    <!-- View Resolver Configuration -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <!-- Component Scanning for Controllers -->
    <context:component-scan base-package="com.example.secureapp.controller"/>

    <!-- Enable AspectJ for AOP -->
    <aop:aspectj-autoproxy/>
</beans>

Shiro Configuration

The Shiro security configuration defines the realm, security manager, and filter chain:

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

    <!-- Custom Realm Implementation -->
    <bean id="authRealm" class="com.example.secureapp.security.AuthRealm">
    </bean>

    <!-- Security Manager -->
    <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="authRealm"/>
    </bean>

    <!-- Shiro Filter Factory Bean -->
    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
        <property name="securityManager" ref="securityManager"/>
        <property name="loginUrl" value="login"/>
        <property name="filterChainDefinitions">
            <value>
                /login = anon
                /authenticate = anon
                /adminPanel = roles["admin"]
                /** = authc
            </value>
        </property>
    </bean>

    <!-- Shiro Lifecycle Bean Post Processor -->
    <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

    <!-- Enable Shiro Annotations via Spring AOP -->
    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
          depends-on="lifecycleBeanPostProcessor"/>
    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>
</beans>

Controller Implementation

The main controller handles authentication requests and demonstrates Shiro annotations for authorization:

package com.example.secureapp.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class AppController {

    @RequestMapping(value = {"/", "/index"})
    public ModelAndView home(Model model) {
        ModelAndView mav = new ModelAndView("index");
        mav.addObject("message", "Welcome to Secure App!");
        return mav;
    }

    @RequestMapping("login")
    public String showLogin(Model model) {
        return "login";
    }

    @RequestMapping("adminPanel")
    public ModelAndView adminPanel(Model model) {
        ModelAndView mav = new ModelAndView("adminPanel");
        mav.addObject("message", "Admin Panel Loaded!");
        return mav;
    }

    @RequestMapping(value = "authenticate", method = RequestMethod.POST)
    public String authenticateUser(String username, String password, Model model) {
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            subject.login(token);
            return "redirect:/index";
        } catch (UnknownAccountException e) {
            model.addAttribute("message", "User not found!");
            return "login";
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("message", "Invalid password");
            return "login";
        }
    }

    @RequiresPermissions("user:query")
    @RequestMapping(value = {"/testPermissions"})
    public ModelAndView testPermissions(Model model) {
        ModelAndView mav = new ModelAndView("testShiro");
        mav.addObject("message", "Permission Test Successful!");
        return mav;
    }

    @RequiresRoles("admin")
    @RequestMapping(value = {"/testRoles"})
    public ModelAndView testRoles(Model model) {
        ModelAndView mav = new ModelAndView("testShiro");
        mav.addObject("message", "Role Test Successful!");
        return mav;
    }
}

Custom Realm Implementation

The custom realm handles authentication and authorization logic:

package com.example.secureapp.security;

import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class AuthRealm extends AuthorizingRealm {
    
    private static final Map<String, String> credentials = new HashMap<>();

    static {
        credentials.put("admin", "password123");
        credentials.put("test", "password123");
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        List<String> permissions = new ArrayList<>();
        
        permissions.add("user:create");
        permissions.add("user:remove");
        
        if ("admin".equals(username)) {
            permissions.add("user:query");
        }
        
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.addStringPermissions(permissions);
        
        if ("admin".equals(username)) {
            info.addRole("admin");
        }
        
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) 
            throws AuthenticationException {
        String username = (String) token.getPrincipal();
        
        if (username == null || username.isEmpty()) {
            return null;
        }
        
        if (!credentials.containsKey(username)) {
            return null;
        }
        
        String password = credentials.get(username);
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}

JSP View Files

index.jsp - Main landing page:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<html>
<body>
    <h2>${message}</h2>
    <form method="post" action="testPermissions">
        <input type="submit" value="Test Permissions">
    </form>
    <form method="post" action="testRoles">
        <input type="submit" value="Test Role Annotation">
    </form>
    <form method="post" action="adminPanel">
        <input type="submit" value="Test XML Role Config">
    </form>
</body>
</html>

login.jsp - Login form:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Login</title>
</head>
<body>
<form id="loginForm" name="loginForm" method="post" action="authenticate">
    <p align="center">User Login</p>
    <table width="296" border="1" align="center">
        <tr>
            <td width="98" height="34">Username:</td>
            <td width="182"><input name="username" type="text" id="username"/></td>
        </tr>
        <tr>
            <td height="36">Password:</td>
            <td><input name="password" type="password" id="password"/></td>
        </tr>
        <tr>
            <td height="35" colspan="2">
                <input type="submit" value="Submit"/>
                <input type="reset" value="Reset"/>
                ${message}
            </td>
        </tr>
    </table>
</form>
</body>
</html>

testShiro.jsp - Authorization test result page:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Test Result</title>
</head>
<body>
    ${message}
</body>
</html>

Testing Results

When testing with incorrect credentials, the system displays an appropriate error message. With valid admin credentials (admin/password123), the user is redirected to the index page successfully.

Testing role-based access with the admin account works correctly. After clearing session data and logging in as a standard user (test/password123), the user can access the index page but receives an authorization exception when attempting to access protected resources that require specific permissions.

The XML-based role configuration and annotation-based role verification both funcsion as expected, providing flexible options for securing application resources.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.