springmvc整合 shiro+ redis实现权限控制

springmvc 是现在主流的mvc框架,shiro是一款轻量级安全框架。与spring security不同,shiro的配置简单,更加容易上手。所以这次采用了springmvc + shiro的组合,来实现简单的权限管理。废话不多说,首先上代码。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itsu</groupId>
    <artifactId>shiro</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <name>shiro Maven Webapp</name>
    <!-- FIXME change it to the project's website -->
    <url>http://www.example.com</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-tx</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.0</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!--        <dependency>-->
        <!--            <groupId>org.apache.shiro</groupId>-->
        <!--            <artifactId>shiro-all</artifactId>-->
        <!--            <version>1.3.2</version>-->
        <!--        </dependency>-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ehcache</groupId>
            <artifactId>ehcache-core</artifactId>
            <version>2.6.11</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context-support</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.28</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.25</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-core</artifactId>
            <version>1.2.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.14</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.16.20</version>
        </dependency>

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-annotations</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.0.8.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.yugabyte</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0-yb-11</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.5.RELEASE</version>
        </dependency>


        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.56</version>
        </dependency>

        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
            <version>2.9.8</version>
        </dependency>
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-cache-support</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>shiro</finalName>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
            <plugins>
                <plugin>
                    <artifactId>maven-clean-plugin</artifactId>
                    <version>3.1.0</version>
                </plugin>
                <!-- see http://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_war_packaging -->
                <plugin>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>3.0.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.8.0</version>
                </plugin>
                <plugin>
                    <artifactId>maven-surefire-plugin</artifactId>
                    <version>2.22.1</version>
                </plugin>
                <plugin>
                    <artifactId>maven-war-plugin</artifactId>
                    <version>3.2.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-install-plugin</artifactId>
                    <version>2.5.2</version>
                </plugin>
                <plugin>
                    <artifactId>maven-deploy-plugin</artifactId>
                    <version>2.8.2</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

在引入了相关jar包后开始搭建spring mvc + shiro环境

1.修改web.xml 加入shiro 过滤器以及引入spring & springmvc配置文件,并进行基本的springmvc环境。关于springmvc的配置网上应该有很多,这里就不再赘述了,在博文最后我会贴上相关配置。

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
         id="WebApp_ID" version="3.1">

    <filter>
        <filter-name>shiroFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetFilterLifecycle</param-name>
            <param-value>true</param-value>
        </init-param>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>shiroFilter</param-value>
        </init-param>
    </filter>

    <filter-mapping>
        <filter-name>shiroFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


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

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:applicationContext-*.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <load-on-startup>0</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <filter>
        <filter-name>characterFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

</web-app>

2. 将ShiroFilterFactoryBean交给spring进行管理。其中shiro内置了几个过滤器,其中 anon 表示无需认证即可访问。authc 表示需要认证才能访问;user 表示设置了"记住我“后可以访问的url;logout表示推出系统的过滤器。

<bean class="org.apache.shiro.spring.web.ShiroFilterFactoryBean" id="shiroFilter">
    <property name="securityManager" ref="securityManager"/>
    <property name="loginUrl" value="/login"/>
    <property name="unauthorizedUrl" value="/error/noAuth"/>
    <property name="filterChainDefinitions">
        <value>
            /logout = logout
            /static/** = anon
            /login.do = anon
            /login = anon
            / = user
            /** = authc
        </value>
    </property>
</bean>

也可以根据自己的需要去自定义过滤器。然后可以通过 filters属性进行spirng注入。如:

<property name="filters">
    <util:map>
        <entry key="sessionFilter" value-ref="sessionfilter"></entry>
    </util:map>
</property>
<bean class="com.itsu.app.shrio.filter.Sessionfilter" id="sessionfilter"/>

3.配置自定义realm。 需要继承AuthorizingRealm,并重写它的两个方法。其中doGetAuthenticationInfo方法是用来验证登陆的。而doGetAuthorizationInfo是用来判断授权信息的。在做认证的时候需要从访问数据库得到用户的账号和密码。

public class MyRealm extends AuthorizingRealm {

    @Resource
    private UserMapper userMapper;

    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        Set<String> roles = getRolesByUsername(userName);
        authorizationInfo.setRoles(roles);
        return authorizationInfo;
    }

    /*模拟获取角色,实际情况应该从数据库获取*/
    private Set<String> getRolesByUsername(String userName) {
        Set<String> roles = new HashSet<>();
        roles.add("admin");
        roles.add("user");
        return roles;

    }

    //    认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName = (String) token.getPrincipal();
        User user = getUserByUserName(userName);
        if (user == null) {
            return null;
        }
        int status = user.getStatus();
