本文參考文獻: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數據:
