前言

这里我才用了Spring Boot的方式创建的项目,所以和Spring xml的注解有所不同。但道理都差不多的。

因为用到了Redis。需要安装Redis的参照:Redis的安装和使用

gitee项目地址(数据库在根目录):https://gitee.com/benzhu/shiro_conversation

项目下载:本地下载

知识点

1.连接Redis用的的包

Spring Boot 提供了对 Redis 集成的组件包:spring-boot-starter-data-redis,spring-boot-starter-data-redis依赖于spring-data-redis 和 lettuce 。Spring Boot 1.0 默认使用的是 Jedis 客户端,2.0 替换成 Lettuce,但如果你从 Spring Boot 1.5.X 切换过来,几乎感受不大差异,这是因为 spring-boot-starter-data-redis 为我们隔离了其中的差异性。

Lettuce 是一个可伸缩线程安全的 Redis 客户端,多个线程可以共享同一个 RedisConnection,它利用优秀 netty NIO 框架来高效地管理多个连接。 如果注入失败的话记得查看是否导入的包名有错。我这里的案例是Spring2版本的。

注意不同版本的spring boot下,redis的starter依赖名略有不同,如果上面的不行,可以尝试spring-boot-starter-data-redis,spring-boot-starter-redis。

2.配置文件的问题

如果配置了连接池的空闲数啥的记得导入下面的包:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>  

3.StringRedisTemplate与RedisTemplate区别点

为什么要特别拎着个出来呢,因为我存入的是byte[]数组,但是在用可视化工具打开Redis的时候发现里面是乱码的,然后尝试处理发现处理失败,程序还是能够正常使用的,就是看着有点不爽。

StringRedisTemplat两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。

两者的关系是StringRedisTemplate继承RedisTemplate;其实他们两者之间的区别主要在于他们使用的序列化类: RedisTemplate使用的是JdkSerializationRedisSerializer存入数据会将数据先序列化成字节数组然后在存入Redis数据库。

StringRedisTemplate使用的是StringRedisSerializere。

教程

1.导入Maven

只给导入包的部分。

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.2.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-web</artifactId>
            <version>1.2.2</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Spring Boot JDBC -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--shiro注解(aop生效) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- Redis的依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
            <version>2.6.2</version>
        </dependency>
    </dependencies>

2.配置文件

这里主要是添加Redis的配置;注意我安装Redis的时候我是没有配置密码的。

# 端口设置
server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/shiro?&useSSL=false&serverTimezone=GMT%2B8&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#开启aop
spring.aop.proxy-target-class=true

# REDIS (RedisProperties)
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=localhost
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
# 连接超时时间(毫秒)
#spring.redis.timeout=0

3.编写User实体类

package com.benzhu.shiro.domain;

public class User {
    private String username;
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

4.编写Shiro配置类

package com.benzhu.shiro.Filter;

import com.benzhu.shiro.realm.CustomRealm;
import com.benzhu.shiro.session.CustomSessionManager;
import com.benzhu.shiro.session.RedisSessionDao;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.util.LinkedHashMap;
import java.util.Map;

@Configuration
public class ShiroConfiguration {

    @Resource(name = "realm")
    private CustomRealm customRealm;
    @Resource(name = "redisSessionDao")
    private RedisSessionDao redisSessionDao;

    @Bean(name="shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager manager) {
        ShiroFilterFactoryBean bean=new ShiroFilterFactoryBean();
        bean.setSecurityManager(manager);
        //配置登录的url和登录成功的url以及验证失败的url
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/error");

        //配置自定义的Filter
        Map<String, Filter>filtersMap = new LinkedHashMap<String, Filter>();
        filtersMap.put("roleOrFilter", new RolesOrFilter());
        bean.setFilters(filtersMap);
        //配置访问权限
        LinkedHashMap<String, String>filterChainDefinitionMap=new LinkedHashMap<>();
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/sublogin", "anon");
        filterChainDefinitionMap.put("/usererror", "anon");
        filterChainDefinitionMap.put("/roles1", "roles[admin]");
        filterChainDefinitionMap.put("/roles2", "roles[admin,user]");
        filterChainDefinitionMap.put("/permission1", "perms[user:update]");
        filterChainDefinitionMap.put("/permission2", "perms[user:update,user:select]");
        filterChainDefinitionMap.put("/roles3", "roleOrFilter[admin,user]");
        filterChainDefinitionMap.put("/**","authc");
        bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return bean;
    }



    //配置核心安全事务管理器
    @Bean(name="securityManager")
    public DefaultWebSecurityManager securityManager(@Qualifier("sessionManager")DefaultWebSessionManager sessionManager) {
        DefaultWebSecurityManager manager=new DefaultWebSecurityManager();
        //配置自定义Realm
        manager.setRealm(customRealm);
        //配置自定义sessionManager
        manager.setSessionManager(sessionManager);
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); //创建加密对象
        matcher.setHashAlgorithmName("md5"); //加密的算法
        matcher.setHashIterations(1);//加密次数
        customRealm.setCredentialsMatcher(matcher); //放入自定义Realm
        return manager;
    }

    //以下是开启注解支持
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    //配置session
    @Bean(name="sessionManager")
    public CustomSessionManager sessionManager(){
        //把sessionManager注入Bean
        CustomSessionManager manager = new CustomSessionManager();
        manager.setSessionDAO(redisSessionDao);
        return manager;
    }
}

5.编写自定义的Realm

package com.benzhu.shiro.realm;

import com.benzhu.shiro.dao.UserDao;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Component("realm")
public class CustomRealm extends AuthorizingRealm {

    @Resource
    private UserDao userDao;

    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {

        String username = (String) principals.getPrimaryPrincipal();
        //从缓存中拿到角色数据(没有设置缓存只能再查一次数据库)
        Set<String> roles = getRolesByUserName(username);
        //从缓存中拿到权限数据(没有设置缓存只能再查一次数据库)
        Set<String> permissions = getPermissionUserName(roles);
        //返回对象
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles(roles);
        authorizationInfo.setStringPermissions(permissions);
        return authorizationInfo;
    }

    private Set<String> getPermissionUserName(Set<String> roles) {
        Set<String> sets = new HashSet<>();
        for (String role : roles){
            List<String> permission = userDao.getPermissionByUserName(role);
            for (String permis:permission) {
                //这里输出可以看出运行了两次
                System.out.println(permis);
                sets.add(permis);
            }
        }
        return sets;
    }

    private Set<String> getRolesByUserName(String username) {
        List<String> roles = userDao.getRolesByUSerName(username);
        Set<String> sets = new HashSet<>(roles);
        return sets;
    }

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //1.从主体传过来的认证信息中获取用户名
        String username = (String) token.getPrincipal();
        //2.通过用户名到数据库中获取凭证
        String password = getPasswordByUserName(username);
        if(password == null){
            return null;
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(username,password,"customRealm");
        //加盐验证
        authenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("benzhu"));
        return authenticationInfo;

    }

    private String getPasswordByUserName(String username) {
        //模拟数据库查询
        return userDao.getUserPassWord(username);
    }

/*    public static void main(String[] args) {
        //计算加密结果的主方法 用来计算加密的密码结果 非必需
        Md5Hash md5Hash = new Md5Hash("123456","benzhu");
        System.out.println(md5Hash);
    }*/
}

6.编写数据库访问的方法

dao/UserDao.java

package com.benzhu.shiro.dao;

import java.util.List;

public interface UserDao {
    String getUserPassWord(String username);

    List<String> getRolesByUSerName(String username);

    List<String> getPermissionByUserName(String username);
}

dao/impl/UserDaoImpl.java

package com.benzhu.shiro.dao.impl;

import com.benzhu.shiro.dao.UserDao;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Component
public class UserDaoImpl implements UserDao {

    @Resource
    private JdbcTemplate jdbcTemplate;

    @Override
    public String getUserPassWord(String username) {
        String sql = "select password from users where username = ?";
        List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>(){

            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("password");
            }
        });

        if(list==null){
            return null;
        }
        return list.get(0);
    }

    @Override
    public List<String> getRolesByUSerName(String username) {
        String sql = "select role_name from user_roles where username = ?";
        List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("role_name");
            }
        });
        return list;
    }

    @Override
    public List<String> getPermissionByUserName(String username) {
        String sql = "select permission from roles_permissions where role_name = ?";
        List<String> list = jdbcTemplate.query(sql, new String[]{username}, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet resultSet, int i) throws SQLException {
                return resultSet.getString("permission");
            }
        });
        return list;
    }
}

7.编写自定义Shiro拦截方法

Filter/AuthorizationFilter.java

package com.benzhu.shiro.Filter;

import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.AuthorizationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public class RolesOrFilter extends AuthorizationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
        Subject subject = getSubject(servletRequest,servletResponse);
        String[] roles = (String[]) o;
        if(roles == null || roles.length == 0){
            return false;
        }
        for (String role : roles){
            if(subject.hasRole(role)){
                return true;
            }
        }
        return false;
    }
}

8.编写controller

controller/UserController.java

package com.benzhu.shiro.controller;

import com.benzhu.shiro.domain.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller("UserController")
@RequestMapping("/")
public class UserController {

    @RequestMapping(value = "login")
    public String login(){
        return "login";
    }

    @RequestMapping(value = "usererror")
    public String error(){
        return "usererror";
    }

    @RequestMapping(value = "sublogin")
    public String sublogin(User user){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(),user.getPassword());
        try {
            subject.login(token);
            try {
                subject.checkRole("admin");
                subject.checkPermission("user:update");
                    //理解非注解也可以做转跳拦截之类的动作
                    if(subject.hasRole("admin")){
                        System.out.println("拥有admin角色!");
                    }
                    if(subject.isPermitted("user:update")){
                        System.out.println("拥有user:update权限!");
                    }
                return "index";
            }catch (UnauthorizedException exception){
                System.out.println("角色授权或者权限授权失败!");
                return "error";
            }
        }catch (AuthenticationException e){
            System.out.println("认证失败!");
            return "usererror";
        }
    }

    @RequiresRoles("admin")
    @RequestMapping("testroels")
    @ResponseBody
    public String testroels(){
        return "有admin角色!";
    }

    @RequiresRoles("admin1")
    @RequestMapping("testroels1")
    @ResponseBody
    public String testroels1(){
        return "有admin1角色!";
    }

    @RequiresPermissions("user:update")
    @RequestMapping("testPermission")
    @ResponseBody
    public String testPermission(){
        return "拥有user:update权限";
    }

    @RequiresPermissions("user:update1")
    @RequestMapping("testPermission1")
    @ResponseBody
    public String testPermission1(){
        return "拥有user:update1权限";
    }

    @RequiresRoles("admin")
    @RequestMapping("exit")
    public String exit(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }

    @RequestMapping("roles1")
    @ResponseBody
    public String roles1(){
        return "拥有admin角色";
    }

    @RequestMapping("roles2")
    @ResponseBody
    public String roles2(){
        return "拥有admin和user角色";
    }

    @RequestMapping("permission1")
    @ResponseBody
    public String permission1(){
        return "拥有user:update权限";
    }

    @RequestMapping("permission2")
    @ResponseBody
    public String permission2(){
        return "拥有user:update和user:select权限";
    }

    @RequestMapping("roles3")
    @ResponseBody
    public String roles3(){
        return "拥有admin或者user角色";
    }
}

9.编写前端页面

这里自己参照controller写一下就好。还要多写一个error页面。

10.编写seesion的缓存在Redis

本文的重点;对Shiro中的Session保存在Redis;对Redis进行增删查改。

session/CustomSessionManager.java

package com.benzhu.shiro.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;

import javax.servlet.ServletRequest;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        if(sessionKey instanceof WebSessionKey){
            //判断sessionKey是不是WebSessionKey是的话才能强转
            request = ((WebSessionKey)sessionKey).getServletRequest();
        }
        if(request != null && sessionId !=null){
            //如果request不为空获取request的session
            Session session = (Session) request.getAttribute(sessionId.toString());
            if(session != null){
                return session;
            }
        }
        //为空就再获取一次
        Session session = super.retrieveSession(sessionKey);
        if(request != null && sessionId != null){
            //获取request的session
            request.setAttribute(sessionId.toString(),session);
        }
        return session;
    }
}

config/RedisConfig.java

package com.benzhu.shiro.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisConfig {

    @Autowired
    private RedisTemplate redisTemplate;


    public void set(byte[] key, byte[] value) {
        //插入session值
        redisTemplate.opsForValue().set(key, value);
        //stringRedisTemplate.opsForValue().set(new String(key), new String(value), i, TimeUnit.SECONDS);
    }

    public void expire(byte[] key, int i) {
        //设置超时时间
        //三个参数 分别是key 时间 时间类型(时分秒等)
        redisTemplate.expire(key, i, TimeUnit.SECONDS);
    }

    public byte[] get(byte[] key) {
        //获取session值
        return (byte[]) redisTemplate.opsForValue().get(key);
    }

    public void del(byte[] key) {
        //删除session值
        redisTemplate.delete(key);
    }

    public Set<byte[]> keys(String shiro_session_prefix) {
        return redisTemplate.keys(shiro_session_prefix + "*");
    }
}

11.重写写Shiro中的Session提高Redis的读取效率

就是重写Shiro的方法把session放到request里面减少读取Redis的次数来提高效率。

session/CustomSessionManager.java

package com.benzhu.shiro.session;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.SessionKey;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.apache.shiro.web.session.mgt.WebSessionKey;

import javax.servlet.ServletRequest;
import java.io.Serializable;

public class CustomSessionManager extends DefaultWebSessionManager {
    @Override
    protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
        Serializable sessionId = getSessionId(sessionKey);
        ServletRequest request = null;
        if(sessionKey instanceof WebSessionKey){
            //判断sessionKey是不是WebSessionKey是的话才能强转
            request = ((WebSessionKey)sessionKey).getServletRequest();
        }
        if(request != null && sessionId !=null){
            //如果request不为空获取request的session
            Session session = (Session) request.getAttribute(sessionId.toString());
            if(session != null){
                return session;
            }
        }
        //为空就再获取一次
        Session session = super.retrieveSession(sessionKey);
        if(request != null && sessionId != null){
            //获取request的session
            request.setAttribute(sessionId.toString(),session);
        }
        return session;
    }
}

12.启动测试

在代码里面也写了一些输出来验证读取的次数是减少了,效果图就不给了,很多种验证的方法。