//        int status = 0;
        if (status != 0) {
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(userName, user.getPassword(), getName());
        /*设置密码加盐*/
        authenticationInfo.setCredentialsSalt(ByteSourceUtil.bytes(userName));
        return authenticationInfo;
    }

    public User getUserByUserName(String userName) {
        System.out.println("从数据库中获取数据");
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_name", userName);
        User user = userMapper.selectOne(queryWrapper);
        return user;
    }

  
}

4.创建webSecurityManager并交给spring进行管理。并将自定义realm注入到DefaultWebSecurityManager中。这里需要注意,HashedCredentialsMatcher这个类是shiro提供的用于密码加密的类。我们可以自定义加密的方式,和加密算法迭代的次数。hashAlgorithmName这个属性表示是hash散裂算法的名称,我用的是MD5,hashIterations表示迭代次数,我设置为1表述密码只需要进行一次MD5加密。需要注意的是,在用来测试时,在数据库中存放的密码是需要先经过MD5加密后的。也就是说,shiro只会比较加密后的密码,和用户所输入的密码是否匹配。

 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"/>
    </bean>

<!--自定义realm实现-->
    <bean id="myRealm" class="com.itsu.app.shrio.realm.MyRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
        <property name="name" value="MyRealm"></property>
    </bean>

<!--配置密码MD5加密-->
    <bean class="org.apache.shiro.authc.credential.HashedCredentialsMatcher" id="credentialsMatcher">
        <property name="hashAlgorithmName" value="MD5"/>
        <property name="hashIterations" value="1"/>
    </bean>

到此为止,一个最基本的spring mvc + shiro环境就已经搭建完成了。我们来写一个登陆功能来看看效果。

<div align="center" style="text-align: center">
    <form id="form">
        <div>
            <label for="userName">用戶名</label>
            <input type="text" id="userName" name="userName">
        </div>
        <div>
            <label for="password">密码</label>
            <input type="password" id="password" name="password">
        </div>
        <div>
            <input type="submit" value="提交">
            <input type="reset" value="重置">
        </div>
    </form>
</div>
</body>
<script type="text/javascript" src="/static/js/jquery-2.1.4.min.js"></script>
<script type="text/javascript">
    $(function () {
        $("#form").submit(function () {
            var userName = $("#userName").val();
            var password = $("#password").val();
            var rememberMe = $("#rememberMe").is(":checked");
            // alert(rememberMe);
            // return false;
            var formData = {userName: userName, password: password, rememberMe: rememberMe};
            submitData(formData);
            return false;
        })

        function submitData(formData) {
            $.ajax({
                url: "/login.do",
                type: "post",
                dataType: "json",
                data: formData,
                success: function (data) {
                    if (data.code == '200') {
                        alert("success " + data.msg);
                    } else {
                        alert("error: " + data.msg);
                    }
                },
                error: function (XMLHttpRequest, textStatus, errorThrown) {
                    console.log(XMLHttpRequest.responseText);
                    console.log(textStatus);
                }
            })
        }
    })
</script>

后台controller代码

 @PostMapping("/login.do")
    @ResponseBody
    public Map login(User user) {
        Map map = new HashMap();
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), user.getPassword());
        subject.login(token);

        if (subject.hasRole("admin")) {
            System.out.println("有admin权限");
       }

        map.put("code", "200");
        map.put("msg", "登录成功");
        return map;
    }

