初识springboot

SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例(转)

1.前言
本文主要介绍使用SpringBoot与shiro实现基于数据库的细粒度动态权限管理系统实例。
使用技术:SpringBoot、mybatis、shiro、thymeleaf、pagehelper、Mapper插件、druid、dataTables、ztree、jQuery
开发工具:intellij idea
数据库:mysql、redis
基本上是基于使用SpringSecurity的demo上修改而成,地址 http://blog.csdn.net/poorcoder_/article/details/70231779

2.表结构
还是是用标准的5张表来展现权限。如下图:image
分别为用户表,角色表,资源表,用户角色表,角色资源表。在这个demo中使用了mybatis-generator自动生成代码。运行mybatis-generator:generate -e 根据数据库中的表,生成 相应的model,mapper单表的增删改查。不过如果是导入本项目的就别运行这个命令了。新增表的话,也要修改mybatis-generator-config.xml中的tableName,指定表名再运行。

3.maven配置

<?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.study</groupId>

<artifactId>springboot-shiro</artifactId>

<version>0.0.1-SNAPSHOT</version>

<packaging>jar</packaging>



<name>springboot-shiro</name>

<description>Demo project for Spring Boot</description>



<parent>
    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-starter-parent</artifactId>

    <version>1.5.2.RELEASE</version>

    <relativePath/> <!-- lookup parent from repository -->

</parent>



<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>

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

</properties>



<dependencies>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter</artifactId>

    </dependency>



    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-test</artifactId>

        <scope>test</scope>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-web</artifactId>

    </dependency>

    <dependency>

        <groupId>org.springframework.boot</groupId>

        <artifactId>spring-boot-starter-thymeleaf</artifactId>

    </dependency>

    <dependency>

        <groupId>com.github.pagehelper</groupId>

        <artifactId>pagehelper-spring-boot-starter</artifactId>

        <version>1.1.0</version>
    </dependency>

    <dependency>

        <groupId>tk.mybatis</groupId>

        <artifactId>mapper-spring-boot-starter</artifactId>

        <version>1.1.1</version>

    </dependency>

    <dependency>

        <groupId>org.apache.shiro</groupId>

        <artifactId>shiro-spring</artifactId>

        <version>1.3.2</version>

    </dependency>

    <dependency>

        <groupId>com.alibaba</groupId>

        <artifactId>druid</artifactId>

        <version>1.0.29</version>

    </dependency>

    <dependency>

        <groupId>mysql</groupId>

        <artifactId>mysql-connector-java</artifactId>

    </dependency>

    <dependency>

        <groupId>net.sourceforge.nekohtml</groupId>

        <artifactId>nekohtml</artifactId>

        <version>1.9.22</version>

    </dependency>

    <dependency>

        <groupId>com.github.theborakompanioni</groupId>

        <artifactId>thymeleaf-extras-shiro</artifactId>

        <version>1.2.1</version>

    </dependency>

    <dependency>

        <groupId>org.crazycake</groupId>

        <artifactId>shiro-redis</artifactId>

        <version>2.4.2.1-RELEASE</version>

    </dependency>

</dependencies>


<build>

    <plugins>

        <plugin>

            <groupId>org.springframework.boot</groupId>

            <artifactId>spring-boot-maven-plugin</artifactId>

        </plugin>

        <plugin>

            <groupId>org.mybatis.generator</groupId>

            <artifactId>mybatis-generator-maven-plugin</artifactId>

            <version>1.3.5</version>

            <configuration>

                <configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile>

                <overwrite>true</overwrite>

                <verbose>true</verbose>

            </configuration>

            <dependencies>

                <dependency>

                    <groupId>mysql</groupId>

                    <artifactId>mysql-connector-java</artifactId>

                    <version>${mysql.version}</version>

                </dependency>

                <dependency>

                    <groupId>tk.mybatis</groupId>

                    <artifactId>mapper</artifactId>

                    <version>3.4.0</version>

                </dependency>

            </dependencies>

        </plugin>

    </plugins>

