当前位置: 首页 > 工具软件 > Shiro-auth > 使用案例 >

SpringBoot集成shiro+shiro-redis实现登录、授权访问和多端登录控制

梁嘉澍
2023-12-01

目录

1、导入依赖

2、配置文件

2、配置shiro拦截器机制

4、定义session管理器MySessionManager

5、配置多设备同时登陆只保证一次有效(此处借鉴网上大佬们)

6、Shiro核心配置(部分配置注释参照博文大佬们信息)

7、跨域配置拦截器AuthenticationFilter

8、Shiro常用工具类封装ShiroUtil

9、登录请求

10、响应信息ObjectResponse

11、博文参考


 

1、导入依赖


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.7.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.crazycake/shiro-redis -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.8.24</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.67</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.1</version>
        </dependency>

2、配置文件

spring:
  redis:
    shiro:
      host: (redis连接地址)
      port: 6379
      password: (密码)
      timeout: 10000

#前后端分离页面地址
shiro:
  unauthorized:
    url: http://localhost:5003/index.html

2、配置shiro拦截器机制

/**
 * @Description //TODO Shiro拦截器机制
 * @Date $ $
 * @Author huangwb
 **/
@Slf4j
public class MyShiroRealm extends AuthorizingRealm {
    @Lazy
    @Autowired
    private ITSysUserInfoService itSysUserInfoService;
    @Lazy
    @Autowired
    private ITSysRoleService tSysRoleService;
    @Lazy
    @Autowired
    private TSysPermissionServiceCache tSysPermissionServiceCache;

    /**
     * @return org.apache.shiro.authz.AuthorizationInfo
     * @Author huangwb
     * @Description //TODO 登录之后 用户访问资源授权执行操作
     * @Date 2020/5/5 10:13
     * @Param [principals]
     **/
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 权限信息对象info,用来存放查出的用户的所有的角色(sysRole)及权限(permission)
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        TSysUserInfo user = ShiroUtils.getUserInfo();
        Set<String> permissionName = new HashSet<>();
        //通过用户id获取用户的角色
        List<TSysRole> sysRoles = tSysRoleService.getAllByUserId(user.getId());
        //定义角色名称集合
        Set<String> roleName = new HashSet<>();
        if (sysRoles != null && !sysRoles.isEmpty()) {
            sysRoles.stream().forEach(sysRole -> {
                //根据角色id获取该角色的所有权限
                List<String> permissions = tSysPermissionServiceCache.getCacheAndSet(sysRole.getId());
                if (permissions != null && !permissions.isEmpty()) {
                    permissionName.addAll(permissions);
                }
                roleName.add(sysRole.getRoleName());
            });
        }
        log.info("{}执行授权,获取到的权限为{}", user.getMobile(), JSON.toJSONString(permissionName));
        //添加进shiro的权限
        authorizationInfo.addStringPermissions(permissionName);
        //添加进shiro的角色
        authorizationInfo.setRoles(roleName);
        // 用户的角色集合
        return authorizationInfo;
    }

    /**
     * @return org.apache.shiro.authc.AuthenticationInfo
     * @Author huangwb
     * @Description //TODO 用户登录身份认证 验证用户输入账户密码是否正确
     * @Date 2020/5/5 10:14
     * @Param [token]
     **/
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        //获取用户的输入的账号.
        String username = (String) token.getPrincipal();
        //通过username从数据库中查找 User对象
        // 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        TSysUserInfo userInfo = itSysUserInfoService.findByUsername(username);
        if (userInfo == null) {
            return null;
        }
        if ("0".equals(userInfo.getStatus())) { //账户冻结
            throw new LockedAccountException();
        }
        String credentials = userInfo.getPassword();
        ByteSource credentialsSalt = ByteSource.Util.bytes(username);
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                //用户信息  可以存用户name 但你通过SecurityUtils.getSubject().getPrincipal()获取到的就是name
                // 如果这里你存入用户对象那你SecurityUtils.getSubject().getPrincipal()获取到的就是用户对象 如果是存入对象可能会造成获取用户信息脏读的问题。
                userInfo,
                credentials, //密码
                credentialsSalt,//密码盐
                //我设置getName()缓存中存的会一直是同一个key,
                //这样会导致后面一个登陆的人会把前面一个登陆的人授权的权限给覆盖掉 而用用户的唯一标识就不会出现这个问题
                userInfo.getMobile()  //realm name
        );
        return authenticationInfo;
    }
}

