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

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

网上很多介绍 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 语句,往数据库插入用户,角色,权限等初始化数据到数据库。

BEGIN; INSERT INTO `sys_permission` VALUES (1, NULL, NULL, '用户管理', NULL, 0, 'user', 'menu', 0, b'1', '/user'); INSERT INTO `sys_permission` VALUES (2, NULL, NULL, '用户列表', NULL, 1, 'user:view', 'menu', 1, b'1', '/user/list'); INSERT INTO `sys_permission` VALUES (3, NULL, NULL, '添加用户', NULL, 1, 'user:add', 'menu', 2, b'1', '/user/add'); INSERT INTO `sys_permission` VALUES (4, NULL, NULL, '删除用户', NULL, 1, 'user:del', 'menu', 3, b'1', '/user/del'); INSERT INTO `sys_permission` VALUES (5, NULL, NULL, '修改用户', NULL, 1, 'user:update', 'menu', 4, b'1', '/user/update'); COMMIT; BEGIN; INSERT INTO `sys_role` VALUES (1, NULL, NULL, NULL, '管理员', b'1', 'admin'); INSERT INTO `sys_role` VALUES (2, NULL, NULL, NULL, '经理', b'1', 'manager'); INSERT INTO `sys_role` VALUES (3, NULL, NULL, NULL, '职员', b'1', 'employee'); COMMIT; BEGIN; INSERT INTO `sys_role_permission` VALUES (1, NULL, NULL, 1, 'admin'); INSERT INTO `sys_role_permission` VALUES (2, NULL, NULL, 2, 'admin'); INSERT INTO `sys_role_permission` VALUES (3, NULL, NULL, 3, 'admin'); INSERT INTO `sys_role_permission` VALUES (4, NULL, NULL, 4, 'admin'); INSERT INTO `sys_role_permission` VALUES (5, NULL, NULL, 5, 'admin'); COMMIT; BEGIN; INSERT INTO `sys_user` VALUES (1, NULL, NULL, '管理员', 'd3c59d25033dbf980d29554025c23a75', '8d78869f470951332959580424d4bf4f', 1, 'admin', NULL, 'admin'); INSERT INTO `sys_user` VALUES (5, NULL, '13800138000', '测试人员', 'ba62e27376828480aeb0e320ade28310', '7FEF647A20958034', 0, 'B9FB25A2536EBE85D29EA00345B5E183', NULL, 'calon'); COMMIT; BEGIN; INSERT INTO `sys_user_role` VALUES (1, NULL, NULL, 'admin', 'admin'); COMMIT;

直接访问 http://localhost:8080/user/add
正常情况会跳转到登录页面。用户名:admin,密码:123456






扫一扫有惊喜: [![imagepng](http://itechor.top/solo/upload/bb791a58c3a84193b7f643b6849482c5_image.png) ](http://ym0214.com)
  • Spring

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

    949 引用 • 1460 回帖
  • Shiro
    20 引用 • 29 回帖
  • Redis

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

    286 引用 • 248 回帖

相关帖子

欢迎来到这里!

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

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