</build>

4.配置Druid
package com.study.config;

import com.alibaba.druid.support.http.StatViewServlet;

import com.alibaba.druid.support.http.WebStatFilter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;

import org.springframework.boot.web.servlet.ServletRegistrationBean;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

/**

  • Created by yangqj on 2017/4/19.

*/

@Configuration

public class DruidConfig {

@Bean

public ServletRegistrationBean druidServlet() {



    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");

    //登录查看信息的账号密码.



    servletRegistrationBean.addInitParameter("loginUsername","admin");


    servletRegistrationBean.addInitParameter("loginPassword","123456");

    return servletRegistrationBean;

}


@Bean

public FilterRegistrationBean filterRegistrationBean() {

    FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();

    filterRegistrationBean.setFilter(new WebStatFilter());

    filterRegistrationBean.addUrlPatterns("/*");

    filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");

    return filterRegistrationBean;

}

}

在application.properties中加入:

数据源基础配置

spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

spring.datasource.driver-class-name=com.mysql.jdbc.Driver

spring.datasource.url=jdbc:mysql://localhost:3306/shiro

spring.datasource.username=root

spring.datasource.password=root

连接池配置

初始化大小,最小,最大

spring.datasource.initialSize=1

spring.datasource.minIdle=1

spring.datasource.maxActive=20

配置好后,运行项目访问http://localhost:8080/druid/ 输入配置的账号密码admin,123456进入:image

5.配置mybatis

使用springboot 整合mybatis非常方便,只需在application.properties

mybatis.type-aliases-package=com.study.model

mybatis.mapper-locations=classpath:mapper/*.xml

mapper.mappers=com.study.util.MyMapper

mapper.not-empty=false

mapper.identity=MYSQL

pagehelper.helperDialect=mysql

pagehelper.reasonable=true

pagehelper.supportMethodsArguments=true

pagehelper.params=count=countSql

将相应的路径改成项目包所在的路径即可。配置文件中可以看出来还加入了pagehelper 和Mapper插件。如果不需要,把上面配置文件中的 pagehelper删除。

MyMapper:

package com.study.util;

/**

  • Created by yangqj on 2017/4/20.

*/

import tk.mybatis.mapper.common.Mapper;

import tk.mybatis.mapper.common.MySqlMapper;

public interface MyMapper extends Mapper, MySqlMapper {

}

对于Springboot整合mybatis可以参考https://github.com/abel533/MyBatis-Spring-Boot

6.thymeleaf配置

thymeleaf是springboot官方推荐的,所以来试一下。

首先加入配置:

#spring.thymeleaf.prefix=classpath:/templates/

#spring.thymeleaf.suffix=.html

#spring.thymeleaf.mode=HTML5

#spring.thymeleaf.encoding=UTF-8

;charset= is added

#spring.thymeleaf.content-type=text/html

set to false for hot refresh

spring.thymeleaf.cache=false

spring.thymeleaf.mode=LEGACYHTML5

可以看到其实上面都是注释了的,因为springboot会根据约定俗成的方式帮我们配置好。所以上面注释部分是springboot自动配置的,如果需要自定义配置,只需要修改上注释部分即可。

后两行没有注释的部分,spring.thymeleaf.cache=false表示关闭缓存,这样修改文件后不需要重新启动,缓存默认是开启的,所以指定为false。但是在intellij idea中还需要按Ctrl + Shift + F9.

对于spring.thymeleaf.mode=LEGACYHTML5。thymeleaf对html中的语法要求非常严格,像我从网上找的模板,使用thymeleaf后报一堆的语法错误,后来没办法,使用弱语法校验,所以加入配置spring.thymeleaf.mode=LEGACYHTML5。加入这个配置后还需要在maven中加入

<groupId>net.sourceforge.nekohtml</groupId>

<artifactId>nekohtml</artifactId>

<version>1.9.22</version>