对于到底是用户对象还是用户名称作为Shiro的缓存可以看看这篇文章

https://blog.csdn.net/qq_34021712/article/details/84723583

4、定义session管理器MySessionManager

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.Serializable;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
/**
 * @Description //TODO 定义session管理器
 * @Date $ $
 * @Author huangwb
 **/
public class MySessionManager extends DefaultWebSessionManager {
    //前端传递sessionid参数名称
    private static final String AUTHORIZATION = "sessionId";

    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";

    public MySessionManager() {
        super();
    }

    @Override
    protected Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        //如果请求头中有 token 则其值为sessionId
        if (!StringUtils.isEmpty(id)) {
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return id;
        } else {
            //否则按默认规则从cookie取sessionId
            return super.getSessionId(request, response);
        }
    }
}

5、配置多设备同时登陆只保证一次有效(此处借鉴网上大佬们)

/**
 * @Description //TODO 唯一登录用户 多设备登录保证只有一个用户登录
 * @Date $ $
 * @Author huangwb
 **/
public class KickoutSessionControlFilter extends AccessControlFilter {
    private final Logger logger = LoggerFactory.getLogger(KickoutSessionControlFilter.class);
    private boolean kickoutAfter = false; //踢出之前登录的/之后登录的用户 默认踢出之前登录的用户
    private int maxSession = 1; //同一个帐号最大会话数 默认1

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

    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("shiro_redis_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();
        session.setTimeout(3600 * 2 * 1000);
        TSysUserInfo user = (TSysUserInfo) SecurityUtils.getSubject().getPrincipal();
        String username = user.getMobile();
        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("kickout") == null) {
            //将sessionId存入队列
            deque.push(sessionId);
            //将用户的sessionId队列缓存
            cache.put(username, deque);
        }
        //如果队列里的sessionId数超出最大会话数,开始踢人
        while (deque.size() > maxSession) {
            Serializable kickoutSessionId = null;
            if (kickoutAfter) { //如果踢出后者
                kickoutSessionId = deque.removeFirst();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            } else { //否则踢出前者
                kickoutSessionId = deque.removeLast();
                //踢出后再更新下缓存队列
                cache.put(username, deque);
            }
            try {
                //获取被踢出的sessionId的session对象
                Session kickoutSession = sessionManager.getSession(new DefaultSessionKey(kickoutSessionId));
                if (kickoutSession != null) {
                    //设置会话的kickout属性表示踢出了
                    kickoutSession.setAttribute("kickout", true);
                }
            } catch (Exception e) {//ignore exception
            }
        }
        //如果被踢出了,直接退出,重定向到踢出后的地址
        if (session.getAttribute("kickout") != null) {
            logger.info("------" + "踢出用户" + username + "登录sessionId=" + sessionId + "------");
            //会话被踢出了
            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"))) {
                /*throw new KickouException();*/
                ObjectResponse<String> objectResponse = new ObjectResponse<>();
                objectResponse.setStatus("100002");
                objectResponse.setMessage("您已经在其他地方登录,请重新登录。如有疑问请联系管理员!");
                //输出json串
                out(response, objectResponse);
            } else {
                ObjectResponse<String> objectResponse = new ObjectResponse<>();
                objectResponse.setStatus("100002");
                objectResponse.setMessage("您已经在其他地方登录,请重新登录。如有疑问请联系管理员!");
                out(response, objectResponse);
            }
        }
        return true;
    }

    private void out(ServletResponse hresponse, ObjectResponse<String> response) {
        try {
            hresponse.setContentType("text/json");
            //设置字符集为'UTF-8'
            hresponse.setCharacterEncoding("UTF-8");
            PrintWriter out = hresponse.getWriter();
            out.write(JSON.toJSONString(response, SerializerFeature.WriteMapNullValue));
            out.flush();
            out.close();
        } catch (Exception e) {
        }
    }
}

