【shiro】初识与集成

个人学习笔记分享,当前能力有限,请勿贬低,菜鸟互学,大佬绕道

如有勘误,欢迎指出和讨论,本文后期也会进行修正和补充

前言

权限管理对于一个成熟的项目是极为基础且必要的部分,我们必须知道来者何人,才能判断这个人能做什么不能做什么

而shiro正是解决方案中最常见的一种
请留意,本文主要整理shiro相关思路,为方便理解,示例代码并不完整,更谈不上严谨
若需要实际使用的demo,请直接查看整理后的代码,完整demo会传到github或码云


1.介绍

  • shiro是Apache下的一个开源项目,提供认证、授权、加密和会话管理等功能
  • shiro属于轻量级框架,相当于SpringSecurity的精简版,更加轻便,当然内容也更少,不过足以应付大量场景

2.结构

shiro的三大核心组件为Subject、SecurityManager 和 Realm

2.1.Subject:认证主体

包括两个信息,便可辨认出需要认证的身份

  • Principals:身份。用于标识登录主体,通常为用户名、手机号等等

  • Credentials:凭证。用于验证主体身份,通常为密码、数字证书等等

2.2.SecurityManager:安全管理器

为shiro的核心,负责管理所有的subject和与之先关的交互操作

2.3.Realm:数据域

即数据来源,shiro会从这里回去安全数据,用于验证身份和分配权限,通常使用缓存或者数据库来实现(数据库和redis都是常见的实现方案)

可以看出,shiro并不提供和维护安全数据,仅仅是进行验证身份和分配权限,需要开发者去维护安全数据


3.细节功能

  • Authentication:身份认证/登录(账号密码验证)。

  • Session Manager:会话管理,用户登录后的session相关管理。

  • Cryptography:加密,密码加密等。

  • Web Support:Web支持,集成Web环境。

  • Caching:缓存,用户信息、角色、权限等缓存到如redis等缓存中。

  • Concurrency:多线程并发验证,在一个线程中开启另一个线程,可以把权限自动传播过去。

  • Testing:测试支持;

  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

  • Remember Me:记住我,登录后,下次再来的话不用登录了。


4.主要业务

4.1.拦截

拦截器会拦截请求,并根据配置,决定下一步业务是拒绝访问重定向进入控制层/业务层

4.2.登录

流程如下:

  • 控制层/业务层根据用户传递的数据创建token,即认证主体Subject
  • 控制层/业务层发起登录行为
  • shiro根据token中的数据,查询数据库账号相关数据,生成认证信息。没有数据则认证失败
  • shiro将认证信息认证主体进行匹配,不匹配则认证失败
  • shiro通过认证主体中的信息,查询数据库里权限数据,并将其写入数据域保存

换言之,通过用户传递的数据生成认证主体,再通过查询数据库生成认证信息,对两者进行匹配,成功就再查询权限信息保存,不匹配就再见

4.3.身份验证

流程如下:

  • 根据请求头中的token在数据域查询认证信息,未查询到则认证失败
  • 对认证信息中的角色、权限或其他自定义信息进行匹配,任意一个不符合则认证失败
  • 认证成功则进入控制层/业务层,且仍可通过代码控制进行身份验证和认证数据读取

也就是,通过token查询认证相关数据,没查到就是没认证


5.集成

5.1.添加依赖

 <dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.5.3</version>
 </dependency>

5.2.建立相关数据库表

此处建立三张表:用户表user_info、角色表role_info、权限表permission_info,并生成对应实体类

(为方便调试,仅生成实体类,并模拟查询过程)

  • UserInfo

    @Data
    @AllArgsConstructor
    public class UserInfo {
        /**
         * 主键id
         */
        private Integer id;
    
        /**
         * 用户名
         */
        private String username;
    
        /**
         * 密码
         */
        private String password;
    
        /**
         * 外键关联role表
         */
        private Integer roleId;
    }
    
  • RoleInfo

    @Data
    @AllArgsConstructor
    public class RoleInfo {
        /**
         * 主键id
         */
        private Integer id;
    
        /**
         * 角色名称
         */
        private String roleName;
    }
    
  • PermissionInfo

    @Data
    @AllArgsConstructor
    public class PermissionInfo {
        /**
         * 主键id
         */
        private Integer id;
    
        /**
         * 权限名
         */
        private String permissionName;
    
        /**
         * 外键关联role
         */
        private Integer roleId;
    }
    
  • 查询方法(模拟查询,方便调试)

        public UserInfo getUserInfo(String userName) {
            UserInfo userInfo;
            switch (userName) {
                case "user1":
                    userInfo = new UserInfo(1, "user1", "pwd1", 1);
                    break;
                case "user2":
                    userInfo = new UserInfo(2, "user2", "pwd2", 2);
                    break;
                case "user3":
                    userInfo = new UserInfo(3, "user3", "pwd3", 3);
                    break;
                default:
                    userInfo = null;
                    break;
            }
            return userInfo;
        }
    
        public List<PermissionInfo> getPermissionList(String userName) {
            List<PermissionInfo> list = new ArrayList<>();
            list.add(new PermissionInfo(1, "student", 1));
            list.add(new PermissionInfo(2, "teacher", 2));
            list.add(new PermissionInfo(3, "teacher", 3));
            list.add(new PermissionInfo(4, "student", 3));
    
            UserInfo userInfo = getUserInfo(userName);
            List<PermissionInfo> result = new ArrayList<>();
            for (PermissionInfo permissionInfo : list) {
                if (permissionInfo.getRoleId() == userInfo.getRoleId()) {
                    result.add(permissionInfo);
                }
            }
            return result;
        }
    

    如上述数据中

    • user1拥有student权限
    • user2拥有teacher权限
    • user3拥有两种权限student&teacher

