SpringBoot 整合 Redis 实现 Shiro 权限控制的集群 Session 共享

本贴最后更新于 2184 天前,其中的信息可能已经东海扬尘

网上很多介绍 springboot+shiro+redis 解决 session 共享的文章,但大部分都是基于 springboot1.5 版本的,有些接口发生变化。
本篇文章主要记录在 SpringBoot2.x 的基础上整合 Shiro 和 Redis,实现权限控制与支持集群下的 Session 共享。

1、项目依赖

项目是基于 Maven 的,首先在 pom.xml 添加以下文件:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.0.5.RELEASE</version>
</parent>

<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  <java.version>1.8</java.version>
  <shiro-spring.version>1.4.0</shiro-spring.version>
  <shiro-redis.version>3.1.0</shiro-redis.version>
</properties>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</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-jdbc</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </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-thymeleaf</artifactId>
  </dependency>
  <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>${shiro-spring.version}</version>
  </dependency>
  <dependency>
    <groupId>org.crazycake</groupId>
    <artifactId>shiro-redis</artifactId>
    <version>${shiro-redis.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
  </dependency>
  <dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
  </dependency>
</dependencies>

利用第三方开源框架 shiro-redis 框架。

2、数据库设计

以下是数据库设计,是根据实体类自动生成,无需手动创建:
imagepng

在 application.properties 文件添加以下内容:

server.port=8090
###datasource
spring.datasource.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=15
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.jpa.hibernate.ddl-auto = update
spring.jpa.show-sql=true
#Redis 
spring.redis.host=10.1.44.13
spring.redis.port=6379
spring.redis.password=
spring.redis.jedis.pool.max-wait=61000ms
spring.redis.jedis.pool.max-active=1000
#session过期时间:秒
shiro.session.expire=1800

下面是实体类:

//用户实体
@Data
@Entity
@Table
public class SysUser implements Serializable {
  private static final long serialVersionUID = 1L;
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private int id;
  @Column(unique = true, length = 32)
  private String uid;// 用户id;
  @Column(unique = true, length = 30)
  private String username;// 登录账号.
  @Column(length = 30)
  private String name;// 名称
  @Column(length = 11)
  private String mobile;
  @Column(length = 32)
  private String password; // 密码;
  @Column(length = 32)
  private String salt;// 加密密码的盐
  @Column(columnDefinition="int default 1",length = 1)
  private int state;// 用户状态,0:创建未认证(比如没有激活,没有输入验证码等等)--等待验证的用户 , 1:正常状态,2:用户被锁定.
  @Transient
  private List roles;
  @Transient
  private List permissions;
  @CreatedDate
  private Date createTime;
  @LastModifiedDate
  private Date updateTime;
  /**
  * 密码盐.
  */
  public String getCredentialsSalt() {
    return this.username + this.salt;
  }
  public SysUser(String username, String name, String mobile, String password) {
    super();
    this.username = username;
    this.name = name;
    this.mobile = mobile;
    this.password = password;
  }
  public SysUser() {
    super();
  }
}

//角色实体
@Data
@Entity
@Table
public class SysRole implements Serializable{
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(unique=true, length=32)
private String roleCode;//程序中判断使用,如"admin",这个是唯一的
@Column(unique=true, length=30)
private String roleName; // 角色名字
@Column(length=200)
private String description; //角色描述,UI界面显示使用
private Boolean state = Boolean.TRUE; //是否可用,如果不可用将不会添加给用户
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
//权限实体
@Data
@Entity
@Table
public class SysPermission implements Serializable{
private static final long serialVersionUID = 960801694129036736L;
  @Id
  @GeneratedValue(strategy=GenerationType.IDENTITY)
  private int id;
  private String PermName;//名称.
  @Column(columnDefinition="enum('menu','button')",length=10)
  private String resourceType;//资源类型,[menu|button]
  @Column(length=200)
  private String url;//资源路径.
  @Column(length=100)
  private String icon;
  private int sort;
  @Column(unique=true,length=20)
  private String permission; //权限字符串,menu例子:role:*,button例子:role:create,role:update,role:delete,role:view
  private int parentId; //父编号
  private Boolean state = Boolean.TRUE;
  @CreatedDate
  private Date createTime;
  @LastModifiedDate
  private Date updateTime;
}
//用户角色关联实体
@Data
@Entity
@Table
public class SysUserRole implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
private String uid;
@Column(length=32)
private String roleCode;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}
//角色权限关联实体
@Data
@Entity
@Table
public class SysRolePermission implements Serializable {
private static final long serialVersionUID = 8949842330879809712L;
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private int id;
@Column(length=32)
private String roleCode;
private int permissionId;
@CreatedDate
private Date createTime;
@LastModifiedDate
private Date updateTime;
}

配置持久类

public interface SysUserDao extends CrudRepository{
  /**通过username查找用户信息;*/
  public SysUser findByUsername(String username);
}
public interface SysRoleDao extends JpaRepository,JpaSpecificationExecutor {

}
public interface SysPermissionDao extends JpaRepository,JpaSpecificationExecutor {

}

3、Shiro 配置

3.1 配置 ShiroConfig

import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
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.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.thymeleaf.util.StringUtils;


@Configuration 
public class ShiroConfig {

@Value("${spring.redis.host}")
private String redisHost;

@Value("${spring.redis.port}")
private int redisPort;

@Value("${spring.redis.password}")
private String redisPassword;

@Value("${shiro.session.expire}")
private int expire;

/**
* ShiroFilterFactoryBean 处理拦截资源文件问题。
*/
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){
  ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
  // 必须设置 SecurityManager  
  shiroFilterFactoryBean.setSecurityManager(securityManager);
  //拦截器.
  Map filterChainDefinitionMap = new LinkedHashMap();
  //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
  filterChainDefinitionMap.put("/logout", "logout");
  filterChainDefinitionMap.put("/js/**", "anon");
  filterChainDefinitionMap.put("/css/**", "anon");
  filterChainDefinitionMap.put("/images/**", "anon");
  //过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 
  filterChainDefinitionMap.put("/**", "authc");
  // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
  shiroFilterFactoryBean.setLoginUrl("/login");
  // 登录成功后要跳转的链接
  shiroFilterFactoryBean.setSuccessUrl("/index");
  //未授权界面;
  shiroFilterFactoryBean.setUnauthorizedUrl("/403");
  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;
}

/**
* 身份认证realm;
* (这个需要自己写,账号密码校验;权限等)
* @return
*/
@Bean
public ShiroRealm myShiroRealm(){
  ShiroRealm myShiroRealm = new ShiroRealm();
  myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());;
  return myShiroRealm;
}