6、Shiro核心配置(部分配置注释参照博文大佬们信息)

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

/**
 * @Description //TODO Shiro配置文件类
 * @Date $ $
 * @Author huangwb
 **/
@Configuration
public class ShiroConfig {
    //redis地址
    @Value("${spring.redis.shiro.host}")
    private String host;
    //redis端口
    @Value("${spring.redis.shiro.port}")
    private int port;
    //redis连接超时时间
    @Value("${spring.redis.shiro.timeout}")
    private int timeout;
    //redis密码
    @Value("${spring.redis.shiro.password}")
    private String password;


    @Value("${shiro.unauthorized.url}")
    private String shiroUnauthorizedUrl;

    //设置session会话过期时间为两小时
    private static final Integer expireTime = 3600 * 2;

    @Bean
    public ShiroFilterFactoryBean shirFilter(DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        //配置多端唯一登录拦截器
        Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
        //为什么用自定义过滤器 (因为原先发现有时候代码一发布出现跨域的问题,可能是我配置有问题 然后就手动写了拦截器)
        filtersMap.put("authc", new AuthenticationFilter());
        filtersMap.put("kickout", kickoutSessionControlFilter());
        shiroFilterFactoryBean.setFilters(filtersMap);
        // authc:该过滤器下的页面必须登录后才能访问,它是Shiro内置的一个拦截器org.apache.shiro.web.filter.authc.FormAuthenticationFilter
        // anon: 可以理解为不拦截
        // user: 登录了就不拦截
        // roles["admin"] 用户拥有admin角色
        // perms["permission1"] 用户拥有permission1权限
        // filter顺序按照定义顺序匹配,匹配到就验证,验证完毕结束。
        // url匹配通配符支持:? * **,分别表示匹配1个,匹配0-n个(不含子路径),匹配下级所有路径
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        //注意过滤器配置顺序 不能颠倒
        //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl
        // 配置不会被拦截的链接 顺序判断,在 ShiroConfiguration 中的 shiroFilter 处配置了 /ajaxLogin=anon,意味着可以不需要认证也可以访问
        filterChainDefinitionMap.put("/sys/user/login", "anon");
        filterChainDefinitionMap.put("/sys/user/logOut", "anon");
        filterChainDefinitionMap.put("/sys/file/**", "anon");
        filterChainDefinitionMap.put("/sys/auth/**", "anon");
        filterChainDefinitionMap.put("/sys/user/group/privilege", "anon");
        filterChainDefinitionMap.put("/sys/sessionCheck/check", "anon");
        filterChainDefinitionMap.put("/**", "authc,kickout");
        //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
        shiroFilterFactoryBean.setLoginUrl(shiroUnauthorizedUrl);
        shiroFilterFactoryBean.setUnauthorizedUrl(shiroUnauthorizedUrl);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 凭证匹配器
     * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了
     * )
     *
     * @return
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(1024);//散列的次数,比如散列两次,相当于 md5(md5(""));
        return hashedCredentialsMatcher;
    }

    @Bean
    public MyShiroRealm myShiroRealm() {
        MyShiroRealm shiroRealm = new MyShiroRealm();
        shiroRealm.setCachingEnabled(true);
        //启用身份验证缓存,即缓存AuthenticationInfo信息,默认false 启用需开启remaberme
        // shiroRealm.setAuthenticationCachingEnabled(true);
        //缓存AuthenticationInfo信息的缓存名称 在ehcache-shiro.xml中有对应缓存的配置
        shiroRealm.setAuthenticationCacheName("authenticationCache");
        //启用授权缓存,即缓存AuthorizationInfo信息,默认false
        shiroRealm.setAuthorizationCachingEnabled(true);
        //缓存AuthorizationInfo信息的缓存名称  在ehcache-shiro.xml中有对应缓存的配置
        shiroRealm.setAuthorizationCacheName("authorizationCache");
        //配置自定义密码比较器
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }


    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        return securityManager;
    }

    //自定义sessionManager
    @Bean
    public SessionManager sessionManager() {
        MySessionManager mySessionManager = new MySessionManager();
        mySessionManager.setSessionDAO(redisSessionDAO());
        return mySessionManager;
    }

    /**
     * 配置shiro redisManager
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setExpire(expireTime);// 配置缓存过期时间
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        redisManager.setDatabase(5);
        return redisManager;
    }

    /**
     * cacheManager 缓存 redis实现
     * <p>
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setExpire(expireTime);
        return redisCacheManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * SessionDAO的作用是为Session提供CRUD并进行持久化的一个shiro组件
     * MemorySessionDAO 直接在内存中进行会话维护
     * EnterpriseCacheSessionDAO  提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setExpire(expireTime);
        return redisSessionDAO;
    }

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

    /**
     * @return
     * @描述:自动创建代理,没有这个鉴权可能会出错开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证 配置以下两个bean(DefaultAdvisorAutoProxyCreator和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * </br>Enable Shiro Annotations for Spring-configured beans. Only run after the lifecycleBeanProcessor(保证实现了Shiro内部lifecycle函数的bean执行) has run
     * </br>不使用注解的话,可以注释掉这两个配置
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }

    /**
     * 限制同一账号登录同时登录人数控制
     *
     * @return
     */
    @Bean
    public KickoutSessionControlFilter kickoutSessionControlFilter() {
        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
        kickoutSessionControlFilter.setCacheManager(cacheManager());
        kickoutSessionControlFilter.setSessionManager(sessionManager());
        kickoutSessionControlFilter.setKickoutAfter(false);
        kickoutSessionControlFilter.setMaxSession(1);
        return kickoutSessionControlFilter;
    }
}