否则会报错的。

在前端页面的头部加入一下配置后,就可以使用thymeleaf了

不过这个项目因为使用了datatables都是使用jquery 的ajax来访问数据与处理数据,所以用到的thymeleaf语法非常少,基本上可以参考的就是js即css的导入和类似于jsp的include功能的部分页面引入。

对于静态文件的引入:

而文件在项目中的位置是static-css-bootstrap.min.css。为什么这样可以访问到该文件,也是因为springboot对于静态文件会自动查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.

页面引入:

局部页面如下:

...

主体页面映入方式:

inclide=”文件路径::局部代码片段名称”

7.shiro配置

配置文件ShiroConfig

package com.study.config;

import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;

import com.github.pagehelper.util.StringUtil;

import com.study.model.Resources;

import com.study.service.ResourcesService;

import com.study.shiro.MyShiroRealm;

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;

import org.apache.shiro.mgt.SecurityManager;

import org.apache.shiro.spring.LifecycleBeanPostProcessor;

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.crazycake.shiro.RedisCacheManager;

import org.crazycake.shiro.RedisManager;

import org.crazycake.shiro.RedisSessionDAO;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;

import java.util.List;

import java.util.Map;

/**

  • Created by yangqj on 2017/4/23.

*/

@Configuration

public class ShiroConfig {

@Autowired(required = false)

private ResourcesService resourcesService;



@Value("${spring.redis.host}")

private String host;



@Value("${spring.redis.port}")

private int port;



@Value("${spring.redis.timeout}")

private int timeout;



@Bean

public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {

    return new LifecycleBeanPostProcessor();

}


/**

 * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean

 * @return

 */

@Bean

public ShiroDialect shiroDialect() {

    return new ShiroDialect();

}

/**

 * ShiroFilterFactoryBean 处理拦截资源文件问题。

 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在

 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager

 *

 Filter Chain定义说明

 1、一个URL可以配置多个Filter,使用逗号分隔

 2、当设置多个过滤器时,全部验证通过,才视为通过

 3、部分过滤器可指定参数,如perms,roles

 *

 */

@Bean

public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){

    System.out.println("ShiroConfiguration.shirFilter()");

    ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();



    // 必须设置 SecurityManager

    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面

    shiroFilterFactoryBean.setLoginUrl("/login");

    // 登录成功后要跳转的链接

    shiroFilterFactoryBean.setSuccessUrl("/usersPage");

    //未授权界面;

    shiroFilterFactoryBean.setUnauthorizedUrl("/403");

    //拦截器.

    Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();



    //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了

    filterChainDefinitionMap.put("/logout", "logout");

    filterChainDefinitionMap.put("/css/**","anon");

    filterChainDefinitionMap.put("/js/**","anon");

    filterChainDefinitionMap.put("/img/**","anon");

    filterChainDefinitionMap.put("/font-awesome/**","anon");

    //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;

    //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->

    //自定义加载权限资源关系

    List<Resources> resourcesList = resourcesService.queryAll();

     for(Resources resources:resourcesList){



        if (StringUtil.isNotEmpty(resources.getResurl())) {

            String permission = "perms[" + resources.getResurl()+ "]";

            filterChainDefinitionMap.put(resources.getResurl(),permission);

        }

    }

    filterChainDefinitionMap.put("/**", "authc");




    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    return shiroFilterFactoryBean;

}



@Bean

public SecurityManager securityManager(){

    DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();

    //设置realm.

    securityManager.setRealm(myShiroRealm());

    // 自定义缓存实现 使用redis

    //securityManager.setCacheManager(cacheManager());

    // 自定义session管理 使用redis

    securityManager.setSessionManager(sessionManager());

    return securityManager;

}



@Bean

public MyShiroRealm myShiroRealm(){

    MyShiroRealm myShiroRealm = new MyShiroRealm();

    myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

    return myShiroRealm;

}



