The most comprehensive microservice permission control scheme in history

1. Microservice permission design

Let me first talk about why I wrote this article, because the actual project needs, we need to control the permissions of each component of our current project page, and then check the commonly used permission frameworks on the Internet, one is shrio, the other is spring security, After reading the comparison, it is said that shrio is lighter and easier to use.

In this article, we also chose shrio as the authority framework for the entire project. At the same time, combined with some spring boot+shrio integration cases done by online tycoons, we can only say that everyone’s drawings are very good..., look at everyone’s function flow chart If you think about it carefully, then you won’t be able to move if you practice it yourself. There are all kinds of pitfalls. . .

Returning to the specific implementation is really a pitfall every step of the way. In the process of practice, I thought about the following solutions. Some of them either didn’t work before starting coding, and some of them found that they didn’t work after typing half of the code. In this project, I also refer to the RCBA authority design Model.

1. Put shrio and gateway in the same service, but this brings a problem. As we all know, the data center realm of shrio needs to use the data in the user service (query the relationship between users, roles, permissions and data ), so here shrio needs to use the service discovery component (the dubbo I use here) to discover user services, but the login in user services needs to use shrio authentication. Some people may have to say here, it can be in user services Then call the shrio service remotely. If this method is possible, you can use this method without looking down... So this causes the two services to be coupled together, and this method is directly pass ed.

2. Each service shares a shrio configuration module. This method also has problems, which are similar to the above problems. Now shrio is a separate module that needs to use user services. You can use dubbo to call remotely, and user services The shrio configuration module needs to be imported through maven, and now the user service is started, and an error will be reported: no service provider was found in the shrio configuration module. Therefore, this scheme can also be pass ed.

I believe that I am not the only one who has done the above two solutions. I can only say that shrio is still suitable for a single architecture... Of course, it does not mean that shrio cannot do microservice permission control. After a week of research and trials , I finally found out how to use shrio to design permissions for microservices. Let me talk about my plan below.

2. Design plan

Combining the above two unworkable methods, we learn from each other's strengths and make up for our weaknesses. The new solution is as follows.

Option One

Since the user service and the shrio module need to be separated but the two need to depend on each other, we can configure a shrio module specifically for the user service, and other services share a shrio module. Of course, the two shrio modules need to share the session session

3. Realization

The sample project is implemented using springboot+mysql+mybatis-plus, and the service discovery and registration tool uses dubbo+zookeeper (here I mainly want to learn the usage of these two components, and you can also use eureka+feign).

3.1 The structure of the project is as follows:

  • common module: The common module of the entire project, common-core contains some constant data, return values, and exceptions required by other microservices, and the common-cache module contains shrio cache configurations required by all microservices, except for user services and other services Required authorization module common-auth.

  • gateway-service service: gateway service, the entrance of all other services.

  • user-api: The data interface defined by the user service.

  • user-provider-service: The implementation of the user service interface, the provider of the user service.

  • user-consumer-service: The outermost layer of user service, the service called by nginx, the consumer of user service.

  • video-api: same as user service api.

  • video-provider: same as user service provider.

  • video-consumer: same as user service consumer.

3.2 The table relationship is as follows

3.3 Shared session session (cache module common-cache)

3.3.1 Why do we need to share session?

Because our project is composed of multiple microservices, when the user service receives the user's login request and the login is successful, we return a sessionId to the user and save it in the cookie of the user's browser, and the user then requests the user service at this time It will carry the sessionId in the cookie, and the server can retrieve the user information stored in the server according to the sessionId carried by the user.

But at this time, if the user requests the video service, the user information stored in the server cannot be retrieved, because the video service does not know whether you have logged in at all, so this requires us to share the user information that has successfully logged in, not just the user service to access.

3.3.2 How to implement shared session?

When we write the relevant configuration of shrio, we all know that we need to customize the security manager of shrio, that is, rewrite DefaultWebSecurityManager, let's take a look at which components will be initialized in the middle of instantiating this security manager class.

The first is the constructor of DefaultWebSecurityManager.

public DefaultWebSecurityManager() {
    super();
    ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
    this.sessionMode = HTTP_SESSION_MODE;
    setSubjectFactory(new DefaultWebSubjectFactory());
    setRememberMeManager(new CookieRememberMeManager());
    setSessionManager(new ServletContainerSessionManager());
}

Enter DefaultSecurityManager, the parent class of DefaultWebSecurityManager, and view the constructor of DefaultSecurityManager.

public DefaultSecurityManager() {
    super();
    this.subjectFactory = new DefaultSubjectFactory();
    this.subjectDAO = new DefaultSubjectDAO();
}

Enter SessionsSecurityManager, the parent class of DefaultSecurityManager, and view the constructor of SessionsSecurityManager.

public SessionsSecurityManager() {
    super();
    this.sessionManager = new DefaultSessionManager();
    applyCacheManagerToSessionManager();
}

In this constructor we see that a default session manager DefaultSessionManager is instantiated. Let's click in and have a look. You can see that the default in DefaultSessionManager is to use memory to save the session (MemorySessionDAO is the class that operates on the session).

public DefaultSessionManager() {
    this.deleteInvalidSessions = true;
    this.sessionFactory = new SimpleSessionFactory();
    this.sessionDAO = new MemorySessionDAO();
}

According to our analysis above, if you want to share the session in each microservice, you cannot put the session in the memory of the server where a certain microservice is located. You need to share the session separately, so we need to write a custom SessionDAO To override the default MemorySessionDAO, let's see how to implement a custom SessionDAO.

According to the sessionDAO relationship diagram above, we can know that AbstractSessionDAO mainly has two subclasses, one is EnterpriseCacheSessionDAO that has been implemented, and the other is MemorySessionDAO. Now we need to replace the default MemorySessionDAO, or we inherit AbstractSessionDAO to implement the method of reading and writing sessions. Or directly use the EnterpriseCacheSessionDAO that it has already implemented for us.

Here I choose to use the EnterpriseCacheSessionDAO class directly.

public EnterpriseCacheSessionDAO() {
    setCacheManager(new AbstractCacheManager() {
        @Override
        protected Cache<Serializable, Session> createCache(String name) throws CacheException {
            return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
        }
    });
}

However, in the construction method of the above class, we can find that it gives us a new AbstractCacheManager cache manager by default, and uses ConcurrentHashMap to save the session session, so if we want to use this EnterpriseCacheSessionDAO class to implement the cache operation, then we will Need to write a custom CacheManager to override its default CacheManager.

3.3.3 Implementation

  • First import the dependencies we need

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <!--import shrio relevant-->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>

</dependencies>
  • Write our own CacheManager

@Component("myCacheManager")
public class MyCacheManager implements CacheManager {

    @Override
    public <K, V> Cache<K, V> getCache(String s) throws CacheException {
        return new MyCache();
    }

}
  • Jedis client

RedisTemplate is not used here, because the query efficiency of RedisTemplate is far lower than that of Jedis client after actual testing and online data retrieval.

public class JedisClient {
    private static Logger logger = LoggerFactory.getLogger(JedisClient.class);

    protected static final ThreadLocal<Jedis> threadLocalJedis = new ThreadLocal<Jedis>();
    private static JedisPool jedisPool;
    private static final String HOST = "localhost";
    private static final int PORT = 6379;
    private static final String PASSWORD = "1234";
    //Control the maximum number of idle (idle) jedis instances in a pool, and the default value is also 8.
    private static int MAX_IDLE = 16;
    //The maximum number of available connection instances, the default value is 8;
    //If the assignment value is -1, it means no limit; if the pool has allocated maxActive jedis instances, the status of the pool is exhausted (exhausted).
    private static int MAX_ACTIVE = -1;
    //overtime time
    private static final int TIMEOUT = 1000 * 5;
    //The maximum time to wait for an available connection, in milliseconds, the default value is -1. Indicates no timeout
    private static int MAX_WAIT = 1000 * 5;

    //Connect to database (0-15)
    private static final int DATABASE = 2;

    static {
        initialPool();
    }

    public static JedisPool initialPool() {
        JedisPool jp = null;
        try {
            JedisPoolConfig config = new JedisPoolConfig();
            config.setMaxIdle(MAX_IDLE);
            config.setMaxTotal(MAX_ACTIVE);
            config.setMaxWaitMillis(MAX_WAIT);
            config.setTestOnCreate(true);
            config.setTestWhileIdle(true);
            config.setTestOnReturn(true);
            jp = new JedisPool(config, HOST, PORT, TIMEOUT, PASSWORD, DATABASE);
            jedisPool = jp;
            threadLocalJedis.set(getJedis());
        } catch (Exception e) {
            e.printStackTrace();
            logger.error("redis server exception", e);
        }
        return jp;
    }

