Chuyển đến nội dung chính

[Shiro][Spring-MVC] Apache Shiro with Spring framework, Java config and WebApplicationInitializer



Recently I was adding Apache Shiro security framework to Spring based web application which is using Java config and doesn't have xml configuration at all, not even web.xml

Apache Shiro documentation is mostly using xml examples so it took some time to put it all together in Java config based application.

Central part of Shiro security is  a realm. Here is how official Shiro documentation defines realms:

"A Realm is a component that can access application-specific security data such as users, roles, and permissions. The Realm translates this application-specific data into a format that Shiro understands so Shiro can in turn provide a single easy-to-understand Subject programming API no matter how many data sources exist or how application-specific your data might be."

Shiro comes with number of out-of-the-box Realm implementations that connects directly to database, to LDAP, etc, but in this example we will use custom Realm implementation since we want to access user data via our own user manager.

First, we have SecurityConfig. java where all security related beans are defined.

package com.xxx.yyy.config;

import com.xxx.yyy.security.CustomSecurityRealm;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

@Configuration
public class SecurityConfig {

    @Bean
    public CustomSecurityRealm customSecurityRealm(){
        return new CustomSecurityRealm();
    }

    @Bean
    public WebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(customSecurityRealm());
        return securityManager;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public MethodInvokingFactoryBean methodInvokingFactoryBean(){
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager");
        methodInvokingFactoryBean.setArguments(new Object[]{securityManager()});
        return methodInvokingFactoryBean;
    }

    @Bean
    @DependsOn(value="lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        return new DefaultAdvisorAutoProxyCreator();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

}


First bean defined is our custom security realm implementation. We will take a look at it in a moment, but for now let just look where is it used. And we don't have to go far, it is used by shiro security manager defined as second bean in SecurityConfig.java. We use DefaultWebSecurityManager since we plan to use Shiro for securing our applications URLs.
We just create an instance and inject our custom securtity realm bean to it.

After that we have few Shiro beans and we just inject our security manager bean wherever required.

Let's look how our custom security realm implementation looks like.

package com.xxx.yyy.security;

import com.xxx.yyy.security.Role;
import com.xxx.yyy.security.Permission;
import com.xxx.yyy.User;
import com.xxx.yyy.UserManager;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

public class CustomSecurityRealm extends AuthorizingRealm {

    @Autowired
    private UserManager userManager;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        Set roles = new HashSet<>();
        Set permissions = new HashSet<>();

        Collection principalsList = principals.byType(User.class);
        for (User user : principalsList) {
            for (Role role : user.getRoles()) {
                roles.add(role.getName());
                for (Iterator iterator = role.getPermissions().iterator(); iterator.hasNext(); ) {
                    Permission permission = iterator.next();
                    permissions.add(new WildcardPermission(permission.name()));
                }
            }
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);
        info.setRoles(roles);
        info.setObjectPermissions(permissions);

        return info;
    }

     @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken upat = (UsernamePasswordToken) token;
        User user = userManager.getByUsername(upat.getUsername());
        if(user != null && user.getPassword().equals(new String(upat.getPassword()))) {
            return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        }
        else {
            throw new AuthenticationException("Invalid username/password combination!");
        }
    }
}

Our security  realm implementation will be used both for authentication and authorization so we extend  AuthorizingRealm which extends AuthenticatingRealm.
Autowired UserManager is our application service for accessing users and their roles and permissions.

doGetAuthenticationInfo method is used to authenticate user, and it has one argument - AuthenticationToken which holds username and password entered by user in login form.
Inside this method we check if user for given username exists and if password matches the password enetered by user. If those conditions are satisfied, we return AuthenticationInfo object with our user object as principal. We use Shiro's SimpleAuthenticationInfo implementation of AuthenticationInfo interface.
If user doesn't exist or password doesn't match we throw Authentication exception.
This is very simple example, in real project we will probably use Shiro's HashedCredentialsMatcher for checking username/password combination since we probably want to use encoded passwords.

doGetAuthorizationInfo method is used by Shiro to get roles and permissions for specific principal(s) so it has PrincipalCollection as argument. For every principal in given collection (usually there will be only one) we will get roles and permissions and set them to AuthorizationInfo which will be returned by this method. We use Shiro's SimpleAuthorizationInfo implementation for this purpose. The code should be pretty self-explanatory.


Now that we have our basic security infrastructure defined, we need to integrate it with our web application.
In order to protect urls we need to add Shiro filter to our web app descriptor.  As I mentioned in the beginning of this post, we don't use web.xml but instead we have WebApplicationInitializer.
here is how it looks like:

