项目代码:https://gitee.com/starrk110/shiroRedisSpringboot.git
实现功能:shiro的认证及权限控制,缓存及session使用redis管理,remenberme功能。(框架基于SSM,使用了mybatis-plus增强)

1:主要使用jar包(见最后)

2:安装redis

  • 安装redis及redis哨兵/集群部署请自行完成这不是重点

3:配置shiro的Configuration类

针对第一次接触shiro及springboot的人简单讲解一些常识,有了解的可以跳过:

  • springboot采用@Configuration+@Bean的形式进行配置,相当于我们在spring中配置的各种bean。
  • shiro是一个管理权限及认证的框架,主体是subject,通过在realm(你可以理解为内置可读写数据库的保险箱)中进行Authentication(认证,就是登陆),Authorization(授权,就是哪些页你能看)。其中还有各种管理器,比如缓存管理器CacheManager、会话管理器SessionManager等。通过这些管理器来进行shiro的个性化配置来达到符合你项目需求的目的。

开始配置

  • shiro有其自己管理生命周期的类,各个bean需要Dependon这个类进行加载。
/**
   * shiro管理生命周期的东西
   * @return
   */
  @Bean(name = "lifecycleBeanPostProcessor")
  public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
  }
  • 我们首先要配置shiro所要使用的所有过滤器以及映射路径,在shiro中其内置了一些过滤器,比如anon(匿名)、logout(登出)、authc(认证)等等,如有个性化添加可以如下配置。其中的RedisTemplete是为了整合redis使用的。
/**
   * 过滤器及映射路径的配置
   */
  @Bean(name = "shiroFilter")
  public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
      RedisTemplate redisTemplate) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    //配置拦截需要user/authc身份的跳转路径。
    shiroFilterFactoryBean.setLoginUrl("/signin.html");
    //配置登陆成功后跳转页面
    shiroFilterFactoryBean.setSuccessUrl("/index.html");
    //配置权限不足时跳转的页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/403.html");

    //过滤器链
    Map<String, Filter> filters = new LinkedHashMap<String, Filter>();
//    filters.put("perms", urlPermissionsFilter());
    filters.put("logout", new MySignOutFilter(redisTemplate));
    shiroFilterFactoryBean.setFilters(filters);

    //权限映射链
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    filterChainDefinitionMap.put("/static/**", "anon");
    filterChainDefinitionMap.put("/403.html", "anon");
    filterChainDefinitionMap.put("/signin.html", "anon");
    filterChainDefinitionMap.put("/signout", "logout");
    filterChainDefinitionMap.put("/**/delete*/**", "perms[Archer]");
    filterChainDefinitionMap.put("/**/select*/**", "perms[Saber]");
    filterChainDefinitionMap.put("/**/find*/**", "perms[Saber]");
    filterChainDefinitionMap.put("/**/update*", "perms[Lancer]");
//        filterChainDefinitionMap.put("/**/insert*", "perms[Berserker]");
    filterChainDefinitionMap.put("/regist.html", "anon");
    filterChainDefinitionMap.put("/contact.html", "authc,roles[天下无双]");
    filterChainDefinitionMap.put("/post.html", "user,roles[Master]");
    filterChainDefinitionMap.put("/about.html", "user");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;

  }
  • 之后配置安全管理模块,也是最为核心的一个模块,是为了配置你所有的manager
/**
   * 安全管理模块,所有的manager在此配置
   * @param redisTemplate
   * @return
   */
  @Bean(name = "securityManager")
  public SecurityManager securityManager(RedisTemplate redisTemplate) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

    //自定义realm
    securityManager.setRealm(myShiroRealm(redisTemplate));

    //自定义session管理 使用redis
    securityManager.setSessionManager(sessionManager(redisTemplate));

//    //自定义缓存实现 使用redis
//    securityManager.setCacheManager(redisCacheManager());

    //注入记住我管理器;
    securityManager.setRememberMeManager(rememberMeManager());

    return securityManager;
  }
  • 这时候我们需要配置一个认证授权模块了,也是最关键的Realm,这个realm需要我们自己去手写其实现方式,来判断你所使用的加密算法,以及你的角色授权,通过继承AuthorizingRealm这个类。
    然后去配置这个自己实现的类,自己实现的类如下