    /**
     * Get jedis instance
     *
     * @return jedis
     */


    public static Jedis getJedis() {
        boolean success = false;
        Jedis jedis = null;
        int i = 0;
        while (!success) {
            i++;
            try {
                if (jedisPool != null) {
                    jedis = threadLocalJedis.get();
                    if (jedis == null) {
                        jedis = jedisPool.getResource();
                    } else {
                        if (!jedis.isConnected() && !jedis.getClient().isBroken()) {
                            threadLocalJedis.set(null);
                            jedis = jedisPool.getResource();
                        }
                        return jedis;
                    }

                } else {
                    throw new RuntimeException("redis Connection pool initialization failed");
                }
            } catch (Exception e) {
                logger.error(Thread.currentThread().getName() + "No." + i + "fetch failed");
                success = false;
                e.printStackTrace();
                logger.error("redis server exception", e);
            }
            if (jedis != null) {
                success = true;
            }
            if (i >= 10 && i < 20) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (i >= 20 && i < 30) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

            if (i >= 30 && i < 40) {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (i >= 40) {
                System.out.println("redis I can't connect at all~~~~(>_<)~~~~");
                return null;
            }

        }
        if (threadLocalJedis.get() == null) {
            threadLocalJedis.set(jedis);
        }
        return jedis;
    }

    /**
     * set key-value
     *
     * @param key
     * @param value
     */

    public static void setValue(byte[] key, byte[] value) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            jedis.set(key, value);

        } catch (Exception e) {
            threadLocalJedis.set(null);
            logger.error("redis server exception", e);
            throw new RuntimeException("redis server exception");
        } finally {
            if (jedis != null) {
                close(jedis);
            }
        }
    }

    /**
     * Set key-value, expiration time
     *
     * @param key
     * @param value
     * @param seconds
     */
    public static void setValue(byte[] key, byte[] value, int seconds) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            jedis.setex(key, seconds, value);
        } catch (Exception e) {
            threadLocalJedis.set(null);
            logger.error("redis server exception", e);
            throw new RuntimeException("redis server exception");
        } finally {
            if (jedis != null) {
                close(jedis);
            }
        }
    }

    public static byte[] getValue(byte[] key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            if (jedis == null || !jedis.exists(key)) {
                return null;
            }
            return jedis.get(key);
        } catch (Exception e) {
            threadLocalJedis.set(null);
            logger.error("redis server exception", e);
            throw new RuntimeException("redis server exception");
        } finally {
            if (jedis != null) {
                close(jedis);
            }
        }
    }

    public static long delkey(byte[] key) {
        Jedis jedis = null;
        try {
            jedis = getJedis();
            if (jedis == null || !jedis.exists(key)) {
                return 0;
            }
            return jedis.del(key);
        } catch (Exception e) {
            threadLocalJedis.set(null);
            logger.error("redis server exception", e);
            throw new RuntimeException("redis server exception");
        } finally {
            if (jedis != null) {
                close(jedis);
            }
        }
    }


    public static void close(Jedis jedis) {
        if (threadLocalJedis.get() == null && jedis != null) {
            jedis.close();
        }
    }

    public static void clear() {
        if (threadLocalJedis.get() == null) {
            return;
        }
        Set<String> keys = threadLocalJedis.get().keys("*");
        keys.forEach(key -> delkey(key.getBytes()));
    }

}
  • Customize our own Cache implementation class

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.session.mgt.SimpleSession;

import java.io.*;
import java.time.Duration;
import java.util.Collection;
import java.util.Set;


public class MyCache<S, V> implements Cache<Object, Object> {


    //Set the expiration time of the cache (30 minutes)
    private Duration cacheExpireTime = Duration.ofMinutes(30);