package com.xxx.yyy;

import com.xxx.yyy.config.DataConfig;
import com.xxx.yyy.config.SecurityConfig;
import com.xxx.yyy.config.WebConfig;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.DispatcherServlet;

public class WebInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {

        // Create the 'root' Spring application context
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register( DataConfig.class, SecurityConfig.class);

        // Manage the lifecycle of the root application context
        container.addListener(new ContextLoaderListener(rootContext));


        // Create the dispatcher servlet's Spring application context
        AnnotationConfigWebApplicationContext dispatcherContext = new AnnotationConfigWebApplicationContext();
        dispatcherContext.setServletContext(container);
        dispatcherContext.setParent(rootContext);
        dispatcherContext.register(WebConfig.class);


        // Register and map the dispatcher servlet
        ServletRegistration.Dynamic dispatcher = container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");


        container.addFilter("shiroFilter", new DelegatingFilterProxy("shiroFilterBean", dispatcherContext))
                           .addMappingForUrlPatterns(null, false, "/*");

    }
}
The code speaks for itself. We have root context with DataConfig (which contains JPA configuration, but this is not relevant for this story) and our SecurityConfig explained earlier.

In order to configure our Spring MVC  we added dispatcher context,and registered WebConfig class which contains required beans.

Last bean is most relevant since it defines Shiro filter which is configured to intercept all URLs.
We use DelegatingFilterProxy as filter implementation, and we provide "shiroFilterBean" for bean name.
This bean is defined in our WebConfig class, so let's take a look at it: 
package com.xxx.yyy;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.xxx.yyy.web"})
public class WebConfig extends WebMvcConfigurerAdapter {


    @Autowired
    private WebSecurityManager securityManager;


    @Bean
    public VelocityConfigurer velocityConfig() {
        VelocityConfigurer configurer = new VelocityConfigurer();
        configurer.setResourceLoaderPath("/WEB-INF/templates");
        Properties props = new Properties();
        props.put("output.encoding", "UTF-8");
        props.put("input.encoding", "UTF-8");
        configurer.setVelocityProperties(props);
        return configurer;
    }

    @Bean
    public VelocityViewResolver viewResolver() {
        VelocityViewResolver resolver = new VelocityLayoutViewResolver();
        resolver.setExposeSpringMacroHelpers(true);
        resolver.setContentType("text/html;charset=UTF-8");
        resolver.setSuffix(".vm");
        return resolver;
    }

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("/WEB-INF/localization/messages");
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setCacheSeconds(10);
        return messageSource;
    }

    @Bean
    public LocaleResolver localeResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        localeResolver.setCookieName("LOCALE");
        return localeResolver;
    }


    @Bean
    public ShiroFilterFactoryBean shiroFilterBean(){
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        Map definitionsMap = new HashMap<>();
        definitionsMap.put("/login.jsp", "authc");
        definitionsMap.put("/admin/**", "authc, roles[admin]");
        definitionsMap.put("/**", "authc");
        shiroFilter.setFilterChainDefinitionMap(definitionsMap);
        shiroFilter.setLoginUrl("/login.jsp");
        shiroFilter.setSecurityManager(securityManager);
        return shiroFilter;
    }
}


This is typical java config based Spring MVC configuration.
Beside usual Spring MVC beans, we added ShiroFilterFactoryBean (at the end) which will be referenced  from our WebApplicationInitializer , remember ?
ShiroFilterFactoryBean requires WebSecurityManager and since we defined it in SecurityConfig.java , all we need to do here is to autowire it to private field (line 12) and inject it to shiroFilterBean (line 60)

In between we just have velocity template engine configuration beans as well as localization beans which are not relevant for security framework.

Now, when user attempts to  access any application URL, Shiro filter will intercept it, and delegate security checking to shiroFilterBean which will use securityManager bean to determine if the user has right to access this specific URL.
If user is not authenticated yet, Shiro filter will redirect user to login page. Here is simple example of login.jsp:


Username:
Password:
Remember Me

And that's it... we have basic Shiro security setup. The way how application persists users, their roles and permission is application specific. All that Shiro has to know about this will get it from our security realm implementation.


Reference:
https://beegor.blogspot.com/2013/10/apache-shiro-with-spring-framework-java.html

Nhận xét

Bài đăng phổ biến từ blog này

[ebook] Phần I - Tổng hợp nội dung sách "Nuôi con không phải cuộc chiến"