/**

 * 凭证匹配器
 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了

 *  所以我们需要修改下doGetAuthenticationInfo中的代码;

 * )

 * @return

 */

@Bean

public HashedCredentialsMatcher hashedCredentialsMatcher(){

    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();



    hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;

    hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));



    return hashedCredentialsMatcher;

}



/**

 *  开启shiro aop注解支持.

 *  使用代理方式;所以需要开启代码支持;

 * @param securityManager
 * @return

 */

@Bean

public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){

    AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();

    authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);

    return authorizationAttributeSourceAdvisor;

}


/**

 * 配置shiro redisManager

 * 使用的是shiro-redis开源插件

 * @return

 */

public RedisManager redisManager() {

    RedisManager redisManager = new RedisManager();

    redisManager.setHost(host);

    redisManager.setPort(port);

    redisManager.setExpire(1800);// 配置缓存过期时间

    redisManager.setTimeout(timeout);

    // redisManager.setPassword(password);

    return redisManager;

}



/**

 * cacheManager 缓存 redis实现

 * 使用的是shiro-redis开源插件

 * @return

 */

public RedisCacheManager cacheManager() {

    RedisCacheManager redisCacheManager = new RedisCacheManager();

    redisCacheManager.setRedisManager(redisManager());

    return redisCacheManager;

}



/**

 * RedisSessionDAO shiro sessionDao层的实现 通过redis

 * 使用的是shiro-redis开源插件

 */

@Bean

public RedisSessionDAO redisSessionDAO() {

    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();

    redisSessionDAO.setRedisManager(redisManager());

    return redisSessionDAO;

}


/**

 * shiro session的管理

 */

@Bean

public DefaultWebSessionManager sessionManager() {

    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

    sessionManager.setSessionDAO(redisSessionDAO());

    return sessionManager;

}

}

配置自定义Realm

package com.study.shiro;

import com.study.model.Resources;

import com.study.model.User;

import com.study.service.ResourcesService;

import com.study.service.UserService;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.authc.*;

import org.apache.shiro.authz.AuthorizationInfo;

import org.apache.shiro.authz.SimpleAuthorizationInfo;

import org.apache.shiro.realm.AuthorizingRealm;

import org.apache.shiro.session.Session;

import org.apache.shiro.subject.PrincipalCollection;

import org.apache.shiro.util.ByteSource;

import javax.annotation.Resource;

import java.util.HashMap;

import java.util.List;

import java.util.Map;

/**

  • Created by yangqj on 2017/4/21.

*/

public class MyShiroRealm extends AuthorizingRealm {

@Resource

private UserService userService;



@Resource

private ResourcesService resourcesService;


//授权

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

    User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}

    Map<String,Object> map = new HashMap<String,Object>();

    map.put("userid",user.getId());

    List<Resources> resourcesList = resourcesService.loadUserResources(map);

    // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    for(Resources resources: resourcesList){

        info.addStringPermission(resources.getResurl());

    }

    return info;

}


//认证

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    //获取用户的输入的账号.

    String username = (String)token.getPrincipal();

    User user = userService.selectByUsername(username);

    if(user==null) throw new UnknownAccountException();

    if (0==user.getEnable()) {

        throw new LockedAccountException(); // 帐号锁定

    }

    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(

            user, //用户

            user.getPassword(), //密码

            ByteSource.Util.bytes(username),

            getName()  //realm name

    );

    // 当验证都通过后,把用户信息放在session里

    Session session = SecurityUtils.getSubject().getSession();

    session.setAttribute("userSession", user);

    session.setAttribute("userSessionId", user.getId());

    return authenticationInfo;

}

}

认证:

shiro的主要模块分别就是授权和认证和会话管理。

我们先讲认证。认证就是验证用户。比如用户登录的时候验证账号密码是否正确。

我们可以把对登录的验证交给shiro。我们执行要查询相应的用户信息,并传给shiro。如下代码则为用户登录:

@RequestMapping(value="/login",method=RequestMethod.POST)