访问http://localhost:8080,会自动跳转到login.html,这是因为配置了<property name="loginUrl" value="/login"/>。当shiro发现你没有进行登陆验证的时候,会主动跳转到你配置的登陆url 也就是  /login。然后我们输入账号和密码看看会发生什么。

输入自己预先设置好的账号和密码,点击登陆。我们看到这里已经登陆成功了。

查看控制台,可以看到控制台打印出来了当前用户拥有admin权限。

5. 通过注解的形式配置(角色、权限)管理

首先需要引入两个类让spring进行管理。 这里有一个坑,这两个bean对象需要在spring-mvc.xml配置文件中进行配置。放在applicationContext xml中无效。 另外shiro底层是通过spring-aop技术实现的注解形式的权限配置。所以,我们在需要在spring-mvc.xml中引入aop的配置。

 <!--配置通过注解控制可访问的URL-->
    <bean class="org.apache.shiro.spring.LifecycleBeanPostProcessor" id="lifecycleBeanPostProcessor"></bean>

    <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"
          id="attributeSourceAdvisor">
        <property name="securityManager" ref="securityManager"/>
    </bean>

<!--开启aop代理-->
    <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

在controller中写两个方法来测试。 @RequriesRoles(value={需要的角色})。对于权限的设置可以使用@RequiresPermissions()注解进行配置,连个注解的使用是一样的,所以省略了@RequiresPermissions()

 @RequiresRoles(value = {"admin"})
    @GetMapping("/testRole")
    @ResponseBody
    public String testRole() {
        return "admin";
    }

    @ResponseBody
    @RequiresRoles(value = {"admin1"})
    @GetMapping(value = "/testRole1")
    public String testRole1() {
        return "admin1角色";
    }

在浏览器中输入http://localhost:8080/testRole ,发现访问成功。

在浏览器中输入http://localhost:8080/testRole1,发现报错了。页面显示Subject does not have role [admin1]。这是因为shiro发现当前登陆的用户没有admin1的角色,于是会抛出AuthenticationException,我在后台配置了全局异常捕获。会自动跳转到500.html错误也,并打印错误信息。

@ControllerAdvice
public class MyExceptionHandler {

    private Logger logger = LoggerFactory.getLogger(MyExceptionHandler.class);

    @ExceptionHandler(value = RuntimeException.class)
    public Object handlerRuntimeException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        logger.error(e.getMessage());
        ModelAndView mv = new ModelAndView("/error/500");
        mv.addObject("msg", e.getMessage());
        return mv;
    }

    @ExceptionHandler(value = AuthenticationException.class)
    @ResponseBody
    public Object handlerAuthenticationException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        logger.error(e.getMessage());
        Map map = new HashMap();
        map.put("code", "500");
        map.put("msg", "用户名或密码错误");
        return map;
    }

}

查看控制台可以很明确的发现这一情况。

6. shiro配置session管理,并使用redis用于存储session

首先需要配置spring-redis相关配置。如下:

