本文参考文献:https://www.cnblogs.com/caichaoqi/p/8900677.html#4371099
本文作为个人学习笔记整理,避免后期找不到处理方案:
1、pom.xml文件
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- shiro+redis缓存插件 --> <dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version> </dependency> <!-- shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.4.0</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-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.0</version> </dependency> <!-- MybatisPlus依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.1.0</version> </dependency> <!--Druid--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.57</version> </dependency>
2、application.yml
server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource url: jdbc:mysql://127.0.0.1:3306/shiro2?serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver redis: database: 0 host: localhost port: 6379 timeout: 6000 jedis: pool: max-active: 1000 max-idle: 10 min-idle: 5 max-wait: -1 mybatis: mapper-locations: - classpath:SQL/**/*Mapper.xml type-aliases-package: xyz.ljcc.shiro.entity mybatis-plus: global-config: db-config: logic-not-delete-value: 0 logic-delete-value: 1 logging: level: xyz: ljcc: shiro: mapper: debug
3、实体类
@Data @NoArgsConstructor // 无参构造器 @AllArgsConstructor // 有参构造器 @TableName("sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) private Integer id; // 主键 @TableField(value = "login_name") private String loginName; // 登录名 @TableField(value = "pass_word") private String password; // 密码 private String salt; // 盐值 @TableLogic // 删除标记 private Integer enable; @TableField(exist = false) private Set<SysRole> roles = new HashSet<SysRole>(); public String getCredentialsSalt() { return this.loginName + this.salt; } }
@Data @NoArgsConstructor @AllArgsConstructor @TableName("sys_role") public class SysRole implements Serializable { private static final long serialVersionUID = 1L; @TableId(type = IdType.AUTO) private Integer id; @TableField(value = "role_name") private String roleName; @TableField(value = "role_desc") private String roleDesc; @TableLogic private Integer enable; @TableField(exist = false) private Set<SysResource> resources = new HashSet<SysResource>(); }
@Data @NoArgsConstructor @AllArgsConstructor @TableName("sys_resource") public class SysResource implements Serializable{ private static final long serialVersionUID = 1L; @TableId(type=IdType.AUTO) private Integer id; @TableField(value="rec_name") private String recName; @TableField(value="rec_url") private String recUrl; @TableField(value="rec_type") private String recType; @TableField(value="parent_id") private Integer parentId; @TableField(value="rec_sort") private Integer recSort; @TableLogic private Integer enable; }
4、Mapper层处理
public interface SysUserMapper extends BaseMapper<SysUser>{ // 获取用户具体信息 【包含:角色 + 资源】 @Select(" SELECT * FROM SYS_USER WHERE id = #{id} ") @Results(id="userRoleResourceMap",value= { @Result(column="id",property="id",id=true), @Result(column="login_name",property="loginName"), @Result(column="pass_word",property="password"), @Result(column="salt",property="salt"), @Result(column="enable",property="enable"), @Result(property="roles",javaType=Set.class,column="id", many=@Many(select="xyz.ljcc.shiro.mapper.SysRoleMapper.getRoleByUserId") ) }) SysUser findUserInfo(Integer id); }
public interface SysRoleMapper extends BaseMapper<SysRole> { @Results({ @Result(id=true,column="id",property="id"), @Result(column="role_name",property="roleName"), @Result(column="role_desc",property="roleDesc"), @Result(column="enable",property="enable"), @Result(property="resources",javaType=Set.class,column="id", many=@Many(select="xyz.ljcc.shiro.mapper.SysResourceMapper.getResourceByRoleId") ) }) @Select(" SELECT R.* FROM SYS_ROLE R,SYS_USER_ROLE UR WHERE R.id = UR.role_id " + " AND UR.user_id = #{userId} ") Set<SysRole> getRoleByUserId(Integer userId); }
public interface SysResourceMapper extends BaseMapper<SysResource> { @Select(" SELECT R.* FROM SYS_RESOURCE R,SYS_ROLE_RESOURCE UR WHERE R.id = UR.resource_id " + " AND UR.role_id = #{roleId} ") Set<SysResource> getResourceByRoleId(Integer roleId); }
5、Service层处理
public interface SysUserService { SysUser findByLoginName(String loginName); /** * 获取用户明细 * 包含:角色,资源 * @param userId * @return */ SysUser findUserInfo(Integer userId); }
@Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Override public SysUser findByLoginName(String loginName) { QueryWrapper<SysUser> queryWrapper = new QueryWrapper<SysUser>(); queryWrapper.eq("login_name", loginName); return baseMapper.selectOne(queryWrapper); } @Override public SysUser findUserInfo(Integer userId) { return baseMapper.findUserInfo(userId); } }
6、配置文件处理
6.1、Shiro配置
package xyz.ljcc.shiro.config; import java.util.LinkedHashMap; import java.util.Map; import javax.servlet.Filter; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.mgt.DefaultSecurityManager; 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.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import xyz.ljcc.shiro.filter.KickoutSessionControlFilter; import xyz.ljcc.shiro.realm.MyShiroRealm; @Configuration public class ShiroConfig { private final String loginUrl = "/auth/login"; private final String CACHE_KEY = "shiro:cache:"; private final String SESSION_KEY = "shiro:session:"; private final int EXPIRE = 1800; @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); // 设置登录页面跳转链接 shiroFilterFactoryBean.setLoginUrl(loginUrl); // 自定义拦截器 Map<String,Filter> filtersMap = new LinkedHashMap<String,Filter>(); // 限制同一账户同时在线个数。 filtersMap.put("kickout", kickoutSessionControlFilter()); shiroFilterFactoryBean.setFilters(filtersMap); // 权限控制 Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String, String>(); // 放开静态资源 filterChainDefinitionMap.put("/static/**", "anon"); // 放开登录请求、登录页面请求、验证码请求等 filterChainDefinitionMap.put("/auth/login", "kickout,anon"); // kickout,anon filterChainDefinitionMap.put("/auth/logout", "logout"); filterChainDefinitionMap.put("/auth/kickout", "anon"); //对所有用户认证 filterChainDefinitionMap.put("/**","kickout,user"); // authc,kickout shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } @Bean public SecurityManager securityManager() { DefaultSecurityManager securityManager = new DefaultWebSecurityManager(); // 设置Realm securityManager.setRealm(myShiroRealm()); // 设置自定义session管理器 -- 使用Redis securityManager.setSessionManager(sessionManager()); // 设置自定义缓存实现 -- 使用Redis securityManager.setCacheManager(cacheManager()); return securityManager; } @Bean public MyShiroRealm myShiroRealm() { MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } /** * HashedCredentialsMatcher 配置加密方式 * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // 配置散列算法 使用MD5加密 hashedCredentialsMatcher.setHashAlgorithmName("MD5"); // 设置散列次数 hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } /** * 配置Shiro redisManager * 使用的是shiro-redis开源插件 * @return */ @Bean public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setExpire(EXPIRE); // 设置过期时间 return redisManager; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * @return */ @Bean public RedisCacheManager cacheManager() { RedisCacheManager cacheManager = new RedisCacheManager(); cacheManager.setRedisManager(redisManager()); cacheManager.setKeyPrefix(CACHE_KEY); return cacheManager; } /** * Session Manager * 使用的是shiro-redis开源插件 * @return */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 * @return */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); redisSessionDAO.setKeyPrefix(SESSION_KEY); return redisSessionDAO; } /** * 限制同一账号登录同时登录人数控制 * @return */ @Bean public KickoutSessionControlFilter kickoutSessionControlFilter() { KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter(); kickoutSessionControlFilter.setCacheManager(cacheManager()); kickoutSessionControlFilter.setSessionManager(sessionManager()); kickoutSessionControlFilter.setKickoutAfter(false); kickoutSessionControlFilter.setMaxSession(1); kickoutSessionControlFilter.setKickoutUrl("/auth/kickout"); return kickoutSessionControlFilter; } /** * 授权所用配置 * @return */ @Bean public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * Shiro生命周期处理器 * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } }
6.2、MybatisPlus配置
package xyz.ljcc.shiro.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.baomidou.mybatisplus.core.injector.ISqlInjector; import com.baomidou.mybatisplus.extension.injector.LogicSqlInjector; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; @Configuration public class MybatisPlusConfig { @Bean public PaginationInterceptor paginationInterceptor() { PaginationInterceptor paginationInterceptor = new PaginationInterceptor(); // 设置方言 paginationInterceptor.setDialectType("mysql"); return paginationInterceptor; } /** * 3.1。1之后不需要配置 * @return */ @Bean public ISqlInjector sqlInjector() { return new LogicSqlInjector(); } }
6.3、全局异常处理
/** * 全局异常处理类 * * @author liucan * */ @ControllerAdvice @Slf4j public class CtrlExceptionHandler { // 拦截未授权页面 @ResponseStatus(value = HttpStatus.FORBIDDEN) @ExceptionHandler(UnauthorizedException.class) public String handleException(UnauthorizedException e) { log.debug(e.getMessage()); return "403"; } // 拦截未认证 @ResponseStatus(value = HttpStatus.FORBIDDEN) @ExceptionHandler(AuthorizationException.class) public String handleException2(AuthorizationException e) { log.debug(e.getMessage()); return "403"; } }
7、自定义Realm
package xyz.ljcc.shiro.realm; import java.util.Set; 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.authc.UsernamePasswordToken; 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.beans.factory.annotation.Autowired; import lombok.extern.slf4j.Slf4j; import xyz.ljcc.shiro.entity.SysResource; import xyz.ljcc.shiro.entity.SysRole; import xyz.ljcc.shiro.entity.SysUser; import xyz.ljcc.shiro.service.SysUserService; @Slf4j public class MyShiroRealm extends AuthorizingRealm{ @Autowired private SysUserService sysUserService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { log.info(" --------- 执行 Shiro授权 --------- "); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); SysUser userInfo = (SysUser) principals.getPrimaryPrincipal(); userInfo = sysUserService.findUserInfo(userInfo.getId()); Set<SysRole> roles = userInfo.getRoles(); for (SysRole sysRole : roles) { authorizationInfo.addRole(sysRole.getRoleName()); Set<SysResource> resources = sysRole.getResources(); for (SysResource sysResource : resources) { authorizationInfo.addStringPermission(sysResource.getRecUrl()); } } return authorizationInfo; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { log.info(" --------- 执行 Shiro认证 --------- "); UsernamePasswordToken token = (UsernamePasswordToken)authcToken; String loginName = token.getUsername(); // 获取数据库中对应的用户信息 SysUser userInfo = sysUserService.findByLoginName(loginName); if(userInfo == null) return null; SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( userInfo, userInfo.getPassword(), ByteSource.Util.bytes(userInfo.getCredentialsSalt()), this.getName() ); return authenticationInfo; } }
8、并发控制处理 -- 同一用户只允许在一处登录 -- 【拦截器】
package xyz.ljcc.shiro.filter; import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.util.Deque; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.session.Session; import org.apache.shiro.session.mgt.DefaultSessionKey; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; import org.apache.shiro.web.util.WebUtils; import com.alibaba.fastjson.JSON; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import xyz.ljcc.shiro.entity.SysUser; /** * 限制并发人数登录 * * @author liucan * */ @Setter @Slf4j public class KickoutSessionControlFilter extends AccessControlFilter { private String kickoutUrl; // 踢出后到的地址 private boolean kickoutAfter = false; // 踢出之前登录的/之后登录的用户 默认踢出之前登录的用户 private int maxSession = 1; // 同一个帐号最大会话数 默认1 private final String CACHE_KEY = "shiro:cache:"; private SessionManager sessionManager; private Cache<String, Deque<Serializable>> cache; // 设置Cache的key的前缀 public void setCacheManager(CacheManager cacheManager) { this.cache = cacheManager.getCache(CACHE_KEY); } @Override protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { return false; } @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { Subject subject = getSubject(request, response); if (!subject.isAuthenticated() && !subject.isRemembered()) { // 如果没有登录,直接进行之后的流程 return true; } Session session = subject.getSession(); String loginName = ((SysUser) subject.getPrincipal()).getLoginName(); Serializable sessionId = session.getId(); // 读取缓存 没有就存入 Deque<Serializable> deque = cache.get(loginName); // 如果此用户没有session队列,也就是还没有登录过,缓存中没有 // 就new一个空队列,不然deque对象为空,会报空指针 if (deque == null) { deque = new LinkedList<Serializable>(); } // 如果队列里没有此sessionId,且用户没有被踢出;放入队列 if (!deque.contains(sessionId) && session.getAttribute("kickout") == null) { // 将sessionId存入队列 deque.push(sessionId); cache.put(loginName, deque); } // 如果队列里的sessionId数超出最大会话数,开始踢人 while (deque.size() > maxSession) { Serializable kickoutSessionId = null; if (kickoutAfter) { // 如果踢出后者 kickoutSessionId = deque.removeFirst(); } else { // 否则踢出前者 kickoutSessionId = deque.removeLast(); } cache.put(loginName, deque); try { // 获取被踢出的sessionId的session对象 Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId)); if (kickoutSession != null) { // 设置会话的kickout属性表示踢出了 kickoutSession.setAttribute("kickout", true); } } catch (Exception e) { } } // 如果被踢出了,直接退出,重定向到踢出后的地址 if (session.getAttribute("kickout") != null) { // 会话被踢出了 try { // 退出登录 subject.logout(); } catch (Exception e) { } saveRequest(request); Map<String, String> resultMap = new HashMap<String, String>(); // 判断是不是Ajax请求 if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) { resultMap.put("user_status", "300"); resultMap.put("message", "您已经在其他地方登录,请重新登录!"); // 输出json串 out(response, resultMap); } else { // 重定向 WebUtils.issueRedirect(request, response, kickoutUrl); } return false; } return true; } private void out(ServletResponse hresponse, Map<String, String> resultMap) throws IOException { try { hresponse.setCharacterEncoding("UTF-8"); PrintWriter out = hresponse.getWriter(); out.println(JSON.toJSONString(resultMap)); out.flush(); out.close(); } catch (Exception e) { log.error("KickoutSessionFilter.class 输出JSON异常,可以忽略。"); } } }
8、Controller层业务
8.1、登录处理
package xyz.ljcc.shiro.controller; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import com.alibaba.druid.util.StringUtils; import lombok.extern.slf4j.Slf4j; import xyz.ljcc.shiro.entity.SysUser; import xyz.ljcc.shiro.utils.RequestUtils; @Controller @RequestMapping("/auth") @Slf4j public class LoginController { @PostMapping("/login") public String doLogin(String username,String password,Model model) { try { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); subject.login(token); SysUser userInfo = (SysUser) subject.getPrincipal(); log.info(userInfo.toString()); } catch (Exception e) { model.addAttribute("msg", "用户名或密码错误"); return "login"; } return "redirect:/auth/index"; } @GetMapping("/login") public String login() { return "login"; } @GetMapping("/index") public String loginSuccessMessage(Model model) { String username = "未登录"; SysUser user = RequestUtils.currentLoginUser(); if(user != null && !StringUtils.isEmpty(user.getLoginName())) { username = user.getLoginName(); } else { return "redirect:/auth/login"; } model.addAttribute("username", username); return "index"; } // 被踢出后跳转的页面 @GetMapping("/kickout") public String kickOut() { return "kickout"; } }
8.2、访问权限测试的链接
@RestController public class TestController { @GetMapping("/user/list") public String userList() { return "user-list ...."; } @RequiresPermissions(value= {"/user/del"}) @GetMapping("/user/del") public String userDel() { return "user-del ...."; } @GetMapping("/role/list") public String roleList() { return "role-list ...."; } }
9、工具类部分:
9.1、加密
/** * Shiro密码生产工具 * @author liucan * */ public class ShiroPWDUtil { /** * 获取随机盐值 与 MD5加密密码 * @param loginName * @param pswd * @return String[] 长度为2 [0]-salt [1]-pswd */ public static String[] getMD5SaltVsPswd(String loginName,String pswd) { String[] strs = new String[2]; String salt = UUID.randomUUID().toString().substring(0, 8); strs[0] = salt; int iterations = 2; // 加密次数 与 ShiroConfig里面的 HashedCredentialsMatcher进行对应 Object result = new SimpleHash("MD5", pswd, ByteSource.Util.bytes(loginName + salt), // 添加验证 iterations); strs[1] = result.toString(); return strs; } }
9.2、Shiro工具
public class RequestUtils { /** * 获取当前登录的用户,若用户未登录,则返回未登录 json * @return */ public static SysUser currentLoginUser() { Subject subject = SecurityUtils.getSubject(); if (subject.isAuthenticated()) { Object principal = subject.getPrincipals().getPrimaryPrincipal(); if (principal instanceof SysUser) { return (SysUser) principal; } } return null; } }
10、页面部分
1、index.html
2、login.html
3、403.html
4、kickout.html
11、测试
使用两个不同的浏览器:为了SeesionId不一致
http://localhost:8080/auth/index -- 会自动返回登录页面
1、在浏览器1中输入用户名密码进行登录
2、在浏览器2中输入用户名密码进行登录
3、刷新浏览器1的index页面
会提示被踢出:
Redis数据: