spring boot shiro+session+redis realizes login session, session retention and distributed session sharing

reference resources: Shiro Springboot cluster shared Session (Redis) + single user login

https://zhuanlan.zhihu.com/p/54176956

Frame construction

1. Basic environment

jdk8
maven
lombok
spring boot 2.5.7

2. Import shiro maven coordinates

<dependency>
   <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.8.0</version>
</dependency>

3. Create a new custom Realm class to realize the core logic of authentication and authentication

Create userinfo java:

@Setter
@Getter
public class UserInfo implements Serializable {
    private String username;
    private String password;
    private Set<String> roles;
    private Set<String> perms;
}

Create customrealm java:

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
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.HashSet;

public class CustomRealm extends AuthorizingRealm {
    /**
     * identity authentication 
     * The main function is to provide an identity authentication function. The basic idea is to find the user's identity information from the database and give it to the Shiro framework. The Shiro framework will automatically compare it with the account information transmitted from the login page to see if it matches. If it matches, the login is successful, otherwise the login fails
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //Log in to TOKEN, including the user account and password
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        //The following multiple judgments can be added or deleted according to the business
        // Judge whether the user name does not exist, and throw an exception if it does not exist
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }
		
		//Analog data, you can obtain the current user information by looking up the database
        UserInfo user = new UserInfo();
        user.setUsername("aesop");
        user.setPassword("123");

        //The roles and permissions of the query user are saved in SimpleAuthenticationInfo, which can be used elsewhere
        //SecurityUtils.getSubject().getPrincipal() can take out all the user's information, including roles and permissions

        /** Save User permissions and roles into the User object*/
        HashSet<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("teacher");
        user.setRoles(roles);

        HashSet<String> perms = new HashSet<>();
        perms.add("blog:read");
        perms.add("blog:search");
        user.setPerms(perms);

        //You can also save additional information to the Session
     	//SecurityUtils.getSubject().getSession().setAttribute(Constants.SESSION_USER_INFO, userInfo);


        //Construction verification information return
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), getName());
        return info;
    }

    /**
     * to grant authorization
     * After the identification is completed, the permission is given to the current user for detailed control according to the permission in the future
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        //Gets the User object corresponding to the current User
        UserInfo user = (UserInfo) getAvailablePrincipal(principals);
        //Create permission object
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //Set user role (user.getRoles () is a set < string >, [admin,student...])
        info.setRoles(user.getRoles());
        //Set user license (user.getPerms() is a set < string >, [blog:read,blog:search...])
        info.setStringPermissions(user.getPerms());
        return info;
    }
}

3. Add Shiro interception configuration

Create shiroconfig java:

package com.example.springshirodemo.config.shiro;

import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;

/**
 * Shiro Core configuration
 */
@Configuration
public class ShiroConfig {
    /**
     * shiro Unified authority determination
     * Intercept or release permissions according to business needs. anon: all requests can be accessed, authc: access can only be accessed after login and authentication
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean() {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager(myRealm()));

        Map<String, Filter> filters = new HashMap<>();
        filters.put("authc", new LoginFormFilter());
        shiroFilterFactoryBean.setFilters(filters);
        Map<String, String> map = new HashMap<>();
        // Log in and log out
        map.put("/doLogin", "anon");
        map.put("/logout", "logout");
        // swagger
        map.put("/swagger**/**", "anon");
        map.put("/webjars/**", "anon");
        map.put("/v2/**", "anon");
        // Authenticate all users
        map.put("/**", "authc");
        // Not logged in, redirect path
        // shiroFilterFactoryBean.setLoginUrl("/login");
        // home page
        // shiroFilterFactoryBean.setSuccessUrl("/index");
        // Error page, authentication failed, jump
        // shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }

    /**
     * Inject custom CustomRealm into SecurityManager
     * @return
     */
    @Bean
    public CustomRealm myRealm() {
        return new CustomRealm();
    }