5.3.自定义Realm

集成AuthorizingRealm类,重写两个方法

  • doGetAuthenticationInfo() 方法:用来验证当前登录的用户,获取认证信息。
  • doGetAuthorizationInfo() 方法:为当前登录成功的用户授予权限和分配角色。
package com.yezi_tool.basic_project.shiro;

import com.yezi_tool.basic_project.commons.model.UserRealmInfo;
import com.yezi_tool.basic_project.entity.PermissionInfo;
import com.yezi_tool.basic_project.entity.RoleInfo;
import com.yezi_tool.basic_project.entity.UserInfo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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 java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Echo_Ye
 * @title 自定义realm
 * @description 自定义数据域
 * @date 2020/8/17 9:25
 * @email echo_yezi@qq.com
 */
@Slf4j
public class MyRealm extends AuthorizingRealm {
    /**
     * 分配权限
     *
     * @param principalCollection 数据源
     * @return 权限和角色信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("-----------------访问授权---------------------");
        //认证信息未登录,可能是用户非正常退出
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            doClearCache(principalCollection);
            SecurityUtils.getSubject().logout();
            return null;
        }
        //获取认证主体
        String username = (String) principalCollection.getPrimaryPrincipal();
        if (username == null) {
            //认证错误
            return null;
        }
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<PermissionInfo> permissionInfoList = getPermissionList(username);
        Set<String> permissionSet = permissionInfoList.stream().map(m -> m.getPermissionName()).collect(Collectors.toSet());
        simpleAuthorizationInfo.setStringPermissions(permissionSet);
        return simpleAuthorizationInfo;
    }

    /**
     * 进行认证
     *
     * @param authenticationToken 数据源
     * @return 认证结果
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        // 根据 Token 获取数据
        String username = (String) authenticationToken.getPrincipal();
        // 根据用户名从数据库中查询该用户
        UserInfo user = getUserInfo(username);
        if (user != null) {
            // 把当前用户存到 Session 中
            SecurityUtils.getSubject().getSession().setAttribute("user", user);
            // 传入用户名和密码进行身份认证,并返回认证信息
            AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), this.getName());
            return authInfo;
        } else {
            //未查找到用户信息,认证失败
            return null;
        }
    }
}

5.4.配置shiro

package com.yezi_tool.basic_project.config;

import com.yezi_tool.basic_project.shiro.MyRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @author Echo_Ye
 * @title shiro配置
 * @description shiro相关配置
 * @date 2020/8/17 11:28
 * @email echo_yezi@qq.com
 */
@Slf4j
@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        // 定义bean
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        // 认证失败url
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 开放静态资源权限
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/assets/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        // 开放swagger页面权限
        filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        // 开放登陆接口
        filterChainDefinitionMap.put("/index", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        // 开放公共接口
        filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
        filterChainDefinitionMap.put("/sms/**", "anon");

        // 其他页面权限限制
        // 其余全部接口必须进行身份验证,此处方便测试限制到shiroTest开头
        filterChainDefinitionMap.put("/shiroTest/**", "authc");
        // "/teacher/"相关接口必须有teacher权限
        filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
        // "/student/"相关接口必须有student权限
        filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");

        //设置拦截器
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        log.info("-----------------shiro配置注入完成---------------------");
        return shiroFilterFactoryBean;
    }

    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm
        securityManager.setRealm(customRealm());
        return securityManager;
    }

    /**
     * 自定义身份认证 realm
     */
    @Bean
    public MyRealm customRealm() {
        return new MyRealm();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}

拦截器常用权限参数列表

  • anon:开放权限,可直接访问
  • authc:需要认证
  • logout:注销,执行后跳转至登录页面,即shiroFilterFactoryBean.setLoginUrl()所设置的页面
  • roles[role1, role2...]:所需角色。若有多个角色,则需满足所有角色才可放行。
  • perms[permission1, permission2...]:所需权限。若有多个权限,则需满足所有权限才可放行。