Phần I - Nuôi con không phải cuộc chiến Chương 1: ăn ngủ tự lập mẹ nhàn con ngoan EASY: eat - activity - Sleep - Your time -> Là chu kỳ sinh hoạt lặp đi lặp lại của bé trong một khoảng thời gian 1 ngày của bé. Có thể bạn quan tâm: Khuyến mãi mua trọn bộ sách Nuôi con không phải cuộc chiến I. Nếp sinh hoạt EASY   1. Lợi ích EASY: Đối với bé: + Nhận biết được những gì xảy ra tiếp theo -> Tăng khả năng tự tin của con. + Tập cho bé phản xạ có điều kiện. + Kết nối nhịp sing học của con. Đối vơí mẹ: + Biết cách phản ứng với những nhu cầu khác nhau của bé, không nhầm lẫn giữa khi bé khóc đòi ăn hay làm nũng.  Về lâu dài: + EASY là nền tảng cơ bản giúp rèn luyện sự tự lập ở bé. + Tạo nếp sinh hoạt ăn ngủ điều độ. (Khi con càng lớn chu kỳ EASY càng dài ra) 2.   Chu kỳ 03h: Cho bé từ 0 - 3 tháng tuổi. Bé ăn cách nhau 03 giờ. Cho con ngủ theo bảng thời gian hoặc căn cứ vào dấu hiệu của bé. Cân nặng tiêu chuẩn 2.7kg 3...

[AI] BÀI 4: Tác nhân và môi trường (Agent and Environment)

1. Agent (tác nhân): l à tất cả những gì có thể nhận thức về môi trường của nó thông qua cảm nhận "Sensor" và đưa ra hành động tác động đến môi trường (effective). Có 03 loại agent: human, software, robotic. + Cấu trúc của Agent: Gồm 2 phần:  Architecture + Agent Program + Phân loại Agent: -  Simple Reflex Agents: Agent phản ứng đơn giản. - Model Based Reflex Agents: Agent phản xạ dựa trên model - Goal Based Agents: Agent dựa trên mục tiêu. - Utility Based Agents: Agent dựa trên tính tiện ích. 2. Turing test : Ứng dụng trong việc kiểm tra và đáng giá máy móc có thật sự thông minh?  https://vi.wikipedia.org/wiki/Ph%C3%A9p_th%E1%BB%AD_Turing   3. Các thuộc tính của môi trường Discrete / Continuous  − If there are a limited number of distinct, clearly defined, states of the environment, the environment is discrete (For example, chess); otherwise it is continuous (For example, driving). Observable / Partially Observable  ...

[Tool] Apache Nifi

Introduction Apache NiFi is a dataflow system based on the concepts of flow-based programming. It supports powerful and scalable directed graphs of data routing, transformation, and system mediation logic. NiFi has a web-based user interface for design, control, feedback, and monitoring of dataflows. It is highly configurable along several dimensions of quality of service, such as loss-tolerant versus guaranteed delivery, low latency versus high throughput, and priority-based queuing. NiFi provides fine-grained data provenance for all data received, forked, joined cloned, modified, sent, and ultimately dropped upon reaching its configured end-state. See the  System Administrator’s Guide  for information about system requirements, installation, and configuration. Once NiFi is installed, use a supported web browser to view the UI. Browser Support Browser Version Chrome Current and Current - 1 FireFox Current and Current - 1 Edge Current an...

[Network] ARQ - Automatic repeat request

Automatic Repeat reQuest (ARQ) hay  Automatic Repeat Query là một phương thức điều khiển lỗi cho quá trình truyền dữ liệu bằng cách sử dụng ACK (acknowledgements) và Time Out, cho phép truyền dữ liệu tin cậy trên nền một dịch vụ không tin cậy (unreliable service). 1. ARQ protocol Gồm 03 loại Stop-and-wait ARQ Go-Back-N ARQ Selective Repeat ARQ / Selective Reject 2. Lĩnh vực liên quan Linked Data Transport Layer OSI Model. Ngoài ra có một số bằng sáng chế trong lĩnh vực live video contribution environments  sử dụng tới ARQ.

[LB-HA] Understand about High Available (HA) and Load Balancing