public class MyShiroRealm extends AuthorizingRealm {
    @Autowired
    private MPTbBloggerService tbBloggerService;


    public MyShiroRealm(){

    }

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        String username = (String)principalCollection.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

        //权限获取
        List<Authority> authorities = tbBloggerService.findAuthorityByUsername(username);

        Set<String> permisssionSets = new HashSet<>();
        for (Authority perm:authorities) {
            permisssionSets.add(perm.getAuthorName());
        }
        info.setStringPermissions(permisssionSets);

        //角色获取
        List<Role> roles = tbBloggerService.findRolesByUsername(username);
        Set<String> rolenames = new HashSet<>();

        for (Role role:roles) {
            rolenames.add(role.getRoleName());
        }
        info.addRoles(rolenames);

        return info;
    }

    /**
     * 认证
     * @param Atoken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken Atoken) throws AuthenticationException {
        UsernamePasswordToken token = (UsernamePasswordToken)Atoken;
        String username = token.getUsername();
        //这里的获取对象是MybatisPlus中的写法,如果不适可以使用mybatis手写。
        TbBlogger tbBlogger = tbBloggerService.selectOne(new EntityWrapper<TbBlogger>().where("username={0}",username));
        if (tbBlogger == null) {
            throw new UnknownAccountException();
        }
        String password =tbBlogger.getPassword();

//      ByteSource.Util.bytes(tbBlogger.getSalt()),简单讲解一下这里,
//对于第一次接触shiro的人来说应该是最难理解的地方,这个SimpleAuthenticationInfo会将你Token中的账号密码通过getName()
//这个方法获取,与你传入的username及password进行对比,byteSource是盐值,
//是为了加密时使用的。这里我采用了盐值存储在用户信息中的方式,而盐值的设置是在
//我注册用户时设置的,我采用的是随机字符串形式,当然你也可以采用随机数格式。
//而这个解密的方式在哪配置的,请看后面。
        SimpleAuthenticationInfo AInfo = new SimpleAuthenticationInfo(username,password, ByteSource.Util.bytes(tbBlogger.getSalt()),getName());
        return AInfo;
    }
}

配置这个类在Configure中

@Bean(name = "myShiroRealm")
  @DependsOn(value = {"lifecycleBeanPostProcessor", "ShiroRedisCacheManager"})
  public MyShiroRealm myShiroRealm(RedisTemplate redisTemplate) {
    MyShiroRealm shiroRealm = new MyShiroRealm();
    //设置缓存管理器
    shiroRealm.setCacheManager(redisCacheManager(redisTemplate));
    shiroRealm.setCachingEnabled(true);
    //设置认证密码算法及迭代复杂度
    shiroRealm.setCredentialsMatcher(credentialsMatcher());
    //认证
    shiroRealm.setAuthenticationCachingEnabled(false);
    //授权
    shiroRealm.setAuthorizationCachingEnabled(false);
    return shiroRealm;
  }

这时候我们就要配置那个加密算法了,当然注册时的加密算法需要你在注册的Controller里写,可以是前台写,可以是后台写,反正是post提交
只要加上这句

//算法,密码,盐值,迭代次数。
SimpleHash password = new SimpleHash("md5",tbBlogger.getPassword(), ByteSource.Util.bytes(tbBlogger.getSalt()),2);
/**
   * realm的认证算法
   * @return
   */
  @Bean(name = "hashedCredentialsMatcher")
  public HashedCredentialsMatcher credentialsMatcher() {
    HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher("md5");
    //2次迭代
    credentialsMatcher.setHashIterations(2);
    credentialsMatcher.setStoredCredentialsHexEncoded(true);
    return credentialsMatcher;
  }
  • 至此其实基本的shiro框架已经配置完成了,但是由于需要整合redis及将shiro的session交由redis管理,才有以下的配置,
  • 配置CacheManager
/**
   * 缓存管理器的配置
   * @param redisTemplate
   * @return
   */
  @Bean(name = "ShiroRedisCacheManager")
  public ShiroRedisCacheManager redisCacheManager(RedisTemplate redisTemplate) {
    ShiroRedisCacheManager redisCacheManager = new ShiroRedisCacheManager(redisTemplate);
    //name是key的前缀,可以设置任何值,无影响,可以设置带项目特色的值
    redisCacheManager.createCache("shiro_redis");
    return redisCacheManager;
  }