6.使用样例

仅供参考思路,正常使用请参考贴在后面的整理后的核心代码

6.1.登录接口(正常使用请勿返回账号数据)

package com.yezi_tool.basic_project.controller;

import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.entity.UserInfo;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @author Echo_Ye
 * @title 登录接口
 * @description 用于登录相关接口
 * @date 2020/8/17 9:39
 * @email echo_yezi@qq.com
 */
@Controller
@RequestMapping("/login")
public class LoginController {

    @Data
    public static class LoginRequest {
        private String username;
        private String password;
    }


    @PostMapping("/login")
    @ResponseBody
    public ReturnMsg login(@RequestBody LoginRequest loginRequest) {
        ReturnMsg returnMsg = new ReturnMsg();
        //组装token
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(loginRequest.username, loginRequest.password);
        try {
            //执行登录
            subject.login(token);
            //获取验证后的对象
            UserInfo userInfo = (UserInfo) subject.getPrincipal();
            //检查权限,无权限将会抛出异常
//            subject.checkPermission("teacher");
            returnMsg.setData(userInfo);
        } catch (UnknownAccountException e) {
            returnMsg = ReturnMsg.error("账号不存在");
        } catch (IncorrectCredentialsException e) {
            returnMsg = ReturnMsg.error("密码错误");
        } catch (LockedAccountException e) {
            returnMsg = ReturnMsg.error("账号被锁定");
        } catch (AuthenticationException e) {
            returnMsg = ReturnMsg.error("认证错误");
        } catch (UnauthorizedException e) {
            returnMsg = ReturnMsg.error("权限错误");
        }

        return returnMsg;
    }
}

6.2.测试权限接口

package com.yezi_tool.basic_project.controller;

import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * @author Echo_Ye
 * @title shiro测试接口
 * @description 用于shiro相关测试
 * @date 2020/8/17 11:54
 * @email echo_yezi@qq.com
 */
@Controller
@RequestMapping("/shiroTest")
public class ShiroTestController {

    @GetMapping("teacher/testShiro")
    @ResponseBody
    public ReturnMsg teacherTest(HttpServletRequest request) {
        return ReturnMsg.success();
    }

    @GetMapping("student/testShiro")
    @ResponseBody
    public ReturnMsg studentTest(HttpServletRequest request) {
        return ReturnMsg.success();
    }

    @GetMapping("checkPermission")
    @ResponseBody
    public ReturnMsg checkPermission(HttpServletRequest request, String permission) {
        SecurityUtils.getSubject().checkPermission(permission);
        return ReturnMsg.success();
    }
    
    @GetMapping("checkSuperAdmin")
    @ResponseBody
    @RequiresPermissions({"teacher","student"})
    public ReturnMsg checkSuperAdmin(HttpServletRequest request) {
        return ReturnMsg.success();
    }
}

6.3.进行测试

使用postman,每次使用同一个token进行测试,操作和结果如下

  • 发起请求/shiroTest/student/testShiro,结果为404的ModelAndView对象,提示/login.html不存在

    未进行登录,故跳转至登录页面,而测试项目并没有这个页面

    [图片上传失败...(image-668f54-1599041285905)]

  • 发起请求LoginController/login,结果为用户信息,即登录成功

    [图片上传失败...(image-d3481b-1599041285906)]

  • 再次发起请求/shiroTest/student/testShiro,结果为请求成功,则证明权限验证有效

    [图片上传失败...(image-70fa4-1599041285906)]

  • 发起请求/shiroTest/checkSuperAdmin,结果为操作失败,后端报错Subject does not have permission [teacher]

    该接口使用了注解,需要studentteacher两个权限,缺少任意一个则会抛出异常

    [图片上传失败...(image-4172e2-1599041285906)]

[图片上传失败...(image-3199e3-1599041285906)]

7.补充

7.1.SimpleAuthorizationInfo权限信息常用操作

  • addRole(String role)addRoles(Collection<String> roles)setRoles(Set<String> roles):添加/批量添加/批量设置角色
  • addStringPermission(String permission)addStringPermissions(Collection<String> permissions)setStringPermissions(Set<String> roles):添加/批量添加/批量设置权限