public String login(HttpServletRequest request, User user, Model model){

    if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) {

        request.setAttribute("msg", "用户名或密码不能为空!");

        return "login";

    }

    Subject subject = SecurityUtils.getSubject();

    UsernamePasswordToken token=new UsernamePasswordToken(user.getUsername(),user.getPassword());

    try {

        subject.login(token);

        return "redirect:usersPage";

    }catch (LockedAccountException lae) {

        token.clear();

        request.setAttribute("msg", "用户已经被锁定不能登录,请与管理员联系!");

        return "login";

    } catch (AuthenticationException e) {

        token.clear();

        request.setAttribute("msg", "用户或密码不正确!");

        return "login";

    }

}

可见用户登陆的代码主要就是 subject.login(token);调用后就会进去我们自定义的realm中的doGetAuthenticationInfo()方法。

//认证

@Override

protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

    //获取用户的输入的账号.

    String username = (String)token.getPrincipal();

    User user = userService.selectByUsername(username);

    if(user==null) throw new UnknownAccountException();

    if (0==user.getEnable()) {

        throw new LockedAccountException(); // 帐号锁定

    }

    SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(

            user, //用户

            user.getPassword(), //密码

            ByteSource.Util.bytes(username),

            getName()  //realm name

    );

    // 当验证都通过后,把用户信息放在session里

    Session session = SecurityUtils.getSubject().getSession();

    session.setAttribute("userSession", user);

    session.setAttribute("userSessionId", user.getId());
    return authenticationInfo;

}

而我们在ShiroConfig中配置了凭证匹配器:

@Bean

public MyShiroRealm myShiroRealm(){

    MyShiroRealm myShiroRealm = new MyShiroRealm();

    myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());

    return myShiroRealm;

}

@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){

    HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();



    hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;

    hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));


    return hashedCredentialsMatcher;

}

所以在认证时的密码是加过密的,使用md5散发将密码与盐值组合加密两次。则我们在增加用户的时候,对用户的密码则要进过相同规则的加密才行。

添加用户代码如下:

@RequestMapping(value = “/add”)

public String add(User user) {

    User u = userService.selectByUsername(user.getUsername());

    if(u != null)

        return "error";

    try {

        user.setEnable(1);

        PasswordHelper passwordHelper = new PasswordHelper();

        passwordHelper.encryptPassword(user);

        userService.save(user);

        return "success";

    } catch (Exception e) {

        e.printStackTrace();

        return "fail";

    }

}

PasswordHelper:

package com.study.util;

import com.study.model.User;

import org.apache.shiro.crypto.RandomNumberGenerator;

import org.apache.shiro.crypto.SecureRandomNumberGenerator;

import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.util.ByteSource;

public class PasswordHelper {

//private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator();

private String algorithmName = "md5";

private int hashIterations = 2;



public void encryptPassword(User user) {

    //String salt=randomNumberGenerator.nextBytes().toHex();

    String newPassword = new SimpleHash(algorithmName, user.getPassword(),  ByteSource.Util.bytes(user.getUsername()), hashIterations).toHex();

    //String newPassword = new SimpleHash(algorithmName, user.getPassword()).toHex();

    user.setPassword(newPassword);



}

public static void main(String[] args) {

    PasswordHelper passwordHelper = new PasswordHelper();

    User user = new User();

    user.setUsername("admin");

        user.setPassword("admin");

    passwordHelper.encryptPassword(user);

    System.out.println(user);

}

}

授权:

接下来讲下授权。在自定义relalm中的代码为:

//授权

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

    User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}

    Map<String,Object> map = new HashMap<String,Object>();

    map.put("userid",user.getId());

    List<Resources> resourcesList = resourcesService.loadUserResources(map);
    // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    for(Resources resources: resourcesList){

        info.addStringPermission(resources.getResurl());

    }

    return info;

}