需要配置jedisConnectionFactory 并配置上你的redis服务器的地址,用户名和密码,端口,数据库编号等等。。。我们可以自己去阿里云、百度云、腾讯云等等购买一台云服务器并配置好redis环境。也可以在自己本地搭建一个redis server。如果是在本地构建redis server,推荐用VMware创建一台Linux虚拟机(任何发行版都可,如Ubuntu、Centos ...),因为redis官方任务redis是应用在服务器端的 key -value内存数据库,因此官方没有推出windows版本。 如果小伙伴们不会用Linux 或者 Linux不够熟练,还有一个办法,虽然redis官方没有推出redis的windows版本,但是微软爸爸倒是十分热心的提供了相应版本。可以通过这个url 去github上下载。https://github.com/MicrosoftArchive/redis  ,但是redis的windows版,目前最新的版本还停留在3.x版本。并且似乎很久没有更新过了...(可能是不再维护了吧.... 你懂的...)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" id="jedisConnectionFactory"
          p:usePool="true" p:poolConfig-ref="poolConfig" p:hostName="xxx.xx.xxx.xx" p:port="6379" p:password="123456"
          p:database="0">
        <!--<property name="usePool" value="true"/>
        <property name="hostName" value="192.168.152.129"/>-->
    </bean>

    <bean class="redis.clients.jedis.JedisPoolConfig" id="poolConfig">
        <property name="maxTotal" value="100"/>
        <property name="maxIdle" value="100"/>
        <property name="minIdle" value="10"/>
        <property name="maxWaitMillis" value="1000"/>
        <property name="blockWhenExhausted" value="true"/>
        <property name="testOnBorrow" value="false"/>
        <property name="testOnReturn" value="false"/>
        <property name="testOnCreate" value="false"/>
    </bean>

    <!--key采用Strng字符串,value采用二进制存储。 方便存储shiro 中的session & authen(认证) & author(授权)对象-->
    <bean class="org.springframework.data.redis.core.RedisTemplate" id="redisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
        <property name="keySerializer" ref="stringRedisSerializer"/>
        <property name="valueSerializer" ref="jdkSerializationRedisSerializer"/>
        <property name="hashKeySerializer" ref="stringRedisSerializer"/>
        <property name="hashValueSerializer" ref="jdkSerializationRedisSerializer"/>
    </bean>

    <!--默认采用String 字符串存储基本的对象信息-->
    <bean class="org.springframework.data.redis.core.StringRedisTemplate" id="stringRedisTemplate">
        <property name="connectionFactory" ref="jedisConnectionFactory"/>
    </bean>

    <!--序列化原文保存-->
    <bean class="org.springframework.data.redis.serializer.StringRedisSerializer" id="stringRedisSerializer"/>

    <!--json 字符串序列化-->
    <bean class="org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer"
          id="genericJackson2JsonRedisSerializer"/>

    <!--jdk默认序列化,实体类默认需要实现序列化接口-->
    <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"
          id="jdkSerializationRedisSerializer"/>

    <!--    <bean class="com.itsu.app.utils.FastJsonRedisSerializer" id="fastJsonRedisSerializer"/>-->
    <!--    <bean class="com.itsu.app.utils.GenericJackson2JsonRedisSerializerEx" id="genericJackson2JsonRedisSerializerEx"/>-->
    <bean id="jedisUtil" class="com.itsu.app.utils.JedisUtil"/>
</beans>

开始配置shiro 整合redis session管理。配置如下,需要自己定义RedisSessionDao,继承AbstractSessionDAO自己去写实现的方法。需要注意,在重写doCreate方法的时候一定要先调用父类的generateSessionId 方法生成一个sessionId 并且调用父类assignSessionId方法将当前要创建的session与你生成的sessionId进行绑定,不然会报错。剩下的都是一些基本的redis操作,就不做赘述了。

<!--session manager-->
    <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
        <property name="globalSessionTimeout" value="300000"/>
        <property name="deleteInvalidSessions" value="true"/>
        <property name="sessionDAO" ref="sessionDao"/>
    </bean>

 <!--redis 管理Session(shiro.session)-->
    <bean id="sessionDao" class="com.itsu.app.session.RedisSessionDao"/>

 <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"/>
        <property name="sessionManager" ref="sessionManager"/>
    </bean>
public class RedisSessionDao extends AbstractSessionDAO {

    @Resource
    private JedisUtil jedisUtil;

    private final String session_prefix = "shiro:session";

    private String getKey(String key) {

        return session_prefix + ":" + key;
    }