7.2.认证主体Subject常用操作

  • login(AuthenticationToken var1):登录,失败会抛出异常
  • logout():登出,注销认证信息
  • getPrincipal():获取认证身份信息,通常读取账号或者全部账号数据
  • getSession().setAttribute(Object var1, Object var2)getSession().getAttribute(Object var1):设置/获取session缓存数据
  • getSession().setTimeout(long var1)getSession().getTimeout():设置/获取session有限时限
  • isPermitted(String var1):是否满足权限,参数也可为Permission var1String... var1List<Permission> var1
  • isPermittedAll(String... var1):是否满足全部权限,参数也可为Collection<Permission> var1
  • checkPermission(String var1):检查权限,权限不足时抛出异常,参数也可为Permission var1String... var1Collection<Permission> var1
  • hasRole(String var1):是否满足角色,参数也可为List<String> var1
  • hasAllRoles(Collection<String> var1):是否满足全部角色
  • checkRole(String var1):检查角色,角色不足时抛出异常
  • checkRoles(String... var1):检查角色,角色不足时抛出异常,参数也可为Collection<String> var1
  • isRemembered():是否记住登录状态
  • isAuthenticated():是否已认证,没错,认证主体并不一定已被认证

7.3.拦截器常用权限参数列表(再重提一遍,很重要)

Filter 解释
anon 无参,开放权限,可以理解为匿名用户或游客
authc 无参,需要认证
logout 无参,注销,执行后会直接跳转到shiroFilterFactoryBean.setLoginUrl();设置的 url
authcBasic 无参,表示 httpBasic 认证
user 无参,表示必须存在用户,当登入操作时不做检查
ssl 无参,表示安全的URL请求,协议为 https
perms[user] 参数可写多个,表示需要某个或某些权限才能通过,多个参数时写 perms["user, admin"],当有多个参数时必须每个参数都通过才算通过
roles[admin] 参数可写多个,表示是某个或某些角色才能通过,多个参数时写 roles["admin,user"],当有多个参数时必须每个参数都通过才算通过
rest[user] 根据请求的方法,相当于 perms[user:method],其中 method 为 post,get,delete 等
port[8081] 当请求的URL端口不是8081时,跳转到schemal://serverName:8081?queryString 其中 schmal 是协议 http 或 https 等等,serverName 是你访问的 Host,8081 是 Port 端口,queryString 是你访问的 URL 里的 ? 后面的参数

此外,权限参数可以自定义,详情请自行查阅资料,或者等我啥时候有空了整理。。。

