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

本文介绍如何使用Spring MVC和Shiro框架搭建权限管理系统,包括配置环境、自定义Realm、整合Redis进行session管理和缓存处理,以及实现基于注解的权限控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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  有需要的小伙伴可以拿去。第一次写技术博客,不到位的地方还请多多包涵。

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

本项目详细介绍请看:http://www.sojson.com/shiro (强烈推荐) Demo已经部署到线上,地址是http://shiro.itboy.net, 管理员帐号:admin,密码:sojson.com 如果密码错误,请用sojson。 PS:你可以注册自己的帐号,然后用管理员赋权限给你自己的帐号,但是,每20分钟会把数据初始化一次。建议自己下载源码,让Demo跑起来,然后跑的更快,有问题加群解决。 声明: 本人提供这个Shiro + SpringMvc + Mybatis + Redis 的Demo 本着学习的态度,如果有欠缺和不足的地方,给予指正,并且多多包涵。 “去其糟粕取其精华”。如果觉得写的好的地方就给个赞,写的不好的地方,也请多多包涵。 使用过程: 1.创建数据库。 创建语句 :tables.sql 2.插入初始化数据 插入初始化数据:init.data.sql 3.运行。 管理员帐号:admin 密码:sojson ps:定时任务的sql会把密码改变为sojson.com 新版本说明:http://www.sojson.com/blog/164.html 和 http://www.sojson.com/blog/165.html 主要解决是之前说的问题:Shiro 教程,关于最近反应的相关异常问题,解决方法合集。 项目在本页面的附件中提取。 一、Cache配置修改。 配置文件(spring-cache.xml )中已经修改为如下配置: <!-- redis 配置,也可以把配置挪到properties配置文件中,再读取 --> <!-- 这种 arguments 构造的方式,之前配置有缺点。 这里之前的配置有问题,因为参数类型不一致,有时候jar和环境的问题,导致参数根据index对应,会处理问题, 理论上加另一个 name,就可以解决,现在把name 和type都加上,更保险。 --> 二、登录获取上一个URL地址报错。 当没有获取到退出前的request ,为null 的时候会报错。在(UserLoginController.java )135行处有所修改。 /** * shiro 获取登录之前的地址 * 之前0.1版本这个没判断空。 */ SavedRequest savedRequest = WebUtils.getSavedRequest(request); String url = null ; if(null != savedRequest){ url = savedRequest.getRequestUrl(); } /** * 我们平常用的获取上一个请求的方式,在Session不一致的情况下是获取不到的 * String url = (String) request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE); */ 三、删除了配置文件中的cookie写入域的问题。 在配置文件里(spring-shiro.xml )中的配置有所修改。 <!-- 会话Cookie模板 --> <!--cookie的name,我故意取名叫xxxxbaidu --> <!--cookie的有效时间 --> <!-- 配置存储Session Cookie的domain为 一级域名 --> 上面配置是去掉了 Session 的存储Key 的作用域,之前设置的.itboy.net ,是写到当前域名的 一级域名 下,这样就可以做到N 个 二级域名 下,三级、四级....下 Session 都是共享的。 <!-- 用户信息记住我功能的相关配置 --> <!-- 配置存储rememberMe Cookie的domain为 一级域名 --> <!-- 30天时间,记住我30天 --> 记住我登录的信息配置。和上面配置是一样的道理,可以在相同 一级域名 下的所有域名都可以获取到登录的信息。 四、简单实现了单个帐号只能在一处登录。 我们在其他的系统中可以看到,单个帐号只允许一人使用,在A处登录了,B处再登录,那A处就被踢出了。如下图所示。 但是此功能不是很完美,当A处被踢出后,再重新登录,这时候B处反应有点慢,具体我还没看,因为是之前加的功能,现在凌晨了,下次我有空再瞧瞧,同学你也可以看看,解决了和我说一声,我把功能修复。 五、修复功能(BUG) 1.修复权限添加功能BUG。 之前功能有问题,每当添加一个权限的时候,默认都给角色为“管理员”的角色默认添加当前新添加的权限。这样达到管理员的权限永远是最大的。由于代码有BUG ,导致所有权限删除了。现已修复。 2.修复项目只能部署到Root目录下的问题。 问题描述:之前项目只能部署到Root 下才能正常运行,目前已经修复,可以带项目路径进行访问了,之前只能这样访问,http://localhost:8080 而不能http://localhost:8080/shiro.demo/ 访问,目前是可以了。 解决方案:在 FreeMarkerViewExtend.java 33行处 增加了BasePath ,通过BasePath 来控制请求目录,在 Freemarker 中可以自由使用,而 JSP 中是直接在 JSP 中获取BasePath 使用。 解决后遗症:因为我们的权限是通过URL 来控制的,那么增加了项目的目录,导致权限不能正确的判断,再加上我们的项目名称(目录)可以自定义,导致更不好判断。 后遗症解决方案:PermissionFilter.java 50行处 解决了这个问题,详情请看代码和注释,其实就是replace 了一下。 HttpServletRequest httpRequest = ((HttpServletRequest)request); /** * 此处是改版后,为了兼容项目不需要部署到root下,也可以正常运行,但是权限没设置目前必须到root 的URI, * 原因:如果你把这个项目叫 ShiroDemo,那么路径就是 /ShiroDemo/xxxx.shtml ,那另外一个人使用,又叫Shiro_Demo,那么就要这么控制/Shiro_Demo/xxxx.shtml * 理解了吗? * 所以这里替换了一下,使用根目录开始的URI */ String uri = httpRequest.getRequestURI();//获取URI String basePath = httpRequest.getContextPath();//获取basePath if(null != uri && uri.startsWith(basePath)){ uri = uri.replace(basePath, ""); } 3.项目启动的时候报错,关于JNDI的错误提示。 其实也不是错,但是看着不舒服,所以还得解决这个问题。解决这个问题需要在web.xml 中的开始部位加入以下代码。 spring.profiles.active dev spring.profiles.default dev spring.liveBeansView.mbeanDomain dev 4.项目Maven打包问题。 打包的时候,不同版本的 Eclipse 还有IDEA 会有打包打不进去Mapper.xml 文件,这个时候要加如下代码(群里同学提供的)。 src/main/java **/*.properties **/*.xml false 在 标签内加入即可,如果还是不能解决,那么请你加群(改名后)说明你的问题,有人会回答你。 5.Tomcat7以上在访问JSP页面的时候,提示JSTL错误。 这个错误是因为Tomcat7 中没有 JSTL 的jar包,现在已经在项目pom.xml 中增加了如下 jar 的引入管理。 javax.servlet jstl 1.2 javax.servlet jsp-api 2.0 provided 如果还是不能解决问题,请在官方群(群号:259217951)内搜索“jstl” 如图下载依赖包。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值