    private void saveSession(Session session) {
        if (session != null && session.getId() != null) {
            String key = getKey(session.getId().toString());
            byte[] sessionValue = SerializationUtils.serialize(session);
            jedisUtil.set(key, sessionValue);
//            jedisUtil.expire(key,);
        }
    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionid = super.generateSessionId(session);
        super.assignSessionId(session, sessionid);
        saveSession(session);
        return sessionid;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        if (sessionId == null) {
            return null;
        }
        String key = getKey(sessionId.toString());
        byte[] sessionByts = jedisUtil.get(key);

        Session session = (Session) SerializationUtils.deserialize(sessionByts);
        return session;

    }

    @Override
    public void update(Session session) throws UnknownSessionException {
        saveSession(session);
    }

    @Override
    public void delete(Session session) {
        if (session != null && session.getId() != null) {
            jedisUtil.del(getKey(session.getId().toString()));
        }
    }

    @Override
    public Collection<Session> getActiveSessions() {
        Set<String> keys = jedisUtil.keys(session_prefix);
        Set<Session> values = new HashSet<>();
        for (String key : keys) {
            Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
            values.add(session);
        }
        return values;
    }
}
@Component
public class JedisUtil {

    @Resource
    private RedisTemplate redisTemplate;

    public void set(String key, byte[] value) {
        redisTemplate.opsForValue().set(key, value);
    }

    public byte[] get(String key) {
        return (byte[]) redisTemplate.opsForValue().get(key);
    }

    public void del(String key) {
        redisTemplate.delete(key);
    }

    public void expire(String key, long time) {
        redisTemplate.expire(key, time, TimeUnit.SECONDS);
    }

    public Set<String> keys(String keyPrefix) {
        Set<String> keys = redisTemplate.keys(keyPrefix + "*");
        return keys;
    }

}

完成之后,我们重写启动一下服务,再次登陆一下。然后我们看看redis中是否有将session存储进来。可以看到,redis的确将session存储进来了。

7. shiro添加缓存处理。

可以看到,在我之前的配置中,认证和授权都会去访问数据库。而我们都知道,访问数据库属于IO 操作,需要读写硬盘。这样的效率一定会很差。理想状态下,用户只需要在第一次登陆的时候访问数据库获取信息进行认证。之后我们将认证信息存储在缓存中。下一次再进行相关操作直接从缓存中读取,而不再去读数据库。这样就能大大的提升性能。而我们的项目中刚好又用到了redis,redis可是时下最主流的缓存的解决方案了。所以我们可以用shiro + reids实现缓存功能。

需要了解的是,shiro默认支持多种缓存方案。redis、Ehache 包括最简单的内存缓存(即定义一个全局的CurrentHashMap用来存储数据)。这里我只针对redis这种缓存方案进行整理。

看配置,首先我定义了一个RedisCacheManager类交给spring进行管理,并传入了一个list,list中存入两个值,分别是缓存的名字。 authenCache表示认证的缓存名,authorCache表示授权的缓存名。创建RedisCache 类实现Cache接口。重写它需要重写的方法。在自定义realm中还需要开启authenticationCachingEnabled =true,和 authorizationCachingEnabled = true表示开启认证和授权的缓存处理。

<bean id="redisCacheManager" class="com.itsu.app.shrio.cache.RedisCacheManager">
        <property name="cacheNames">
            <list>
                <value>authenCache</value>
                <value>authorCache</value>
            </list>
        </property>
    </bean>

<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
        <property name="realm" ref="myRealm"/>
        <property name="cacheManager" ref="redisCacheManager"/>
        <property name="sessionManager" ref="sessionManager"/>
        <property name="rememberMeManager" ref="rememberMeManager"/>
    </bean>

 <!--自定义realm实现-->
    <bean id="myRealm" class="com.itsu.app.shrio.realm.MyRealm">
        <property name="credentialsMatcher" ref="credentialsMatcher"></property>
        <property name="name" value="MyRealm"></property>
        <property name="authenticationCachingEnabled" value="true"/>
        <property name="authenticationCacheName" value="authenCache"/>
        <property name="authorizationCachingEnabled" value="true"/>
        <property name="authorizationCacheName" value="authorCache"/>
    </bean>
public class RedisCacheManager implements CacheManager {