7.4.控制层注解

  • @RequiresAuthentication

    验证用户是否登录,等同于方法subject.isAuthenticated() 结果为true时。

  • @RequiresUser

    验证用户是否被记忆,user有两种含义:

    一种是成功登录的(subject.isAuthenticated() 结果为true);

    另外一种是被记忆的(subject.isRemembered()结果为true)。

  • @RequiresGuest

    验证是否是一个guest的请求,与@RequiresUser完全相反。

    换言之,RequiresUser == !RequiresGuest。

    此时subject.getPrincipal() 结果为null.

  • @RequiresRoles

    例如:

    • @RequiresRoles("roleA")
    • @RequiresRoles({"roleA", “roleB"})

    验证角色,任意一个不符合则抛出异常

  • @RequiresPermissions

    例如:

    • @RequiresPermissions("file")

    • @RequiresPermissions({"file:read", "file:write"} )

    验证权限,任意一个不符合则抛出异常

8.整理后代码

8.1.整理内容

  • 优化代码结构
  • 查询账号方法修正为从数据库查询(使用mybatisPlus)
  • 密码改为密文(MD5加密+盐)
  • 登录结果仅提示成功或失败
  • shiro相关异常使用全局异常捕获
  • 自定义token对象,并添加其余参数(如验证码,登录端),并进行验证
  • 通用方法写到BaseController
  • 增加多地登录踢下线机制
  • 缓存改用redis
  • 增加RememberMe机制

相关内容会另起他文整理,此处不做赘述,有兴趣的不妨瞅一下我更新没。。。

8.2.核心源码:

ShiroConfig.java

package com.yezi_tool.basic_project.config;

import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.shiro.KickoutSessionControlFilter;
import com.yezi_tool.basic_project.shiro.MyRealm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
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.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.springframework.context.annotation.DependsOn;

import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.Filter;

/**
 * @author Echo_Ye
 * @title shiro配置
 * @description shiro相关配置
 * @date 2020/8/17 11:28
 * @email echo_yezi@qq.com
 */
@Slf4j
@Configuration
public class ShiroConfig {    //获取application.properties参数,此处不能加static关键字
    @Value("${spring.redis.port}")
    private String port;

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

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

    /**
     * Shiro生命周期处理器
     */
    @Bean(name = "lifecycleBeanPostProcessor")
    public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
        log.info("-----------------Shiro生命周期周期处理器设置---------------------");
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions)
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 开启aop注解支持
     */
    @Bean
    @ConditionalOnMissingBean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        // 定义bean
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 登录url,默认会自动寻找Web工程根目录下的"/login.jsp"页面 或 "/login" 映射
        shiroFilterFactoryBean.setLoginUrl("/login.html");
        // 认证失败url
        shiroFilterFactoryBean.setUnauthorizedUrl("/notRole");
        // 登录成功url
//        shiroFilterFactoryBean.setSuccessUrl("/index");
        //当访问受限url
//        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");

        //自定义拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        //限制同一帐号同时在线的个数。
        filtersMap.put("kickout", kickoutSessionControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);

        // 设置拦截器
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 开放静态资源权限
        filterChainDefinitionMap.put("/webjars/**", "anon");
        filterChainDefinitionMap.put("/assets/**", "anon");
        filterChainDefinitionMap.put("/img/**", "anon");
        filterChainDefinitionMap.put("/js/**", "anon");
        filterChainDefinitionMap.put("/css/**", "anon");
        // 开放swagger页面权限
        filterChainDefinitionMap.put("/swagger-ui.html/**", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        // 开放登陆接口
        filterChainDefinitionMap.put("/index", "anon");
        filterChainDefinitionMap.put("/login/**", "anon");
        filterChainDefinitionMap.put("/logout", "logout");
        // 开放公共接口
        filterChainDefinitionMap.put("/mongodbFile/getImage", "anon");
        filterChainDefinitionMap.put("/sms/**", "anon");

        // 其他页面权限限制
        // 其余全部接口必须进行身份验证,且强制下线
        filterChainDefinitionMap.put("/**", "user,kickout");
        // "/teacher/"相关接口必须有teacher权限
        filterChainDefinitionMap.put("/shiroTest/teacher/**", "perms[teacher]");
        // "/student/"相关接口必须有student权限
        filterChainDefinitionMap.put("/shiroTest/student/**", "perms[student]");
        //设置拦截器
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        log.info("-----------------shiro配置注入完成---------------------");
        return shiroFilterFactoryBean;
    }


    /**
     * 注入 securityManager
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm
        securityManager.setRealm(customRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(redisCacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义rememberMe管理
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /**
     * 自定义身份认证 realm
     */
    @Bean
    public MyRealm customRealm() {
        MyRealm myRealm = new MyRealm();
        //设置加密方式
        myRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return myRealm;
    }

    /**
     * 加密策略
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
        //指定加密方式
        credentialsMatcher.setHashAlgorithmName(ConfigConstants.SHIRO_ENCODE_MODE);
        //加密次数
        credentialsMatcher.setHashIterations(ConfigConstants.SHIRO_ENCODE_TIMES);
        //此处的设置,true加密用的hex编码,false用的base64编码
        credentialsMatcher.setStoredCredentialsHexEncoded(true);
        return credentialsMatcher;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     */
    public RedisCacheManager redisCacheManager() {
        log.info("-----------------创建缓存管理器---------------------");

        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        //redis中针对不同用户缓存(此处的id需要对应user实体中的id字段,用于唯一标识)
        redisCacheManager.setPrincipalIdFieldName("id");
//        用户权限信息缓存时间
//        redisCacheManager.setExpire(200000);
        return redisCacheManager;
    }

    /**
     * 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.setRedisManager(redisManager());
        return redisSessionDAO;
    }


    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     */
    public RedisManager redisManager() {
        log.info("-----------------创建RedisManager,连接Redis..URL= " + host + ":" + port + "---------------------");
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host + ":" + port);//老版本是分别setHost和setPort,新版本只需要setHost就可以了
        if (!StringUtils.isEmpty(redisPassword)) {
            redisManager.setPassword(redisPassword);
        }
        return redisManager;
    }

    /**
     * 限制同一账号登录同时登录人数控制
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        kickoutSessionControlFilter.setCacheManager(redisCacheManager());
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        kickoutSessionControlFilter.setKickoutAfter(false);
        kickoutSessionControlFilter.setMaxSession(ConfigConstants.SHIRO_SESSION_KICKOUT_MAX_SESSION);
        kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
        return kickoutSessionControlFilter;
    }

    /**
     * Cookie
     */
    @Bean
    public SimpleCookie rememberMeCookie() {
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie(ConfigConstants.SHIRO_COOKIE_KEY_REMEMBER_ME);
        //如果httpOnly设置为true,则客户端不会暴露给客户端脚本代码,使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击;
        simpleCookie.setHttpOnly(true);
        //记住我cookie生效时间,单位是秒
        simpleCookie.setMaxAge(600);
        return simpleCookie;
    }

    /**
     * cookie管理器;
     */
    @Bean
    public CookieRememberMeManager rememberMeManager() {
        log.info("-----------------创建RememberMe管理器---------------------");
        CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
        //rememberme cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位)
        byte[] cipherKey = Base64.decode(ConfigConstants.SHIRO_COOKIE_CIPHER_KEY);
        cookieRememberMeManager.setCipherKey(cipherKey);
        cookieRememberMeManager.setCookie(rememberMeCookie());
        return cookieRememberMeManager;
    }

}