从以上代码中可以看出来,我根据用户id查询出用户的权限,放入SimpleAuthorizationInfo。关联表user_role,role_resources,resources,三张表,根据用户所拥有的角色,角色所拥有的权限,查询出分配给该用户的所有权限的url。当访问的链接中配置在shiro中时,或者使用shiro标签,shiro权限注解时,则会访问该方法,判断该用户是否拥有相应的权限。

在ShiroConfig中有如下代码:

@Bean

public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){

    System.out.println("ShiroConfiguration.shirFilter()");

    ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();



    // 必须设置 SecurityManager

    shiroFilterFactoryBean.setSecurityManager(securityManager);

    // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面

    shiroFilterFactoryBean.setLoginUrl("/login");

    // 登录成功后要跳转的链接

    shiroFilterFactoryBean.setSuccessUrl("/usersPage");

    //未授权界面;

    shiroFilterFactoryBean.setUnauthorizedUrl("/403");

    //拦截器.

    Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();


    //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了

    filterChainDefinitionMap.put("/logout", "logout");

    filterChainDefinitionMap.put("/css/**","anon");

    filterChainDefinitionMap.put("/js/**","anon");

    filterChainDefinitionMap.put("/img/**","anon");

    filterChainDefinitionMap.put("/font-awesome/**","anon");

    //<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;

    //<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->

    //自定义加载权限资源关系

    List<Resources> resourcesList = resourcesService.queryAll();
     for(Resources resources:resourcesList){



        if (StringUtil.isNotEmpty(resources.getResurl())) {

            String permission = "perms[" + resources.getResurl()+ "]";

            filterChainDefinitionMap.put(resources.getResurl(),permission);

        }

    }

    filterChainDefinitionMap.put("/**", "authc");




    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

    return shiroFilterFactoryBean;

}

该代码片段为配置shiro的过滤器。以上代码将静态文件设置为任何权限都可访问,然后

List resourcesList = resourcesService.queryAll();

     for(Resources resources:resourcesList){



        if (StringUtil.isNotEmpty(resources.getResurl())) {
            String permission = "perms[" + resources.getResurl()+ "]";

            filterChainDefinitionMap.put(resources.getResurl(),permission);

        }

    }

在数据中查询所有的资源,将该资源的url当作key,配置拥有该url权限的用户才可访问该url。