    private List<String> cacheNames;

    public void setCacheNames(List<String> cacheNames) {
        this.cacheNames = cacheNames;
    }

    @Resource
    private JedisUtil jedisUtil;

    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        Cache<K, V> cache = null;
        if (cacheNames.contains(name)) {
            cache = new RedisCache(name, jedisUtil);
        }
        return cache;
    }
}
@Component
public class RedisCache<K, V> implements Cache<K, V> {

    private JedisUtil jedisUtil;

    private RedisSerializer serializer = new JdkSerializationRedisSerializer();

    private final String cache_prefix = "cache";

    private String cacheName;

    public RedisCache() {
    }

    public RedisCache(String name, JedisUtil jedisUtil) {
        this.cacheName = name;
        this.jedisUtil = jedisUtil;
    }

    private String getKey(K key) {
        return cache_prefix + ":" + key + ":" + this.cacheName;
    }


    @Override
    public V get(K key) throws CacheException {
        byte[] value = jedisUtil.get(getKey(key));
        if (value != null) {
            return (V) serializer.deserialize(value);
        }
        return null;
    }

    @Override
    public V put(K key, V value) throws CacheException {
        String k = getKey(key);
        byte[] v = serializer.serialize(value);
        jedisUtil.set(k, v);
//        jedisUtil.expire(k, 600);

        return value;
    }

    @Override
    public V remove(K key) throws CacheException {
        String k = getKey(key);
        byte[] v = jedisUtil.get(k);
        jedisUtil.del(k);
        if (v != null) {
            return (V) SerializationUtils.deserialize(v);
        }
        return null;
    }

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

    @Override
    public int size() {
      ....
    }

    @Override
    public Set<K> keys() {
        ....
    }

    @Override
    public Collection<V> values() {
        ....
    }
}

 

完成以后我们再重启服务器来看效果,重写登陆以后查看redis的情况

发现缓存的确被写入了redis。

总结(遇到的坑):

spring + shiro + redis整合其中存在一些影藏的bug。

例1:在自定义realm中我对密码的加盐处理,需要将盐转换成 ByteSource对象,Shiro自己提供了一个Util,在ByteSource.Util.bytes() 方法,但是遗憾的是这个类没有实现序列化接口。因为我定义的redis对value的序列化方式为默认的JdkSerializationRedisSerializer,这个序列化功能可以将对象转成二进制文件存储进redis,好是很好用,但是要求被序列化的对象统统的需要实现Serializable接口。然而,shiro自己提供的SimpleByteSource并没有实现这个接口。。。(是不是很坑爹!当时我也是一点一点去撸源码才发现的这个bug),所以细心的小伙伴应该发现了我在自定义realm中并没有用shiro提供的bytesource util 而是自己定义了一个MySimpleByteSource,并提供了一个ByteSourceUtil类来将自定义realm中的”盐“转为ByteSource对象。

SimpleByteSource源码:

自己实现ByteSource接口

public class MySimpleByteSource implements ByteSource, Serializable {

    private static final long serialVersionUID = 1269274896730884458L;
    private byte[] bytes;
    private String cachedHex;
    private String cachedBase64;

    public MySimpleByteSource() {

    }

    public MySimpleByteSource(byte[] bytes) {
        this.bytes = bytes;
    }

    public MySimpleByteSource(char[] chars) {
        this.bytes = CodecSupport.toBytes(chars);
    }

    public MySimpleByteSource(String string) {
        this.bytes = CodecSupport.toBytes(string);
    }

    public MySimpleByteSource(ByteSource source) {
        this.bytes = source.getBytes();
    }