这个manager由于要使用redis进行存储,所以需要我们自己来实现,当然也很简单。如下

public class ShiroRedisCacheManager extends AbstractCacheManager{

    private RedisTemplate<byte[],byte[]> redisTemplate;

    public ShiroRedisCacheManager(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }
    //为了个性化配置redis存储时的key,我们选择了加前缀的方式,所以写了一个带名字及redis操作的构造函数的Cache类
    @Override
    protected Cache createCache(String name) throws CacheException {
        return new ShiroRedisCache(redisTemplate,name);
    }
}

个性化的Cache

public class ShiroRedisCache<K,V> implements Cache<K,V> {
    private RedisTemplate redisTemplate;
    private String prefix = "shiro_redis:";

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public ShiroRedisCache(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;
    }

    public ShiroRedisCache(RedisTemplate redisTemplate,String prefix){
        this(redisTemplate);
        this.prefix = prefix;
    }

    @Override
    public V get(K k) throws CacheException {
        if (k == null) {
            return null;
        }
        byte[] bytes = getBytesKey(k);
        return (V)redisTemplate.opsForValue().get(bytes);

    }

    @Override
    public V put(K k, V v) throws CacheException {
        if (k== null || v == null) {
            return null;
        }

        byte[] bytes = getBytesKey(k);
        redisTemplate.opsForValue().set(bytes, v);
        return v;
    }

    @Override
    public V remove(K k) throws CacheException {
        if(k==null){
            return null;
        }
        byte[] bytes =getBytesKey(k);
        V v = (V)redisTemplate.opsForValue().get(bytes);
        redisTemplate.delete(bytes);
        return v;
    }

    @Override
    public void clear() throws CacheException {
        redisTemplate.getConnectionFactory().getConnection().flushDb();

    }

    @Override
    public int size() {
        return redisTemplate.getConnectionFactory().getConnection().dbSize().intValue();
    }

    @Override
    public Set<K> keys() {
        byte[] bytes = (prefix+"*").getBytes();
        Set<byte[]> keys = redisTemplate.keys(bytes);
        Set<K> sets = new HashSet<>();
        for (byte[] key:keys) {
            sets.add((K)key);
        }
        return sets;
    }

    @Override
    public Collection<V> values() {
        Set<K> keys = keys();
        List<V> values = new ArrayList<>(keys.size());
        for(K k :keys){
            values.add(get(k));
        }
        return values;
    }

    private byte[] getBytesKey(K key){
        if(key instanceof String){
            String prekey = this.prefix + key;
            return prekey.getBytes();
        }else {
            return SerializeUtil.serialize(key);
        }
    }

}
  • 我们将session交由Redis去管理,这个时候就需要进行redis的写入,所以需要一个redisSessionDao,写入的key使用我们自己生成一个个性化的key,设置为SessionId,当然你完全可以使用JDK的那个UUID,然后Value的值为session的内容。
/**
   *  配置sessionmanager,由redis存储数据
   */
  @Bean(name = "sessionManager")
  @DependsOn(value = "lifecycleBeanPostProcessor")
  public DefaultWebSessionManager sessionManager(RedisTemplate redisTemplate) {
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    MyRedisSessionDao redisSessionDao = new MyRedisSessionDao(redisTemplate);
    //这个name的作用也不大,只是有特色的cookie的名称。
    redisSessionDao.setSessionIdGenerator(sessionIdGenerator("starrkCookie"));
    sessionManager.setSessionDAO(redisSessionDao);
    sessionManager.setDeleteInvalidSessions(true);
    SimpleCookie cookie = new SimpleCookie();
    cookie.setName("starrkCookie");
    sessionManager.setSessionIdCookie(cookie);
    sessionManager.setSessionIdCookieEnabled(true);
    return sessionManager;
  }

  /**
   * 自定义的SessionId生成器
   * @param name
   * @return
   */
  public MySessionIdGenerator sessionIdGenerator(String name) {
    return new MySessionIdGenerator(name);
  }

我们实现的sessionDao

public class MyRedisSessionDao extends EnterpriseCacheSessionDAO {

    private RedisTemplate<byte[],byte[]> redisTemplate;
    public MyRedisSessionDao(RedisTemplate redisTemplate){
        this.redisTemplate = redisTemplate;

    }

    @Override
    protected Serializable doCreate(Session session) {
        Serializable sessionId = super.doCreate(session);
        redisTemplate.opsForValue().set(sessionId.toString().getBytes(),sessionToByte(session));
        return sessionId;
    }

    @Override
    protected Session doReadSession(Serializable sessionId) {
        Session session = super.doReadSession(sessionId);
        if(session == null){
            byte[] bytes =  redisTemplate.opsForValue().get(sessionId.toString().getBytes());
            if(bytes != null && bytes.length > 0){
                session = byteToSession(bytes);
            }
        }
        return session;
    }

    //设置session的最后一次访问时间
    @Override
    protected void doUpdate(Session session) {
        super.doUpdate(session);
        redisTemplate.opsForValue().set(session.getId().toString().getBytes(),sessionToByte(session));
    }

    // 删除session
    @Override
    protected void doDelete(Session session) {
        super.doDelete(session);
        redisTemplate.delete(session.getId().toString().getBytes());
    }

    private byte[] sessionToByte(Session session){
        if (null == session){
            return null;
        }
        ByteArrayOutputStream bo = new ByteArrayOutputStream();
        byte[] bytes = null;
        ObjectOutputStream oo ;
        try {
            oo = new ObjectOutputStream(bo);
            oo.writeObject(session);
            bytes = bo.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }
        return bytes;

    }
    private Session byteToSession(byte[] bytes){
        if(0==bytes.length){
            return null;
        }
        ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
        ObjectInputStream in;
        SimpleSession session = null;
        try {
            in = new ObjectInputStream(bi);
            session = (SimpleSession) in.readObject();
        }catch (Exception e){
            e.printStackTrace();
        }
        return session;
    }

}
  • 然后我们配置了记住我功能
/**
   * 这个参数是RememberMecookie的名称,随便起。
   * remenberMeCookie是一个实现了将用户名保存在客户端的一个cookie,与登陆时的cookie是两个simpleCookie。
   * 登陆时会根据权限去匹配,如是user权限,则不会先去认证模块认证,而是先去搜索cookie中是否有rememberMeCookie,
   * 如果存在该cookie,则可以绕过认证模块,直接寻找授权模块获取角色权限信息。
   * 如果权限是authc,则仍会跳转到登陆页面去进行登陆认证.
   * @return
   */
  public SimpleCookie rememberMeCookie() {
    SimpleCookie simpleCookie = new SimpleCookie("remenbermeCookie");
    //<!-- 记住我cookie生效时间30天 ,单位秒;-->
    simpleCookie.setMaxAge(60);
    return simpleCookie;
  }

  /**
   * cookie管理对象;记住我功能
   */
  public CookieRememberMeManager rememberMeManager() {
    CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
    cookieRememberMeManager.setCookie(rememberMeCookie());
    //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
    cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
    return cookieRememberMeManager;
  }

至此,全部的springboot整合redis及shiro的配置类@Configuration就写完了。

  • *

调用

我们在登陆时进行了调用
主要通过前面讲的主体SecurityUtils.getSubject().login(token)来进行登陆。
具体实现如下。

/**
   * 通过用户名和密码查询用户信息
   * @param loginVo
   * @param request
   * @return
   */
    @PostMapping("/signinbyUAP")
    //封装了一个Vo来接收username,password,rememberme属性
    public String getTbBloggerInfo(LoginVo loginVo, HttpServletRequest request){
        System.out.println("登陆时的Vo:"+loginVo);
        UsernamePasswordToken token = new UsernamePasswordToken(loginVo.getUsername(),loginVo.getPassword(),loginVo.isRemenberMe());
        try {
            SecurityUtils.getSubject().login(token);
            return "index";
        }catch(Exception e) {
            request.setAttribute("wrongMessage",e.getMessage());
            return "signin.html";
        }

    }

jar包

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.46</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>2.1.9</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>2.1.9</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>