7、跨域配置拦截器AuthenticationFilter

/**
 * @Description //TODO 跨域配置
 * @Date 2020/4/21 9:57
 * @Author huangwb
 **/
public class AuthenticationFilter extends FormAuthenticationFilter {
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        // 错误异常提示
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String sessionId = ((HttpServletRequest) request).getHeader("sessionId");
        if (sessionId == null) {
            setHeader(httpRequest, httpResponse);
            httpResponse.setCharacterEncoding("UTF-8");
            httpResponse.setContentType("application/json");
            httpResponse.getWriter().write(JSONObject.toJSONString(new ObjectResponse<String>().addError("请先登录!")));
            return false;
        } else {
            return true;
        }

    }

    /**
     * 为response设置header,实现跨域
     */
    private void setHeader(HttpServletRequest request, HttpServletResponse response) {
        //跨域的header设置
        response.setHeader("Access-control-Allow-Origin", request.getHeader("Origin"));
        response.setHeader("Access-Control-Allow-Methods", request.getMethod());
        response.setHeader("Access-Control-Allow-Credentials", "true");
        response.setHeader("Access-Control-Allow-Headers", request.getHeader("Access-Control-Request-Headers"));
        //防止乱码,适用于传输JSON数据
        //Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        response.setStatus(HttpStatus.OK.value());
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (request instanceof HttpServletRequest) {
            if (((HttpServletRequest) request).getMethod().toUpperCase().equals("OPTIONS")) {
                return true;
            }
        }
        return super.isAccessAllowed(request, response, mappedValue);
    }
}

8、Shiro常用工具类封装ShiroUtil

/**
 * @Description //TODO Shiro工具类
 * @Date $ $
 * @Author huangwb
 **/

public class ShiroUtils {
    /**
     * 私有构造器
     **/
    private ShiroUtils() {
    }

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

    /**
     * 获取当前用户Session
     *
     * @Return SysUserEntity 用户信息
     */
    public static Session getSession() {
        return SecurityUtils.getSubject().getSession();
    }