    /**
     * Get the value value according to the corresponding key
     *
     * @param s
     * @return
     * @throws CacheException
     */
    @Override
    public Object get(Object s) throws CacheException {
        System.out.println("get()method....");
        byte[] bytes = JedisClient.getValue(objectToBytes(s));
        return bytes == null ? null : (SimpleSession) bytesToObject(bytes);
    }

    /**
     * Save K-V to redis
     * Note: the saved value is of string type
     *
     * @param s
     * @param o
     * @return
     * @throws CacheException
     */

    @Override
    public Object put(Object s, Object o) throws CacheException {
        JedisClient.setValue(objectToBytes(s), objectToBytes(o), (int) cacheExpireTime.getSeconds());
        return s;
    }


    public byte[] objectToBytes(Object object) {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] bytes = null;
        try {
            ObjectOutputStream op = new ObjectOutputStream(outputStream);
            op.writeObject(object);
            bytes = outputStream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bytes;
    }

    public Object bytesToObject(byte[] bytes) {
        ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        Object object = null;
        try {
            ObjectInputStream ois = new ObjectInputStream(inputStream);
            object = ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return object;
    }

    /**
     * Delete cache, according to key
     *
     * @param s
     * @return
     * @throws CacheException
     */
    @Override
    public Object remove(Object s) throws CacheException {
        return JedisClient.delkey(objectToBytes(s));
    }

    /**
     * clear all caches
     *
     * @throws CacheException
     */

    @Override
    public void clear() throws CacheException {
        JedisClient.clear();
    }

    /**
     * number of caches
     *
     * @return
     */
    @Override
    public int size() {
        return JedisClient.getJedis().dbSize().intValue();
//        return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();
    }

    @Override
    public Set keys() {
        return JedisClient.getJedis().keys("*");
    }

    @Override
    public Collection values() {
        return null;
    }

}

Note that the objectToBytes and bytesToObject methods above convert the session into a byte array and then store it in redis. Taking it out from redis also converts the byte array into a session object, otherwise an error will be reported. This is because shrio uses the simpleSession class of its own package, and the fields in this class are transient and cannot be serialized directly. We need to convert each object into a byte array before we can operate it.

Of course, if we use RedisTemplate, we don’t need to write these two methods when configuring, and just use the default JDK serialization method.

private transient Serializable id;
private transient Date startTimestamp;
private transient Date stopTimestamp;
private transient Date lastAccessTime;
private transient long timeout;
private transient boolean expired;
private transient String host;
private transient Map<Object, Object> attributes;

Because the cache module here is an independent module that needs to be used by other microservices, so if other microservices can automatically configure our custom cache manager CacheManager component, we also need to create a new folder under the resources folder META- INF, and create a new spring.factories file under the META-INF folder. The content in spring.factories is as follows:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.qzwang.common.cache.config.MyCacheManager

3.4 Authorization module common-auth

  • First import the dependencies we need

 <dependencies>
    <dependency>
        <groupId>com.qzwang</groupId>
        <artifactId>user-dubbo-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!--dubbo-->
    <dependency>
        <groupId>com.gitee.reger</groupId>
        <artifactId>spring-boot-starter-dubbo</artifactId>
        <version>1.1.3</version>
    </dependency>
    <!--Join the shared session cache module-->
    <dependency>
        <groupId>com.qzwang</groupId>
        <artifactId>common-cache</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>
  • Customize realm to verify user access rights

Note that only permission verification is implemented here, and user authentication is not implemented, so the user authentication doGetAuthenticationInfo method returns null directly.

import com.alibaba.dubbo.config.annotation.Reference;
import com.qzwang.user.api.service.UserService;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

public class UserRealm extends AuthorizingRealm {

    @Reference(version = "0.0.1")
    private UserService userService;
    //authorized
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //get username
        String userName = (String) principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
        System.out.println("username=" + userName);
        //Set roles for users
        authenticationInfo.setRoles(userService.selectRolesByUsername(userName));
        //Set permissions for users
        authenticationInfo.setStringPermissions(userService.selectPermissionByUsername(userName));

        return authenticationInfo;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        return null;
    }
}
  • Shrio's configuration center, some core configurations of shrio, including shrio's security manager and filters are all set in this class.

import com.qzwang.common.cache.config.MyCacheManager;
import com.qzwang.common.cache.config.MySessionDao;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {

    // ShiroFilterFactoryBean
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();

        //intercept
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        //shiroFilterFactoryBean.setLoginUrl("/user/index");
        // Set up a security manager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        return shiroFilterFactoryBean;
    }

    // DefaultWebSecurityManager
    // @Qualifier It can be directly the method name of the bean, or you can set a name for the bean, such as @Bean(name="myRealm"), and you can get the bean by name in @Qulifier
    @Bean(name = "SecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm,
                                                                  @Qualifier("myDefaultWebSessionManager") DefaultWebSessionManager defaultWebSessionManager,
                                                                  @Qualifier("myCacheManager") MyCacheManager myCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // Associate UserRealm
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(defaultWebSessionManager);
        securityManager.setCacheManager(myCacheManager);
        return securityManager;
    }

    // To create a Realm object, a custom class is required
    @Bean
    public UserRealm userRealm() {
        return new UserRealm();
    }


    /**
     * The following DefaultAdvisorAutoProxyCreator and AuthorizationAttributeSourceAdvisor must be defined,
     * Otherwise @RequiresRoles and @RequiresPermissions cannot be used
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }


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

    /**
     * Set up a custom session manager
     */
    @Bean
    public DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionIdCookie(simpleCookie);
        defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        return defaultWebSessionManager;
    }
    @Bean
    public SimpleCookie simpleCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("myCookie");
        simpleCookie.setPath("/");
        simpleCookie.setMaxAge(30);
        return simpleCookie;
    }
}

3.5 User consumer service user-consumer

First import the dependencies we need.

<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.qzwang</groupId>
        <artifactId>user-dubbo-api</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    <!-- dubbo+zookeeper+zkclient -->
    <dependency>
        <groupId>com.gitee.reger</groupId>
        <artifactId>spring-boot-starter-dubbo</artifactId>
        <version>1.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.6.2</version>
    </dependency>
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.11</version>
    </dependency>
    
    <!--import cache management-->
    <dependency>
        <groupId>com.qzwang</groupId>
        <artifactId>common-cache</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

</dependencies>

The cache of this service uses the cache of the public module (common-cache), and the shrio configuration needs to use our own configuration. Here, we need to implement authentication and authorization in realm.

import com.alibaba.dubbo.config.annotation.Reference;
import com.qzwang.user.api.model.User;
import com.qzwang.user.api.service.UserService;
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 org.springframework.stereotype.Component;

@Component
public class UserRealm extends AuthorizingRealm {

    @Reference(version = "0.0.1")
    private UserService userService;
    //authorized
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        //get username
        String userName = (String) principalCollection.getPrimaryPrincipal();
        System.out.println("userName=" + userName);
        SimpleAuthorizationInfo authenticationInfo = new SimpleAuthorizationInfo();
        //Set roles for users
        authenticationInfo.setRoles(userService.selectRolesByUsername(userName));
        //Set permissions for users
        authenticationInfo.setStringPermissions(userService.selectPermissionByUsername(userName));
        return authenticationInfo;
    }


    //certified
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String userName = (String) authenticationToken.getPrincipal();
        User user = userService.selectByUsername(userName);
        if (user != null) {
            AuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");
            return authenticationInfo;
        }
        return null;
    }
}

Related configuration of shrio

import com.qzwang.common.cache.config.MyCacheManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfig {


    // ShiroFilterFactoryBean
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //Set up a security manager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        //Add shiro's built-in filter
        /*
           anon: Access without authentication
           authc: Must be authenticated to access
           UserController: Must have remember me feature to access
           perms: Have a resource permission to access
           role: Have a role permission to access
         */
        //intercept
        Map<String, String> filterMap = new LinkedHashMap<>();

        //authorized
        // filterMap.put("/UserController/add", "perms[UserController:add]");
        filterMap.put("/user/testFunc", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);

        //set unauthorized page
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/unAuth");
        //set login request
        // shiroFilterFactoryBean.setLoginUrl("/user/index");

        return shiroFilterFactoryBean;
    }

    // DefaultWebSecurityManager
    //@Qualifier can directly be the method name of the bean, or you can set a name for the bean, such as @Bean(name="myRealm"), and you can get the bean by name in @Qulifier
    @Bean(name = "SecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm,
                                                                  @Qualifier("myDefaultWebSessionManager") DefaultWebSessionManager defaultWebSessionManager,
                                                                  @Qualifier("myCacheManager") MyCacheManager myCacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //Associate UserRealm
        securityManager.setRealm(userRealm);
        securityManager.setCacheManager(myCacheManager);
        securityManager.setSessionManager(defaultWebSessionManager);

        return securityManager;
    }
    /**
     * The following DefaultAdvisorAutoProxyCreator and AuthorizationAttributeSourceAdvisor must be defined,
     * Otherwise @RequiresRoles and @RequiresPermissions cannot be used
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }


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

    /**
     * Set up a custom session manager
     */
    @Bean
    public DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());
        defaultWebSessionManager.setSessionIdCookie(simpleCookie);
        return defaultWebSessionManager;
    }

    @Bean
    public SimpleCookie simpleCookie() {
        SimpleCookie simpleCookie = new SimpleCookie("myCookie");
        simpleCookie.setPath("/");
        simpleCookie.setMaxAge(30);
        return simpleCookie;
    }

}

Configure User Unauthenticated Exception Interception

import com.qzwang.common.core.config.ExceptionConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.Properties;

@Configuration
public class AuthorizationExceptionConfig {
    Logger logger = LoggerFactory.getLogger(ExceptionConfig.class);

    /**
     * Catch unauthenticated methods
     *
     * @return
     */
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        Properties properties = new Properties();
        properties.setProperty("org.apache.shiro.authz.AuthorizationException", "/user/unAuth");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }
}

The user login interface is as follows:

@RestController
@RequestMapping("/user")
public class UserController {
    @Reference(version = "0.0.1")
    private UserService userService;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    public R login(@RequestBody User user) {
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return R.ok();
        } catch (Exception e) {
            e.printStackTrace();
            return R.failed();
        }
    }
    
  @RequestMapping(value = "/unAuth", method = RequestMethod.GET)
    public R unAuth() {
        return R.failed("The user is not authorized!");
    }

 @RequiresRoles("admin")
    @RequestMapping(value = "/testFunc", method = RequestMethod.GET)
    public R testFunc() {
        return R.ok("yes success!!!");
    }
}

1. The user logs in first

2. Access the /user/testFunc interface. Note that this interface requires the admin role, but the zhangsan user in the database does not have this role, so there is no permission to access this interface.

3. Now add an admin role to zhangsan in the database, and then test.

3.6 Video consumer service video-consumer

For this service, I mainly test whether it is possible to share session sessions and implement permission control.

First import the required modules

<dependencies>
    <dependency>
        <groupId>com.qzwang</groupId>
        <artifactId>common-auth</artifactId>
        <version>0.0.1</version>
    </dependency>

    <!-- dubbo+zookeeper+zkclient -->
    <dependency>
        <groupId>com.gitee.reger</groupId>
        <artifactId>spring-boot-starter-dubbo</artifactId>
        <version>1.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.6.2</version>
    </dependency>
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.11</version>
    </dependency>
</dependencies>

Write an interface test below, pay attention. Because what we import here is the public authorization common-auth module, and each interface configured in this module requires authentication to access. Let's first test the access to the interface without logging in.

@RestController
@RequestMapping("/video")
public class VideoController {
    @RequestMapping("/getVideo")
    public R getVideo() {
        return R.ok();
    }
}

You can see that it jumps to the default login page of shrio. Next, we will test to access the interface after the login is successful.

It can be seen that the user's session information is shared. Let's try to add permissions to the interface.

@RestController
@RequestMapping("/video")
public class VideoController {
    @RequestMapping("/getVideo")
    @RequiresRoles("admin")
    public R getVideo() {
        return R.ok();
    }
}

This interface cannot be accessed without zhangsan permission.

Since the unauthorized interface /user/unAuth configured above is in the user service, it prompts that the interface cannot be found. Here we need to configure a gateway for these microservices (we will not expand how to configure it here, this is not the focus of this article) . The above test for accessing the interface when the user has the admin role is as follows.

Therefore, after testing the public module common-Auth, the redis sharing of user sessions and permission realm data is realized, which is perfect! ! !

Tags: Java Dubbo Microservices

Posted by Travis Estill on Sun, 01 Jan 2023 10:22:23 +0300