High Available (HA) :  Hỗ trợ dự phòng tiến trình. Hoạt đông với cơ chế Active - Passive . Hệ thống tồn tại 02 loại Component với role 'Active' và 'Passive'.  Active   Component sẽ đảm nhận việc xử lý tiến trình. Passive Component đóng vai trò backup. Trường hợp Active Component gặp lỗi (fail, downtime) hệ thống sẽ chuyển sang hoạt động trên B ackup  Component . Quá trình chuyển từ Active Component sang Passive Component gọi là 'Fail over'. Một số khái niệm liên quan đến HA: - FailOver: Chuyển đổi tiến trình chạy trên Passive Component khi Active Component gặp sự cố. - Fail Back: Khôi phục lại tiến trình hoạt động trên Active Component sau khi tiến trình dịch chuyển đến Passive Component trong quá trình FailOver. - Fault - Tolerant: Công nghệ giúp đảm bảo tính liên tục của dịch vụ. Trường hợp một thành phần trong hệ thống bị hoạt động gián đoạn vẫn cho phép toàn bộ hệ thống hoạt động ổn định. Load Balancing : Hoạt động với cơ chế Active - Active ....

[Xu hướng] Open API – Xu thế phát triển mới của ngành công nghiệp phần mềm

Xuất bản: 2016-06-30 00:19:09 Các nhà phát triển đã nhận ra rằng việc lãng phí thời gian công sức vào thiết kế lại những thứ đã được các công ty khác xây dựng là hết sức không nên. Thay vào đó, họ hoàn toàn có thể dựa vào những API được các nhà cung cấp nền tảng như Salesforce, Amazon, Google… và mới đây là các nhà phát triển độc lập khác đưa ra. Trong ngành công nghiệp sản xuất phần mềm, mỗi ứng dụng khi được đưa tới công chúng đều nhằm mục đích phục vụ cho một nhu cầu người dùng nhất định.  Có những ứng dụng phục vụ cho mục đích học tập, hoặc giải trí, du lịch, một số khác phục vụ cho mục đích đi lại như GrabTaxi hay Uber chẳng hạn. Mặc dù những ứng dụng này có thể giúp đáp ứng những nhu cầu khác nhau của người sử dụng nhưng cũng không phải vì thế mà chúng được làm ra theo những cách hoàn toàn khác nhau. Bên cạnh đó, tuy có những mục đích sử dụng khác nhau nhưng những ứng dụng như vậy vẫn có những điểm chung, tương đồng về chức năng.  Dù bạn đang sử dụng Faceb...

[Mac OS] Cài đặt maven apache

Ngày tạo: 25/12/2016 Bước 1: Download maven apache từ liên kết:  https://maven.apache.org/download.cgi?Preferred=ftp://mirror.reverse.net/pub/apache/ Bước 2: Cài đăt biến môi trường: mở vào file .bash_profile thêm các dòng. (Nếu file chưa tồn tại thì tạo mới) $ vim ~/.bash_profile export JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home export M2_HOME=/Users/mac/Downloads/Programs/apache-maven-3.3.9 export M2=$M2_HOME/bin export PATH=$PATH:$M2_HOME/bin Bước 3 :  Restart Terminal, kiểm tra cài đặt thành công từ terminal bằng lệnh: $ mvn -version Trường hợp cài đặt thành công kết quả trả về như sau: Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-10T23:41:47+07:00) Maven home: /Users/mac/Downloads/Programs/apache-maven-3.3.9 Java version: 1.8.0_60, vendor: Oracle Corporation Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_60.jdk/Contents/Home/jre Default locale: en_US, platform encoding: UTF-8 ...

[PM4P] First step for reaching to PMP Certificate

27/07/2019: First step for reaching to PMP Certificate

Quản lý Session khi cấu hình dự phòng máy chủ sử dụng HAproxy

1.        Vấn đề Cùng một HTTP session có thể nằm trên nhiều kết nối TCP khác nhau. Trong điều kiện không sử dụng Load Balancer, sẽ không phát sinh các vấn đề về quản lý phiên – session. Thông tin session của tất cả người dùng được nhận biết thông qua một máy chủ duy nhất. Tất cả kết nối của Client đều được chuyển đến một máy chủ duy nhất. Ở chế độ dự phòng, khi người quản trị cài đặt nhiều hơn một server, vấn đề về quản lý session sẽ xuất hiện. Máy chủ ứng dụng có nguy cơ không thể access thông tin session người dùng. 2.        Phương án cấu hình dự phòng khi sử dụng session ·          Replication : Sử dụng cơ chế sao lưu session của web server để đảm bảo rằng tất cả các máy chủ ứng dụng thuộc cluster khác nhau đều có thông tin của Session. Một số web server phổ biến như tomcat đều hỗ trợ cơ chế replication session này. ·       ...