    public MySimpleByteSource(File file) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(file);
    }

    public MySimpleByteSource(InputStream stream) {
        this.bytes = (new MySimpleByteSource.BytesHelper()).getBytes(stream);
    }

    public static boolean isCompatible(Object o) {
        return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream;
    }

    public byte[] getBytes() {
        return this.bytes;
    }

    public boolean isEmpty() {
        return this.bytes == null || this.bytes.length == 0;
    }

    public String toHex() {
        if (this.cachedHex == null) {
            this.cachedHex = Hex.encodeToString(this.getBytes());
        }

        return this.cachedHex;
    }

    public String toBase64() {
        if (this.cachedBase64 == null) {
            this.cachedBase64 = Base64.encodeToString(this.getBytes());
        }

        return this.cachedBase64;
    }

    public String toString() {
        return this.toBase64();
    }

    public int hashCode() {
        return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0;
    }

    public boolean equals(Object o) {
        if (o == this) {
            return true;
        } else if (o instanceof ByteSource) {
            ByteSource bs = (ByteSource) o;
            return Arrays.equals(this.getBytes(), bs.getBytes());
        } else {
            return false;
        }
    }

    private static final class BytesHelper extends CodecSupport {
        private BytesHelper() {
        }

        public byte[] getBytes(File file) {
            return this.toBytes(file);
        }

        public byte[] getBytes(InputStream stream) {
            return this.toBytes(stream);
        }
    }
}

自己提供的ByteSourceUtil

public class ByteSourceUtil {
    public static ByteSource bytes(byte[] bytes) {
        return new MySimpleByteSource(bytes);
    }

    public static ByteSource bytes(String arg0) {
        return new MySimpleByteSource(arg0.getBytes());
    }

}

例2:本来并不打算使用JdkSerializationRedisSerializer序列化功能,而是打算使用redis 官方推荐的GenericJackson2JsonRedisSerializer序列化功能,可以将java 对象转化为json字符串进行存储,并且在反序列化的时候不用提供对象的Class属性即可反序列化成java对象。这样redis中保存的value的可读性就能打他提升。(json总比二进制能够看得懂吧。。。) 我之前一直用的这个,但是这一次我发现GenericJackson2JsonRedisSerializer对于对象中的一些新的属性支持的不太好。在集成shiro Session这个功能的时候,GenericJackson2JsonRedisSerializer一直不能将json字符串反序列化成Session对象。至今还没有找到原因,总是报有个别属性无法反序列化。。。 如果哪位大神知道是怎么回事,应该怎么解决的话,还请赐教。

代码的github地址:https://github.com/zjwan461/shiro  有需要的小伙伴可以拿去。第一次写技术博客,不到位的地方还请多多包涵。

如需转载还请标注原文出处,谢谢合作!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
springboot:是一个基于Java开发的框架,简化了Spring应用的初始化配置和部署过程。它提供了一套开发规范和约定,帮助开发人员快速搭建高效稳定的应用程序。 mybatis-plus:是基于MyBatis的增强工具,提供了一些便捷的CRUD操作方法和代码生成功能,简化了数据库操作的开发工作。它能够轻松集成到SpringBoot应用中,提高开发效率。 springmvc:是一种基于MVC设计模式的Web框架,用于构建Web应用程序。它能够从URL中解析请求参数,并将请求分发给对应的Controller进行处理。SpringMVC提供了一套灵活的配置和注解方式,支持RESTful风格的API开发。 shiro:是一种用于身份验证和授权的框架,可以集成到SpringBoot应用中。它提供了一套简单易用的API,可以处理用户认证、角色授权、会话管理等安全相关的功能。Shiro还支持集成其他认证方式,如LDAP、OAuth等。 redis:是一种开源的内存数据库,采用键值对存储数据。Redis具有高性能、高并发和持久化等特点,常用于缓存、消息队列和分布式锁等场景。在企业级报表后台管理系统中,可以使用Redis来进行缓存数据,提高系统的响应速度和性能。 企业级报表后台管理系统:是一种用于统一管理和生成报表的系统。它通常包括用户权限管理、报表设计、报表生成、数据分析等功能。使用SpringBoot、MyBatis-Plus、SpringMVCShiroRedis等技术,可以快速搭建一个可靠、高效的报表管理系统,满足企业对数据分析和决策的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值