/**
 * cacheManager 缓存 redis实现
 * 使用的是shiro-redis开源插件
 */
public RedisCacheManager cacheManager() {
  RedisCacheManager redisCacheManager = new RedisCacheManager();
  redisCacheManager.setRedisManager(redisManager());
  redisCacheManager.setExpire(expire);//缓存过期时间:秒
  return redisCacheManager;
}
    
 /**
 * 配置shiro redisManager
 * 使用的是shiro-redis开源插件
 */
public RedisManager redisManager() {
  RedisManager redisManager = new RedisManager();
  redisManager.setHost(redisHost);
  redisManager.setPort(redisPort);
  redisManager.setTimeout(30000);//连接redis超时
  if(!StringUtils._isEmpty_(redisPassword))
	redisManager.setPassword(redisPassword);
  return redisManager;
}

 /**
 * Session Manager
 * 使用的是shiro-redis开源插件
 */
 @Bean
 public DefaultWebSessionManager sessionManager() {
   DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
   sessionManager.setSessionDAO(redisSessionDAO());
   return sessionManager;
 }

 /**
 * RedisSessionDAO shiro sessionDao层的实现 通过redis
 * 使用的是shiro-redis开源插件
 */
 @Bean
 public RedisSessionDAO redisSessionDAO() {
   RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
   redisSessionDAO.setExpire(expire);//session会话过期时间,默认就是1800秒
   redisSessionDAO.setRedisManager(redisManager());
   return redisSessionDAO;
 }

/**
* 凭证匹配器
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
  HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
  hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
  hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));
  return hashedCredentialsMatcher;
}

/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
* @param securityManager
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
  AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
  authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
  return authorizationAttributeSourceAdvisor;
}
}

3.2 配置 ShiroRealm

import javax.annotation.Resource;
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 cn.grgpay.entity.SysPermission;
import cn.grgpay.entity.SysRole;
import cn.grgpay.entity.SysUser;
import cn.grgpay.service.SysUserService;

/**
 * 身份校验核心类;
 * @version v.0.1
 */