MyRealm.java

package com.yezi_tool.basic_project.shiro;

import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.entity.UserInfo;
import com.yezi_tool.basic_project.service.IUserInfoService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;

import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * @author Echo_Ye
 * @title 自定义realm
 * @description 自定义数据域
 * @date 2020/8/17 9:25
 * @email echo_yezi@qq.com
 */
@Slf4j
public class MyRealm extends AuthorizingRealm {

    /**
     * 用户信息业务层
     */
    @Autowired
    @Qualifier("userInfoService")
    private IUserInfoService userInfoService;

    /**
     * 分配权限
     *
     * @param principalCollection 数据源
     * @return 权限和角色信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        log.info("-----------------访问授权---------------------");
        //认证信息未登录,可能是用户非正常退出
        if (!SecurityUtils.getSubject().isAuthenticated()) {
            doClearCache(principalCollection);
            SecurityUtils.getSubject().logout();
            return null;
        }
        //获取认证主体
        UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
        if (userInfo == null) {
            //认证错误
            return null;
        }
        String username = userInfo.getUsername();
        //添加角色和权限
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        List<String> permissionInfoList = userInfoService.queryPermission(username);
        Set<String> permissionSet = permissionInfoList.stream().collect(Collectors.toSet());
        simpleAuthorizationInfo.setStringPermissions(permissionSet);

        // 存储信息到session,根据自身需求添加
        SecurityUtils.getSubject().getSession().setAttribute(ConfigConstants.SHIRO_SESSION_KEY_USER_INFO, userInfo);
        return simpleAuthorizationInfo;
    }

    /**
     * 进行认证
     *
     * @param authenticationToken 数据源
     * @return 认证结果
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) {
        //获取自定义token
        CustomAuthenticationToken token = (CustomAuthenticationToken) authenticationToken;
        // 根据 Token 获取数据
        String username = (String) token.getPrincipal();
        // 根据用户名从数据库中查询该用户
        UserInfo user = userInfoService.selectByUserName(username);
        if (user != null) {
            // 传入用户名和密码进行身份认证,并返回认证信息
            //自定义盐值
            ByteSource salt = ByteSource.Util.bytes(user.getSalt());
            AuthenticationInfo authInfo = new SimpleAuthenticationInfo(user, user.getPassword(), salt, this.getName());
            return authInfo;
        } else {
            //未查找到用户信息,认证失败
            return null;
        }
    }

}

ConfigConstants.java

package com.yezi_tool.basic_project.commons.constants;

/**
 * @author Echo_Ye
 * @title 配置信息常量
 * @description 用于各种配置
 * @date 2020/8/19 18:13
 * @email echo_yezi@qq.com
 */
public class ConfigConstants {
    /**
     * 序列化相关
     */
    public static final long SERIAL_VERSION_UID     = 1L;    //序列号

    /**
     * shiro相关
     */
    public static final String SHIRO_ENCODE_MODE                    = "MD5";    //加密方式
    public static final int SHIRO_ENCODE_TIMES                      = 1;    //加密次数
    public static final String SHIRO_REDIS_PREFIX_CACHE             = "shiro_redis_cache";    //缓存前缀
    public static final String SHIRO_SESSION_PREFIX_KICKOUT         = "kickout";    //缓存前缀
    public static final int SHIRO_SESSION_KICKOUT_MAX_SESSION       = 1;    //踢下线最多人数
    public static final String SHIRO_SESSION_KEY_USER_INFO          = "user_info";    //缓存-用户信息
    public static final String SHIRO_COOKIE_KEY_REMEMBER_ME         = "rememberMe";    //cookie-记住我
    public static final String SHIRO_COOKIE_CIPHER_KEY              = "qU7b1ChYNwqbrEwTlPbO9Q==";    //cookie-加密秘钥

    /**
     * redis相关
     */
    public static final String REDIS_KEY_CAPTCHA="captcha";
}

BaseExceptionHandler.java

package com.yezi_tool.basic_project.interceptors;

import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
import com.yezi_tool.basic_project.commons.exception.BaseException;
import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.commons.utils.ObjectMapperFactory;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.NestedServletException;

import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

/**
 * @title 统一异常处理
 * @description 统一处理项目内的异常
 * @author Echo_Ye
 * @date 2020/8/24 10:42
 * @email echo_yezi@qq.com
 */
@Slf4j
@ControllerAdvice
public class BaseExceptionHandler {
    /**
     * 统一异常处理,仅处理NestedServletException
     */
    @ExceptionHandler({NestedServletException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ModelAndView servletException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        ModelAndView mav = new ModelAndView();
        ReturnMsg message = ReturnMsg.FAIL;
        out(response, message);
        return mav;
    }


    /**
     * 统一异常处理,仅处理ShiroException
     */
    @ExceptionHandler({ShiroException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ReturnMsg shiroException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        // 打印异常信息至控制台,开始处理异常
        log.error("异常统一处理-ShiroException:" + exception.getLocalizedMessage(), exception);
        //异常默认为是操作失败
        ReturnMsg message = ReturnMsg.FAIL;
        //确认错误类型
        if (exception instanceof UnknownAccountException) {
            //账号错误
            message.setMsg(ResponseConstants.SHIRO_MSG_UNKNOWN_ACCOUNT);
        } else if (exception instanceof IncorrectCredentialsException) {
            //密码错误
            message.setMsg(ResponseConstants.SHIRO_MSG_INCORRECT_CREDENTIALS);
        } else if (exception instanceof LockedAccountException) {
            //账号被锁定
            message.setMsg(ResponseConstants.SHIRO_MSG_LOCKED_ACCOUNT);
        } else if (exception instanceof AuthenticationException) {
            //认证错误
            message.setMsg(ResponseConstants.SHIRO_MSG_AUTHENTICATION_ERROR);
        } else if (exception instanceof UnauthorizedException) {
            //权限不足
            message.setMsg(ResponseConstants.SHIRO_MSG_UNAUTHORIZED);
        }
        //返回消息体
        return message;
    }


    /**
     * 统一异常处理
     */
    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ReturnMsg processException(HttpServletRequest request, HttpServletResponse response, Exception exception) {
        // 打印异常信息至控制台,开始处理异常
        log.error("异常统一处理-Exception:" + exception.getLocalizedMessage(), exception);
        //异常默认为是操作失败
        ReturnMsg message = ReturnMsg.FAIL;
        // 检查异常的类型
        if (exception instanceof NestedServletException) {
            // 异步请求错误,已处理
        } else if (exception instanceof BaseException) {
            // 自定义类型的异常,转换为自定义异常
            message = ((BaseException) exception).asReturnMsg();
        } else {
            // 非自定义类型异常,打印错误信息至日志,封装ReturnMsg对象
            log.error(request.getRequestURI(), exception);
            message = ReturnMsg.FAIL;
        }
        //返回消息体
        return message;
    }

    /**
     * response 输出JSON
     */
    public static void out(ServletResponse response, ReturnMsg returnMsg) {
        PrintWriter out = null;
        try {
            response.setContentType("application/json;charset=utf-8");
            out = response.getWriter();
            out.println(ObjectMapperFactory.getInstance().writeValueAsString(returnMsg));
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (null != out) {
                out.flush();
                out.close();
            }
        }
    }
}

CustomAuthenticationToken.java

package com.yezi_tool.basic_project.shiro;

import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import lombok.Data;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;

/**
 * @author Echo_Ye
 * @title 自定义token
 * @description 自定义token,适用于shiro
 * @date 2020/8/20 11:56
 * @email echo_yezi@qq.com
 */
@Data
public class CustomAuthenticationToken extends UsernamePasswordToken {
    /**
     * 序列号
     */
    private static final long serialVersionUID = ConfigConstants.SERIAL_VERSION_UID;

    /**
     * 验证码字段
     */
    public static final String DEFAULT_CAPTCHA_PARAM = "captcha";

    /**
     * 登录类型
     */
    private int loginType;

    /**
     * 登录方式
     */
    private String loginMode;

    /**
     * 手机验证码
     */
    private String code;

    /**
     * 验证码
     */
    private String captcha;


    /**
     * 从request请求中获取验证码
     */
    public String getCaptcha(ServletRequest request) {
        return WebUtils.getCleanParam(request, DEFAULT_CAPTCHA_PARAM);
    }


    public CustomAuthenticationToken(String username, String password) {
        super(username, password);
    }

    public CustomAuthenticationToken(String username, String password, String captcha) {
        super(username, password);
        this.captcha = captcha;
    }
}

KickoutSessionControlFilter.java

package com.yezi_tool.basic_project.shiro;

import com.alibaba.fastjson.JSON;
import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.entity.UserInfo;
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 javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
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;

public class KickoutSessionControlFilter extends AccessControlFilter {

    private String kickoutUrl; //踢出后到的地址
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1
    private String kickoutKey = ConfigConstants.SHIRO_SESSION_PREFIX_KICKOUT;//提出字段名

    private SessionManager sessionManager;
    private Cache<String, Deque<Serializable>> cache;

    public void setKickoutUrl(String kickoutUrl) {
        this.kickoutUrl = kickoutUrl;
    }

    public void setKickoutAfter(boolean kickoutAfter) {
        this.kickoutAfter = kickoutAfter;
    }

    public void setMaxSession(int maxSession) {
        this.maxSession = maxSession;
    }

    public void setSessionManager(SessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    //设置Cache的key的前缀
    public void setCacheManager(CacheManager cacheManager) {
        this.cache = cacheManager.getCache(ConfigConstants.SHIRO_REDIS_PREFIX_CACHE);
    }

    @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();
        UserInfo user = (UserInfo) subject.getPrincipal();
        String username = user.getUsername();
        Serializable sessionId = session.getId();

        //读取缓存   没有就存入
        Deque<Serializable> deque = cache.get(username);

        //如果此用户没有session队列,也就是还没有登录过,缓存中没有
        //就new一个空队列,不然deque对象为空,会报空指针
        if (deque == null) {
            deque = new LinkedList<Serializable>();
        }

        //如果队列里没有此sessionId,且用户没有被踢出;放入队列
        if (!deque.contains(sessionId) && session.getAttribute(kickoutKey) == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }

        //如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if (kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下缓存队列
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下缓存队列
            }
            cache.put(username, deque);


            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute(kickoutKey, true);
                }
            } catch (Exception e) {//ignore exception
            }
        }

        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute(kickoutKey) != null) {
            //会话被踢出了
            try {
                //退出登录
                subject.logout();
            } catch (Exception e) { //ignore
            }
            saveRequest(request);

            Map<String, String> resultMap = new HashMap<String, String>();
            //判断是不是Ajax请求
            if ("XMLHttpRequest".equalsIgnoreCase(((HttpServletRequest) request).getHeader("X-Requested-With"))) {
                resultMap.put("code ", "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) {
            System.err.println("KickoutSessionFilter.class 输出JSON异常,可以忽略。");
        }
    }
}