    /**
     * Inject custom CustomRealm into SecurityManager
     * Shiro Manage internal component instances through SecurityManager, and provide various security management services through it
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(CustomRealm customRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // Custom Realm
        securityManager.setRealm(customRealm);
        return securityManager;
    }
}

When the user-defined login fails or there is no login, the json format is returned instead of redirecting to login JSP page. Note: after this is configured, the redirect path configuration setLoginUrl will fail
Create ShiroLoginFilter class:

import cn.aesop.common.restful.ResultBean;
import cn.aesop.common.restful.ResultCode;
import com.alibaba.fastjson.JSON;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * @author: hxy
 * @description: Intercept the requests without login and return all json information Overwrite shiro's original jump login Interception method of JSP
 * @date: 2017/10/24 10:11
 */
public class ShiroLoginFilter extends FormAuthenticationFilter {

	@Override
	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
		PrintWriter out = null;
		HttpServletResponse res = (HttpServletResponse) response;
		try {
			res.setCharacterEncoding("UTF-8");
			res.setContentType("application/json");
			out = response.getWriter();
			out.println(JSON.toJSONString(ResultBean.FAIL(ResultCode.E_201)));
		} catch (Exception e) {
		} finally {
			if (null != out) {
				out.flush();
				out.close();
			}
		}
		return false;
	}
}

4. Annotation authority

Add the following comments on the Controller interface to intercept requests without permission

 @RequiresRoles(value={"admin","user"},logical = Logical.OR)
 @RequiresPermissions(value={"add","update"},logical = Logical.AND)

If multiple permissions / roles are verified, they are separated by "," in the middle. By default, all listed permissions / roles must be met at the same time before they take effect. However, there is logical = logical in the annotation Or this one. You can make permission control more flexible here.

If OR is set here, it means that one of the listed conditions can be met. If it is not written OR set to logical = logical And means that all listed must be met before entering the method.

I didn't have a deep understanding of the method of using subject to control through code, so I didn't find such permission control. In addition, the use of annotations is more concise and clear, so individuals prefer to use annotation to control.

So far, a basic shrio + spring boot framework has been built

5. Get context information

After successful login, you can obtain the currently logged in user information through the following code

Subject currentUser = SecurityUtils.getSubject();
UserInfo principal = (UserInfo)currentUser.getPrincipal();

//Or get customized information from the session
//Session session = SecurityUtils.getSubject().getSession();
//UserInfo principal = (UserInfo) session.getAttribute(Constants.SESSION_USER_INFO);

5. Password encryption

The above example password is directly stored in plaintext in the database, which is not secure. It can only be stored after encryption, and it should form a system with identity authentication. The basic modification steps are described below:
1) Create voucher matcher

/**
 * Voucher matcher
 * (Because our password verification was handed over to Shiro's SimpleAuthenticationInfo for processing
 * Therefore, we need to modify the code in doGetAuthenticationInfo;
 * )
 * The voucher matcher can be extended to realize the functions of locking after entering the number of password errors, and the next time
 */
@Bean(name = "credentialsMatcher")
public HashedCredentialsMatcher hashedCredentialsMatcher() {
	HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
	//Hash algorithm: MD5 algorithm is used here;
	hashedCredentialsMatcher.setHashAlgorithmName("md5");
	//The number of hashes, such as hashing twice, is equivalent to md5(md5(""));
	hashedCredentialsMatcher.setHashIterations(2);
	//storedCredentialsHexEncoded is true by default. In this case, the password is used, and the Hex code is used for encryption; Base64 encoding is used when false
	hashedCredentialsMatcher.setStoredCredentialsHexEncoded(true);
	return hashedCredentialsMatcher;
}

2) Set the voucher matcher at the injection CustomRealm. The modification code is as follows

/**
 * Inject custom permission verification object
 * Shiro Realm The user-defined Realm inherited from the authoringrealm, that is, the class that Shiro authenticates the user to log in is user-defined
 */
@Bean
public CustomerRealm userRealm() {
	CustomerRealm realm = new CustomerRealm();
	realm.setCredentialsMatcher(hashedCredentialsMatcher());
	return realm;
}

3) Modify the doGetAuthenticationInfo method of the CustomerRealm class

...

//Add salt=username+salt
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(username+"salt"), getName());
...

4) When registering a user or creating a password, use the following rules to create an encrypted password and store it in the database

// md5 + salt + hash hash times
Md5Hash md5Hash2 = new Md5Hash(password, username+"salt", 2);
return md5Hash2.toString();

reference resources: shiro uses Md5 encryption

6. session persistence and distributed session sharing