最后加入 filterChainDefinitionMap.put(“/*”, “authc”);表示其他没有配置的链接都需要认证才可访问。注意这个要放最后面,因为shiro的匹配是从上往下,如果匹配到就不继续匹配了,所以把 /放到最前面,则 后面的链接都无法匹配到了。

而这段代码是在项目启动的时候加载的。加载的数据是放到内存中的。但是当权限增加或者删除时,正常情况下不会重新启动来,重新加载权限。所以需要调用以下代码的updatePermission()方法来重新加载权限。其实下面的代码有些重复了,可以稍微调整下,我就先这么写了。

package com.study.shiro;

import com.github.pagehelper.util.StringUtil;

import com.study.model.Resources;

import com.study.model.User;

import com.study.service.ResourcesService;

import org.apache.shiro.SecurityUtils;

import org.apache.shiro.mgt.RealmSecurityManager;

import org.apache.shiro.session.Session;

import org.apache.shiro.spring.web.ShiroFilterFactoryBean;

import org.apache.shiro.subject.SimplePrincipalCollection;

import org.apache.shiro.subject.support.DefaultSubjectContext;

import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;

import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;

import org.apache.shiro.web.servlet.AbstractShiroFilter;
import org.crazycake.shiro.RedisSessionDAO;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import java.util.*;

/**

  • Created by yangqj on 2017/4/30.

*/

@Service

public class ShiroService {

@Autowired

private ShiroFilterFactoryBean shiroFilterFactoryBean;

@Autowired

private ResourcesService resourcesService;

@Autowired

private RedisSessionDAO redisSessionDAO;

/**

 * 初始化权限

 */

public Map<String, String> loadFilterChainDefinitions() {

    // 权限控制map.从数据库获取

    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();

    filterChainDefinitionMap.put("/logout", "logout");

    filterChainDefinitionMap.put("/css/**","anon");

    filterChainDefinitionMap.put("/js/**","anon");

    filterChainDefinitionMap.put("/img/**","anon");

    filterChainDefinitionMap.put("/font-awesome/**","anon");

    List<Resources> resourcesList = resourcesService.queryAll();

    for(Resources resources:resourcesList){



        if (StringUtil.isNotEmpty(resources.getResurl())) {

            String permission = "perms[" + resources.getResurl()+ "]";

            filterChainDefinitionMap.put(resources.getResurl(),permission);

        }

    }

    filterChainDefinitionMap.put("/**", "authc");

    return filterChainDefinitionMap;

}


/**
 * 重新加载权限
 */

public void updatePermission() {



    synchronized (shiroFilterFactoryBean) {



        AbstractShiroFilter shiroFilter = null;

        try {

            shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean

                    .getObject();

        } catch (Exception e) {

            throw new RuntimeException(

                    "get ShiroFilter from shiroFilterFactoryBean error!");

        }



        PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter

                .getFilterChainResolver();

        DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver

                .getFilterChainManager();



        // 清空老的权限控制

        manager.getFilterChains().clear();



        shiroFilterFactoryBean.getFilterChainDefinitionMap().clear();

        shiroFilterFactoryBean

                .setFilterChainDefinitionMap(loadFilterChainDefinitions());

        // 重新构建生成

        Map<String, String> chains = shiroFilterFactoryBean

                .getFilterChainDefinitionMap();

        for (Map.Entry<String, String> entry : chains.entrySet()) {

            String url = entry.getKey();

            String chainDefinition = entry.getValue().trim()

                    .replace(" ", "");

            manager.createChain(url, chainDefinition);

        }



        System.out.println("更新权限成功!!");

    }

}

}

会话管理

这个例子使用了redis保存session。这样可以实现集群的session共享。在ShiroConfig中有代码:

@Bean

public SecurityManager securityManager(){

    DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();

    //设置realm.

    securityManager.setRealm(myShiroRealm());

    // 自定义缓存实现 使用redis

    //securityManager.setCacheManager(cacheManager());

    // 自定义session管理 使用redis

    securityManager.setSessionManager(sessionManager());

    return securityManager;

}

配置了自定义session,网上已经有大神实现了 使用redis 自定义session管理,直接拿来用,引入包

<groupId>org.crazycake</groupId>

<artifactId>shiro-redis</artifactId>

<version>2.4.2.1-RELEASE</version>

然后再配置:

/**

 * 配置shiro redisManager

 * 使用的是shiro-redis开源插件

 * @return

 */

public RedisManager redisManager() {

    RedisManager redisManager = new RedisManager();

    redisManager.setHost(host);

    redisManager.setPort(port);

    redisManager.setExpire(1800);// 配置缓存过期时间

    redisManager.setTimeout(timeout);

    // redisManager.setPassword(password);

    return redisManager;

}


/**

 * cacheManager 缓存 redis实现

 * 使用的是shiro-redis开源插件

 * @return

 */

public RedisCacheManager cacheManager() {

    RedisCacheManager redisCacheManager = new RedisCacheManager();

    redisCacheManager.setRedisManager(redisManager());

    return redisCacheManager;

}




/**

 * RedisSessionDAO shiro sessionDao层的实现 通过redis

 * 使用的是shiro-redis开源插件

 */

@Bean

public RedisSessionDAO redisSessionDAO() {

    RedisSessionDAO redisSessionDAO = new RedisSessionDAO();

    redisSessionDAO.setRedisManager(redisManager());

    return redisSessionDAO;

}


/**

 * shiro session的管理

 */

@Bean

public DefaultWebSessionManager sessionManager() {

    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();

    sessionManager.setSessionDAO(redisSessionDAO());

    return sessionManager;

}

RedisConfig:

package com.study.config;

import org.apache.log4j.Logger;

import org.springframework.beans.factory.annotation.Value;

import org.springframework.cache.annotation.CachingConfigurerSupport;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import redis.clients.jedis.JedisPool;

import redis.clients.jedis.JedisPoolConfig;

/**

  • Created by yangqj on 2017/4/30.
    */
    @Configuration

@EnableCaching

public class RedisConfig extends CachingConfigurerSupport {
@Value("${spring.redis.host}")

private String host;



@Value("${spring.redis.port}")

private int port;



@Value("${spring.redis.timeout}")

private int timeout;


@Value("${spring.redis.pool.max-idle}")

private int maxIdle;



@Value("${spring.redis.pool.max-wait}")

private long maxWaitMillis;



@Bean

public JedisPool redisPoolFactory() {

    Logger.getLogger(getClass()).info("JedisPool注入成功!!");

    Logger.getLogger(getClass()).info("redis地址:" + host + ":" + port);

    JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();

    jedisPoolConfig.setMaxIdle(maxIdle);

    jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);


    JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);


    return jedisPool;

}

}