BaseController.java

package com.yezi_tool.basic_project.controller;

import com.yezi_tool.basic_project.commons.constants.ConfigConstants;
import com.yezi_tool.basic_project.commons.constants.ResponseConstants;
import com.yezi_tool.basic_project.commons.exception.BaseException;
import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
import org.apache.shiro.SecurityUtils;
import org.springframework.stereotype.Controller;

@Controller
public class BaseController {

    /**
     * 校验验证码
     *
     * @param token token对象
     * @throws Exception 校验失败则抛出异常
     */
    public void checkCaptcha(CustomAuthenticationToken token) throws Exception {
        String captcha = token.getCaptcha();
        String exitCode = (String) SecurityUtils.getSubject().getSession().getAttribute(ConfigConstants.REDIS_KEY_CAPTCHA);
        if (null == captcha || !captcha.equalsIgnoreCase(exitCode)) {
            throw new BaseException(ResponseConstants.RETURN_MSG_INCORRECT_CAPTCHA);
        }
    }

    /**
     * 获取认证者身份
     *
     * @return 认证者身份信息
     */
    public Object getPrimaryPrincipal() {
        return SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal();
    }

    /**
     * 获取session字段值
     *
     * @param key session的字段key
     * @return session的字段值
     */
    public Object getSessionAttribute(String key) {
        return SecurityUtils.getSubject().getSession().getAttribute(key);
    }
}

LoginController.java

package com.yezi_tool.basic_project.controller;

import com.yezi_tool.basic_project.commons.model.ReturnMsg;
import com.yezi_tool.basic_project.shiro.CustomAuthenticationToken;
import lombok.Data;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

/**
 * @author Echo_Ye
 * @title 登录接口
 * @description 用于登录相关接口
 * @date 2020/8/17 9:39
 * @email echo_yezi@qq.com
 */
@Controller
@RequestMapping("/login")
public class LoginController extends BaseController {

    @Data
    private static class LoginRequest {
        private String username;
        private String password;
        private Boolean rememberMe;
    }


    @PostMapping("/login")
    @ResponseBody
    public ReturnMsg login(@RequestBody LoginRequest loginRequest) throws Exception {
        //组装token
        Subject subject = SecurityUtils.getSubject();
        CustomAuthenticationToken token = new CustomAuthenticationToken(loginRequest.username, loginRequest.password);
        // 设置rememberMe字段
        if(loginRequest.rememberMe!=null) {
            token.setRememberMe(loginRequest.rememberMe);
        }
        //判断验证码,暂不启用
//        checkCaptcha(token);
        //执行登录
        subject.login(token);

        return ReturnMsg.success();
    }

}

8.3.全部代码:

demo地址https://gitee.com/echo_ye/shiro-demo

demo已能正常运转预期所有功能,但仅供参考,请视实际业务自行删减和修改,有疑问或者建议可以留言或者联系我~

BB两句

真的是了解越多,越觉得其强大,越觉得自己弱鸡。。。


作者:Echo_Ye

WX:Echo_YeZ

EMAIL :echo_yezi@qq.com

个人站点:在搭了在搭了。。。(右键 - 新建文件夹)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容