public class ShiroRealm extends AuthorizingRealm{
@Resource
private SysUserService userInfoService;
/**
* 认证信息.(身份验证) 
* :
* Authentication 是用来验证用户身份
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
  //获取用户的输入的账号.
  String username = (String)token.getPrincipal();
  //通过username从数据库中查找 User对象,如果找到,没找到.
  //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
  SysUser userInfo = userInfoService.findByUsername(username);
  if(userInfo == null){
	return null;
  }
  //加密方式;
  //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配,如果觉得人家的不好可以自定义实现
  SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
  userInfo, //用户名
  userInfo.getPassword(), //密码
 ByteSource.Util._bytes_(userInfo.getCredentialsSalt()),//salt=username+salt
 getName() //realm name
 );
  return authenticationInfo;
}

/**
* 此方法调用 hasRole,hasPermission的时候才会进行回调.
* 权限信息.(授权):
* 1、如果用户正常退出,缓存自动清空;
* 2、如果用户非正常退出,缓存自动清空;
* 3、如果我们修改了用户的权限,而用户不退出系统,修改的权限无法立即生效。
* (需要手动编程进行实现;放在service进行调用)
* 在权限修改后调用realm中的方法,realm已经由spring管理,所以从spring中获取realm实例,
* 调用clearCached方法;
* :Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
  SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
  SysUser userInfo = (SysUser)principals.getPrimaryPrincipal();
  for(SysRole role:userInfo.getRoles()){
    authorizationInfo.addRole(role.getRoleCode());
  }
  for(SysPermission p:userInfo.getPermissions()){
    authorizationInfo.addStringPermission(p.getPermission());
  }
  return authorizationInfo;
}
}

4、页面

imagepng

5、Controller

@Controller
public class HomeController {
  @RequestMapping({"/","/index"})
  public String index(){
    return "/index";
  }

  @RequestMapping(value = "/login", method = RequestMethod.GET)
  public String login() {
    return "login";
  }

// 登录提交地址和applicationontext-shiro.xml配置的loginurl一致。 (配置文件方式的说法)
  @RequestMapping(value="/login",method=RequestMethod.POST)
  public String login(HttpServletRequest request, Map map) throws Exception {
    System.out.println("HomeController.login()");
    // 登录失败从request中获取shiro处理的异常信息。
    // shiroLoginFailure:就是shiro异常类的全类名.
    String exception = (String) request.getAttribute("shiroLoginFailure");
    System.out.println("exception=" + exception);
    String msg = "";
    if (exception != null) {
      if (UnknownAccountException.class.getName().equals(exception)) {
        System.out.println("UnknownAccountException -- > 账号不存在:");
        msg = "UnknownAccountException -- > 账号不存在:";
      } else if (IncorrectCredentialsException.class.getName().equals(exception)) {
        System.out.println("IncorrectCredentialsException -- > 密码不正确:");  
        msg = "IncorrectCredentialsException -- > 密码不正确:";
      } else if("kaptchaValidateFailed".equals(exception)) {
        System.out.println("kaptchaValidateFailed -- > 验证码错误");
        msg = "kaptchaValidateFailed -- > 验证码错误";
      } else {
        msg = "else >> "+exception;
        System.out.println("else -- >" + exception);
      }
    }
  map.put("msg", msg);
  // 此方法不处理登录成功,由shiro进行处理.
  return "/login";
  }
}
@Controller
@RequestMapping("/user")
public class UserInfoController {
  @Value("${server.port}")
  private int port;
  @Autowired
  private SysUserService sysUserService;

  /**
  * 用户添加
  * **@return**
  */
  @PostMapping("/add")
  @RequiresPermissions("user:add")//权限管理;
  public String userInfoAdd(HttpServletRequest request,String username,String name,String mobile,String password){
    sysUserService.add(new SysUser(username, name, mobile, password));
    return "userInfoAdd";
  }

  /**
  * 用户删除
  * **@return**
  */
  @RequestMapping("/del")
  @RequiresPermissions("user:del")//权限管理;
  public String userDel(HttpServletRequest request){
    System.out.println(port+":从session拿出:"+request.getSession().getAttribute("calonUser"));
    return "userInfoDel";
  }
}

启动程序后表自动生成,然后执行以下 sql 语句,往数据库插入用户,角色,权限等初始化数据到数据库。


                
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    943 引用 • 1460 回帖 • 3 关注
  • Shiro
    20 引用 • 29 回帖
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 44 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...