配置文件 application.properties中加入:

#redis

Redis服务器地址

spring.redis.host= localhost

Redis服务器连接端口

spring.redis.port= 6379

连接池中的最大空闲连接

spring.redis.pool.max-idle= 8

连接池中的最小空闲连接

spring.redis.pool.min-idle= 0

连接池最大连接数(使用负值表示没有限制)

spring.redis.pool.max-active= 8

连接池最大阻塞等待时间(使用负值表示没有限制)

spring.redis.pool.max-wait= -1

连接超时时间(毫秒)

spring.redis.timeout= 0

当然运行的时候要先启动redis。将自己的redis配置在以上配置中。这样session就存在redis中了。

上面ShiroConfig中的securityManager()方法中,我把

//securityManager.setCacheManager(cacheManager());

这行代码注了,是这样的,因为每次在需要验证的地方,比如在subject.hasRole(“admin”) 或 subject.isPermitted(“admin”)、@RequiresRoles(“admin”) 、 shiro:hasPermission=”/users/add”的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()。但是以为这些信息不是经常变的,所以有必要进行缓存。把这行代码的注释打开,的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()的返回结果会被redis缓存。但是这里稍微有个小问题,就是在刚修改用户的权限时,无法立即失效。本来我是使用了ShiroService中的clearUserAuthByUserId()想清除当前session存在的用户的权限缓存,但是没有效果。不知道什么原因。希望哪个大神看到后帮忙弄个解决方法。所以我干脆就把doGetAuthorizationInfo()的返回结果通过spring cache的方式加入缓存。

@Cacheable(cacheNames=“resources”,key="#map[‘userid’].toString()+#map[‘type’]")
public List loadUserResources(Map<String, Object> map) {

    return resourcesMapper.loadUserResources(map);

}

这样也可以实现,然后在修改权限时加上注解

@CacheEvict(cacheNames=“resources”, allEntries=true)

这样修改权限后可以立即生效。其实我感觉这样不好,因为清楚了我是清除了所有用户的权限缓存,其实只要修改当前session在线中被修改权限的用户就行了。 先这样吧,以后再研究下,修改得更好一点。

按钮控制

在前端页面,对按钮进行细粒度权限控制,只需要在按钮上加上shiro:hasPermission

新增

这里的参数就是我们在ShiroConfig-shirFilter()权限加载时的过滤器 中的value,也就是资源的url。

filterChainDefinitionMap.put(resources.getResurl(),permission);

8.效果图

image

image

9.运行、下载

下载项目后运行resources下的shiro.sql文件。需要运行redis后运行项目。访问http://localhost:8080/ 账号密码:admin admin 或user1 user1.新增的用户也可以登录。

github下载地址:https://github.com/lovelyCoder/springboot-shiro

转载请标明出处:http://blog.csdn.net/poorCoder_/article/details/71374002

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值