Save sessions to redis, and use the same redis for multi machine deployment to ensure that sessions are shared with each other; The system restarts and the user does not need to log in again
1)maven pom joins redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2)application.yml configuration

spring:
  redis:
    host: localhost #redis service PI
    port: 6379      #Server

Basic operation of Redis

@Autowired
private RedisTemplate<String, Object> redisTemplate;
//preservation
redisTemplate.opsForValue().set("key-1", "value-1"); 
//Preservation with expiration date
redisTemplate.opsForValue().set("key-1", "value-1", 120, TimeUnit.SECONDS);
//delete
redisTemplate.delete("key-1");

3) Create a class, inherit cacheingsessiondao, and customize the session persistence implementation
The four methods to Override are:
doCreate: when shiro creates a session, it saves the session to redis
doUpdate: the effective time to refresh the session when the user maintains the session
doDelete: deletes the session from redis when the user logs off or the session expires
doReadSession: shiro obtains the Session object from sessionId and redis

Create redissessiondao java

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.CachingSessionDAO;
import org.springframework.data.redis.core.RedisTemplate;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

/**
 * Custom session persistence implementation, Shiro extension for cluster sharing
 */
public class RedisSessionDAO extends CachingSessionDAO {
    //The prefix of the SessionID stored in Redis
    private static final String PREFIX = "SENTGON_SHOP_SHIRO_SESSION_ID";
    //Validity period (time unit will be increased in subsequent use, in seconds)
    private static final int EXPRIE = 86400; //1 day
    //Redis operation tool 
    private RedisTemplate<Serializable, Session> redisTemplate;

    //Constructor
    public RedisSessionDAO(RedisTemplate<Serializable, Session> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }


    /**
     * shiro When creating a session, save the session to redis
     * @param session
     * @return
     */
    @Override
    protected Serializable doCreate(Session session) {
        //Generate SessionID
        Serializable serializable = this.generateSessionId(session);
        assignSessionId(session, serializable);
        //Store the sessionid as the Key and the session as the value in redis
        redisTemplate.opsForValue().set(PREFIX+serializable, session);
        return serializable;
    }

    /**
     * When the user maintains the session, refresh the effective time of the session
     * @param session
     */
    @Override
    protected void doUpdate(Session session) {
        //Set session validity
        session.setTimeout(EXPRIE * 1000);
        //Store the sessionid as the Key and the session as the value in redis, and set the validity period
        redisTemplate.opsForValue().set(PREFIX+session.getId(), session, EXPRIE, TimeUnit.SECONDS);
    }

    /**
     * When the user logs off or the session expires, the session is deleted from redis
     * @param session
     */
    @Override
    protected void doDelete(Session session) {
        //null validation
        if (session == null) {
            return;
        }
        //Delete the k-v of the specified SessionId from Redis
        redisTemplate.delete(PREFIX+session.getId());
    }

    /**
     *  shiro Obtain the Session object through sessionId and redis
     * @param sessionId
     * @return
     */
    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        //Read Session object from Redis
        Session session = redisTemplate.opsForValue().get(PREFIX+sessionId);
        return session;
    }
}

4) Inject RedisSessionManager into SecurityManager

@Autowired
private RedisTemplate redisTemplate;

/**
 * Register RedisSessionDao in the container
 * @param redisTemplate
 * @return
 */
@Bean
public SessionDAO redisSessionDAO(RedisTemplate redisTemplate) {
	return new RedisSessionDAO(redisTemplate);
}

/**
 * Inject custom CustomRealm into SecurityManager
 * Shiro Manage internal component instances through SecurityManager, and provide various security management services through it
 * @return
 */
@Bean
public DefaultWebSecurityManager securityManager(CustomerRealm customRealm) {
	DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
	// Custom Realm
	securityManager.setRealm(customRealm);
	// Rewrite the session manager and inject custom SessionDao
	DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
	defaultWebSessionManager.setSessionDAO(redisSessionDAO(redisTemplate));
	securityManager.setSessionManager(defaultWebSessionManager);
	return securityManager;
}

So far, Shiro's cluster sharing Session has been completed

Keywords: Redis Spring Boot Distribution

Added by Getran on Sat, 11 Dec 2021 15:11:59 +0200