    /**
     * 用户登出
     */
    public static void logout() {
        SecurityUtils.getSubject().logout();
    }

    /**
     * 获取当前用户信息
     *
     * @Return SysUserEntity 用户信息
     */
    public static TSysUserInfo getUserInfo() {
        TSysUserInfo user = (TSysUserInfo) SecurityUtils.getSubject().getPrincipal();
        return user;
    }

    /**
     * 获取登录用户的id
     *
     * @return
     */
    public static String getLoginUserId() {
        return getUserInfo().getId();
    }

    /**
     * 获取登录用户的名称
     *
     * @return
     */
    public static String getLoginUserName() {
        return getUserInfo().getUserRealName();
    }

    /**
     * 删除用户缓存信息
     *
     * @Param username  用户名称
     * @Param isRemoveSession 是否删除Session,删除后用户需重新登录 如果为false代表只需要重新授权即可
     */
    public static void deleteCache(String username, boolean isRemoveSession) {
        //从缓存中获取Session
        Session session = null;
        // 获取当前已登录的用户session列表
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        Object attribute = null;
        // 遍历Session,找到该用户名称对应的Session
        for (Session sessionInfo : sessions) {
            attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            String name = ((SimplePrincipalCollection) attribute).getPrimaryPrincipal().toString();
            if (name == null) {
                continue;
            }
            if (Objects.equals(name, username)) {
                session = sessionInfo;
                // 清除该用户以前登录时保存的session,强制退出  -> 单用户登录处理
                if (isRemoveSession) {
                    redisSessionDAO.delete(session);
                }
            }
        }
        if (session == null || attribute == null) {
            return;
        }
        //删除session重新登录
        if (isRemoveSession) {
            redisSessionDAO.delete(session);
        }
        //删除Cache,再访问受限接口时会重新授权
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        Authenticator authc = securityManager.getAuthenticator();
        ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
    }

    /**
     * 从缓存中获取指定用户名的Session
     *
     * @param username
     */
    private static Session getSessionByUsername(String username) {
        // 获取当前已登录的用户session列表
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        TSysUserInfo user;
        Object attribute;
        // 遍历Session,找到该用户名称对应的Session
        for (Session session : sessions) {
            attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if (attribute == null) {
                continue;
            }
            user = (TSysUserInfo) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if (user == null) {
                continue;
            }
            if (Objects.equals(user.getMobile(), username)) {
                return session;
            }
        }
        return null;
    }

    /**
     * 获取加密后的密码
     *
     * @param principal   账户
     * @param credentials 密码
     * @return
     */
    public static String getPassword(Object principal, Object credentials) {
        String hashAlgorithmName = "md5";
        int hashIterations = 1024;
        ByteSource credentialsSalt = ByteSource.Util.bytes(principal);//账号
        String obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations).toHex();
        return obj;
    }


    /**
     * @param principal
     * @title 刷新用户权限 重新授权
     * @desc principal为用户的认证信息
     */
    public static void reloadAuthorizing(Object principal) throws Exception {
        RealmSecurityManager rsm = (RealmSecurityManager) SecurityUtils.getSecurityManager();
        MyShiroRealm myShiroRealm = (MyShiroRealm) rsm.getRealms().iterator().next();
        Subject subject = SecurityUtils.getSubject();
        if (subject != null) {
            String realmName = subject.getPrincipals().getRealmNames().iterator().next();
            SimplePrincipalCollection principals = new SimplePrincipalCollection(principal, realmName);
            subject.runAs(principals);
            if (myShiroRealm.isAuthenticationCachingEnabled()) {
                myShiroRealm.getAuthenticationCache().remove(principals);
            }
            if (myShiroRealm.isAuthorizationCachingEnabled()) {
                // 删除指定用户shiro权限
                myShiroRealm.getAuthorizationCache().remove(principals);
            }
            // 刷新权限
            subject.releaseRunAs();
        }
    }
}

9、登录请求

/**
     * 系统用户登录
     *
     * @param map
     * @param request
     * @return
     */
    @PostMapping("/login")
    public ObjectResponse<String> login(@RequestBody Map<String, String> map, HttpServletRequest request) {
        ObjectResponse<String> response = new ObjectResponse<>();
        Subject subject = SecurityUtils.getSubject();
        String mobile = map.get("mobile");
        if (Strings.isNullOrEmpty(mobile)) {
            response.setStatus(String.valueOf(RspEnumCode.LOGIN_USER_NAME_IS_NULL_OR_EMPTY.getCode()));
           response.setMessage(RspEnumCode.LOGIN_USER_NAME_IS_NULL_OR_EMPTY.getMessage());
            return response;
        }
        String password = map.get("password");
        if (Strings.isNullOrEmpty(password)) {
            response.setStatus(String.valueOf(RspEnumCode.LOGIN_USER_PASSWORD_IS_NULL_OR_EMPTY.getCode()));
            response.setMessage(RspEnumCode.LOGIN_USER_PASSWORD_IS_NULL_OR_EMPTY.getMessage());
            return response;
        }
       //是最常见的用户名/密码的认证机制;同时,由于它实现了RememberMeAuthenticationToken接口,我们可以通过令牌设置“记住我”的功能
        UsernamePasswordToken token = new UsernamePasswordToken(Base64Utils.decode(mobile), Base64Utils.decode(password));
        Map<String, Object> params = new HashMap<>();
        try {
            subject.login(token);
            TSysUserInfo sysUserInfo = ShiroUtils.getUserInfo();
           //刷新缓存中的用户权限 防止Shiro缓存机制导致 超管给用户重新赋予权限不生效的问题
            ShiroUtils.reloadAuthorizing(sysUserInfo.getMobile());
            params.put("sessionId", subject.getSession().getId());
            sysUserInfo.setPassword(null);
            sysUserInfo.setSalt(null);
            params.put("sysUserInfo", sysUserInfo);
            List<String> permissionsByUserId =         itSysPermissionService.getPermissionsByUserId(sysUserInfo.getId());
            if (permissionsByUserId != null && !permissionsByUserId.isEmpty()) {
                params.put("permissions", permissionsByUserId);
            } else {
                params.put("permissions", permissionsByUserId);
            }
            response.setMessage("登录成功");
        } catch (IncorrectCredentialsException e) {
            response.setMessage("账户密码错误");
            response.setStatus("100001");
            return response;
        } catch (LockedAccountException e) {
            response.setMessage("登录失败!请联系管理员");
            response.setStatus("100001");
            return response;
        } catch (AuthenticationException e) {
            response.setMessage("该用户不存在");
            response.setStatus("100001");
            return response;
        } catch (Exception e) {
            response.setMessage("系统异常");
            response.setStatus("100001");
            return response;
        }
        response.setData(Base64Utils.encode(JSON.toJSONString(params, SerializerFeature.WriteMapNullValue)));
        return response;
    }

10、响应信息ObjectResponse

/**
 * @Description //TODO
 * @Date $ $
 * @Author huangwb
 **/
@Getter
@Setter
public class BaseResponse {
    private String status = "200";

    private String message;

}
/**
 * @Description //TODO
 * @Date $ $
 * @Author huangwb
 **/
public class ObjectResponse<T> extends BaseResponse implements Serializable {
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public ObjectResponse<T> addError(String status, String message) {
        this.setStatus(status);
        this.setMessage(message);
        return this;
    }
    public ObjectResponse<T> addError( String message) {
        this.setStatus("100008");
        this.setMessage(message);
        return this;
    }
    public ObjectResponse<T> success(T data) {
        this.data = data;
        return this;
    }
}

11、博文参考

Shiro整合SpringBoot

https://blog.csdn.net/qq_34021712/article/details/80774649

UsernamePasswordToken说明

https://blog.csdn.net/qq_32567149/article/details/81098087

 类似资料: