Springboot shiro implements single sign-on SSO

          By default, shiro uses session to store login information. This is not a problem for a single application, but it does not work for distributed applications or cluster applications, because clusters or distributed system applications are deployed on different JVMs. The session cannot be shared. If you use redis to store login information, you can solve this problem. Here, simply use the shiro-redis framework to achieve this function

1. The process is as follows:

1.1. First create a parent project shiroredisso

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

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.shiroredis</groupId>
    <artifactId>shiro-redis-sso</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>shiro-redis-sso</name>
    <packaging>pom</packaging>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <modules>
        <module>user</module>
        <module>other</module>
    </modules>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.0</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
             <version>5.1.43</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.29</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-all</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.2.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>



</project>

1.2. Two sub-projects other, user

<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.shiroredis</groupId>
        <artifactId>shiro-redis-sso</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.shiroredis1</groupId>
    <artifactId>other</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>other</name>


</project>
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.shiroredis</groupId>
        <artifactId>shiro-redis-sso</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>
    <groupId>com.shiroredis</groupId>
    <artifactId>user</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user</name>


</project>

2. Create an entity class in the user module 

package com.shiroredis.entity;

import lombok.Data;

import java.io.Serializable;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 16:19
 **/
@Data
public class User implements Serializable {
    private Long id;
    private String username;
    private String password;

2.1. UserMapper

package com.shiroredis.dao;

import com.shiroredis.entity.User;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 16:45
 **/
public interface UserMapper {
    User selectUserByUsernameAndPassword(String username, String password);
}

2.2.UserMapper.xml file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shiroredis.dao.UserMapper">
    <select id="selectUserByUsernameAndPassword" resultType="com.shiroredis.entity.User">
      select * from user where username = #{username} and password = #{password}
    </select>

</mapper>

  3.User module configuration file

server.port=8080

#mysql
spring.datasource.url=jdbc:mysql://localhost:3306/ease-run?serverTimezone=Asia/Chongqing&useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&useSSL=false&verifyServerCertificate=false&autoReconnct=true&autoReconnectForPools=true&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#mybatis-plus
#Mybatis扫描
mybatis.mapper-locations=classpath*:mapper/*.xml
#起别名。可省略写mybatis的xml中的resultType的全路径
mybatis.type-aliases-package=com.shiroredis.entity
#druid配置
# 初始化大小,最小,最大
spring.datasource.initialSize=5
spring.datasource.minIdle=5
spring.datasource.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.maxWait=60000 
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.minEvictableIdleTimeMillis=300000
# 校验SQL,Oracle配置 spring.datasource.validationQuery=SELECT 1 FROM DUAL,如果不配validationQuery项,则下面三项配置无用
spring.datasource.validationQuery=SELECT 'x'
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
# 打开PSCache,并且指定每个连接上PSCache的大小
spring.datasource.poolPreparedStatements=true
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
spring.datasource.filters=stat,wall,logback
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
# 合并多个DruidDataSource的监控数据
spring.datasource.useGlobalDataSourceStat=true
spring.redis.host=localhost:6379
#log
logging.path=./logs
logging.file=Log   
logging.config=classpath:logback-spring-dev.xml

4.自定义UserRealm.java

package com.shiroredis.realm;

import com.shiroredis.dao.UserMapper;
import com.shiroredis.entity.User;
import org.apache.shiro.authc.*;
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.beans.factory.annotation.Autowired;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 16:51
 *  自定义realm
 **/
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserMapper userMapper;

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // TODO Auto-generated method stub
        System.out.println("认证");
        //shiro判断逻辑
        UsernamePasswordToken user = (UsernamePasswordToken) authenticationToken;
        User newUser = userMapper.selectUserByUsernameAndPassword(user.getUsername(),String.valueOf(user.getPassword()));
        if(newUser == null){
            //用户名错误
            return null;
        }
        return new SimpleAuthenticationInfo (newUser,newUser.getPassword(),"");
    }
}

Core shiroConfig class

Here replace the default shiro sessionmanager and cachemanager with crazycake based on redis-based sessionmanager and cachemanager, you can use redis to manage login information. One thing to note is shiroconfig. If you want to use @Value annotation to read configuration data, you need to change

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

4.1.ShiroConfig.java

package com.shiroredis.ShiroConfig;

import com.shiroredis.realm.UserRealm;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
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.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 16:56
 **/
@Configuration
public class ShiroConfig {
    @Value("${spring.redis.host}")
    String host;
    //    @Value("${spring.redis.port}")
//    int port;
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager (); // crazycake 实现
        redisManager.setHost (host);
//      redisManager.setPort(port);
        redisManager.setTimeout (180000);
        return redisManager;
    }
    /**
     * 自定义生成会话ID的方法,具体的类需要实现SessionIdGenerator接口
     * shiro的SessionDAO实现使用SessionIdGenerator接口自动的生成会话session ID;
     * SessionIdGenerator的具体实现类是JavaUuidSessionIdGenerator,生成会话ID的方法如下:
     * public Serializable generateId(Session session) {
     * return UUID.randomUUID().toString();
     * }
     *会话ID生成器
     * @return
     */
    @Bean
    public JavaUuidSessionIdGenerator sessionIdGenerator() {
        return new JavaUuidSessionIdGenerator ();
    }
    /**
     * 会话DAO
     * @return
     */
    @Bean
    public RedisSessionDAO sessionDAO() {
        RedisSessionDAO sessionDAO = new RedisSessionDAO (); // crazycake 实现
        sessionDAO.setRedisManager (redisManager ());
        sessionDAO.setSessionIdGenerator (sessionIdGenerator ()); //  Session ID 生成器
        return sessionDAO;
    }
    /**
     * 会话Cookie模板
     * @return
     */
    @Bean
    public SimpleCookie cookie(){
        SimpleCookie cookie = new SimpleCookie("SHAREJSESSIONID"); //  cookie的name,对应的默认是 JSESSIONID
        cookie.setHttpOnly(true);
        cookie.setPath("/");        //  path为 / 用于多个系统共享JSESSIONID
        return cookie;
    }

    /**
     * 会话管理器
     * @return
     */
    @Bean
    public DefaultWebSessionManager sessionManager(){
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(-1000L);    // 设置session超时
        sessionManager.setDeleteInvalidSessions(true);      // 删除无效session
        sessionManager.setSessionIdCookie(cookie());            // 设置JSESSIONID
        sessionManager.setSessionDAO(sessionDAO());         // 设置sessionDAO
        return sessionManager;
    }
    /**
     * 1. 配置SecurityManager
     * securityManager安全管理器
     * Shiro的核心安全接口,这个属性是必须的
     * @return
     */
    @Bean
    public DefaultWebSecurityManager securityManager(){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm());  // 设置realm
        securityManager.setSessionManager(sessionManager());    // 设置sessionManager
        securityManager.setCacheManager(redisCacheManager()); // 配置缓存的话,退出登录的时候crazycake会报错,要求放在session里面的实体类必须有个id标识
        return securityManager;
    }
    /**
     * 2. 配置缓存
     * @return
     */
//    @Bean
//    public CacheManager cacheManager(){
//        EhCacheManager ehCacheManager = new EhCacheManager();
//        ehCacheManager.setCacheManagerConfigFile("classpath:ehcache.xml");
//        return ehCacheManager;
//    }
    @Bean
   public CacheManager redisCacheManager() {
       RedisCacheManager cacheManager = new RedisCacheManager();   // crazycake 实现
       cacheManager.setRedisManager(redisManager());
       return cacheManager;

   }
    /**
     * 3. 配置Realm
     * @return
     */
    @Bean
   public AuthorizingRealm  realm() {
        return new UserRealm ();
    }
    /**
     * 4. 配置LifecycleBeanPostProcessor,可以来自动的调用配置在Spring IOC容器中 Shiro Bean 的生命周期方法
     * @return
     */
    @Bean
    public static LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    /**
     * 5. 启用IOC容器中使用Shiro的注解,但是必须配置第四步才可以使用
     * @return
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator(){
        return new DefaultAdvisorAutoProxyCreator();
    }
    /**
     * 6. 配置ShiroFilter
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(){
        LinkedHashMap<String, String> map = new LinkedHashMap<>();

        map.put("/user/login", "anon");
        map.put("/user/logout", "anon");

        // everything else requires authentication:
        map.put("/**", "authc");

        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 配置SecurityManager
        factoryBean.setSecurityManager(securityManager());
        // 配置权限路径
        factoryBean.setFilterChainDefinitionMap(map);

        // 配置登录url
        factoryBean.setLoginUrl("/");
        // 配置无权限路径
        factoryBean.setUnauthorizedUrl("/unauthorized");
        return factoryBean;
    }


}

4.2.UserController

package com.shiroredis.controller;

import com.shiroredis.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 18:14
 **/
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/login")
    public User login(@RequestParam(value = "username") String username,
                      @RequestParam(value = "password") String password){
        Subject subject = SecurityUtils.getSubject();
        subject.login(new UsernamePasswordToken (username, password));
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        return user;
    }

    @RequestMapping("/logout")
    public Boolean logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return true;
    }

    @RequestMapping("/get")
    public User get(){
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        return user;
    }

}

5.User project effect

5.1. redis data

The cookie data can be found to be the same as the redis key

6. The other project module, here is relatively simple

@SpringBootApplication annotation is replaced by

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class})

Cancel the automatic injection of the data source, because there is no need to read user data from the database

6.1.1.other project configuration file

server.port=8082
spring.redis.host=localhost:6379

6.2.1.other project user entity class is the same as user project entity class

6.2.2. UserRealm should be careful to delete the content in the authentication method, because other modules do not need to log in, just get the logged-in user information

package com.shiroredis.realm;

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;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 19:54
 * 自定义realm
 **/
public class UserRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("权限配置-->MyShiroRealm.doGetAuthorizationInfo()");
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        return authorizationInfo;

    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        return null;
    }
}

Just copy it directly in the shiroconfig user project

Controller does not need to provide login, logout method

package com.shiroredis.controller;

import com.shiroredis.entity.User;
import org.apache.shiro.SecurityUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: wangxiaobo
 * @create: 2020-08-27 19:56
 **/
@RestController
@RequestMapping("/user")
public class UserController {
    @RequestMapping("/get")
    public User get(){
        User user = (User) SecurityUtils.getSubject().getPrincipal();
        return user;
    }

}

running result

There is one place to note, because shiro-redis uses ThreadLocal, it may cause memory overflow in high concurrency scenarios. The solution is to disable ThreadLocal and upgrade shiro-redis version to 3.2.3

<dependency>
     <groupId>org.crazycake</groupId>
     <artifactId>shiro-redis</artifactId>
     <version>3.2.3</version>
</dependency>

shiroConfig adds    sessionDAO.setSessionInMemoryEnabled(false); just   disable ThreadLocal

@Bean
public RedisSessionDAO sessionDAO(){
        RedisSessionDAO sessionDAO = new RedisSessionDAO(); // crazycake 实现
        sessionDAO.setSessionInMemoryEnabled(false);
        sessionDAO.setRedisManager(redisManager());
        sessionDAO.setSessionIdGenerator(sessionIdGenerator()); //  Session ID 生成器
        return sessionDAO;
}

 

Guess you like

Origin blog.csdn.net/qq_34709784/article/details/108268774