Bootstrap

一个基于 Spring boot + shiro + redis mybatis 的单点登录starter

项目链接:
https://gitee.com/jlxxw/edits-shiro-spring-boot-starter

这是我个人参考 https://gitee.com/liselotte/spring-boot-shiro-demo 进行修改,简单的封装一个基于shiro的单点登录starter
我个人不反对你们转发、复制、包括下载以及修改源码等等操作,如果你觉得有用,点个赞,如果你觉得我写的很差,请轻点喷我,毕竟我还是一个菜鸡。。。。
注意事项:
1、支持微服务模块
2、此项目未上传至中央仓库,如有需要,可自行下载,打包成jar,上传至个人私服即可,例如:阿里云的云效私服(云效是免费的)
3、项目默认使用druid数据源
4、默认使用JedisPool 连接池
5、关于 sys_user 表,此表仅存储了用户必要的登录信息,用户名和密码,但此表有自增主键 user_id,如果您的业务有需要,可以将用户的基本信息表中新增一个user_id字段,与之关联即可

使用方法
1、引入依赖(可以自行 mav clean install
2、实现need.implement包内的接口
3、配置Controller相关鉴权注解即可

一:使用须知:
1、配置项目数据源
2、配置项目redis

二:简述说明: 1、引入此依赖会自动创建数据库表,建表SQL如下:

        create table  if not exists sys_menu(
          menu_id     bigint auto_increment comment '权限ID' primary key,
          parent_id   bigint null comment '父级菜单ID',
          name  varchar(50) null comment '权限名称',
          perms varchar(500) null comment '权限标识',
          url varchar(256) null comment 'url地址',
          create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
          icon varchar(256) null comment '图标',
          remark    varchar(256) null comment '备注信息',
        ) comment '权限表' charset = utf8;
        
         create table  if not exists sys_role
        (
          role_id   bigint auto_increment comment '角色ID' primary key,
          role_name varchar(50) not null comment '角色名称',
          remark    varchar(256) null comment '备注信息',
          create_time  datetime default CURRENT_TIMESTAMP null comment '创建时间'
        ) comment '角色表' charset = utf8;
        
         create table  if not exists sys_role_menu
        (
          id      bigint auto_increment comment 'ID' primary key,
          role_id bigint null comment '角色ID',
          menu_id bigint null comment '权限ID'
        ) comment '角色与权限关系表' charset = utf8;
        
        create table  if not exists sys_user
        (
          user_id     bigint auto_increment comment '用户ID' primary key,
          username    varchar(50) not null comment '用户名',
          password    varchar(100)  null comment '密码',
          salt        varchar(50)   null comment '盐值',
          status      varchar(50) null comment '状态:NORMAL正常  PROHIBIT禁用',
          create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',
          create_user varchar(256) null comment '创建该账号的操作者',
          modify_time datetime default CURRENT_TIMESTAMP null comment '修改时间',
          modify_user varchar(256) null comment '修改该账户的操作者',
          remark      varchar(256)  null comment '备注信息'
        ) comment '系统用户表' charset = utf8;
        
         create table if not exists sys_user_role
        (
          id      bigint auto_increment comment 'ID' primary key,
          user_id bigint null comment '用户ID',
          role_id bigint null comment '角色ID'
        ) comment '用户与角色关系表' charset = utf8;

2、实现org.edith.shiro.need.implement.UrlFilterInterface接口,重构方法,方法返回值可选,并添加@Service注解
/**
* 需要配置的过滤器URL信息
*/
public interface UrlFilterInterface {

    /**
     * 允许匿名访问URL集合,比如css等
     * @return
     */
    Set<String> getAnonUrlSet();

    /**
     * 需要鉴权操作的URL集合
     * @return
     */
    Set<String> getAuthcUrlSet();

    /**
     * 自定义的鉴权过滤器
     * @return
     */
    Set<Filter> getAuthcFilters();

    /**
     * 自定义的匿名访问过滤器
     * @return
     */
    Set<Filter> getAnonFilters();
}
自定义用户状态处理逻辑:
(可选)实现 org.edith.shiro.need.implement.UserStatusHandler 接口,并添加@Service注解,实现自定义的用户状态处理逻辑
建议使用方法:实现此接口后,可以通过全局异常处理,来处理用户状态

3、鉴权操作,
推荐在controller的@RequestMapping 方法标注上 添加@RequiresPermissions(“此处值对应sys_menu表的perms字段值”)
shiro鉴权操作可参考:https://www.cnblogs.com/limingxian537423/p/7928234.html
四:注意事项:

1、为了安全起见,starter中没有预留各个数据表的insert接口,仅留有login/logout接口
2、如果需要更改sys_user表,建议先引入该starter,启动一次项目,start会自动创建数据库表,而后在进行修改数据库表即可
3、可以扩展自己的用户业务数据,将自己用户表与sys_user表做一个关联关系即可
4、sys_user表保存数据时,需要添加盐值,可使用org.edith.shiro.util.UserPasswordUtils工具进行添加,盐值位20长度的随机字符串
5、sys_user表中,用户状态值有两个分别是 NORMAL(正常)、PROHIBIT(禁止)

五:接口文档: 基础返回值格式:

                {
                    "success":true/false,
                    "code":"错误码",
                    "message":"相关文本信息",
                    "data":"返回值具体数据内容JSON格式"
                }

1、登录接口:

        请求方式:POST
        url:/user/login
        contentType:application/json
        import:{"username":"登录用户名","password":"登录用户密码"}
        output:{
                    "success":true,
                    "code":"SUCCESS",
                    "message":"操作成功",
                    "data":null
                }

2、注销接口:(注意:此接口需要携带token参数,否则不会完成注销功能)

        请求方式:POST
        url:/user/logout
        contentType:application/json
        import:{无任何参数}
        output:{
                    "success":true,
                    "code":"SUCCESS",
                    "message":"操作成功",
                    data":{"token":"token值"} 
                }

六:返回值枚举信息 枚举类:org.edith.shiro.enums.ResponseEnum

{"code":"SUCCESS","message":"操作成功"}

{"code":"FAILURE","message":"操作失败"}

{"code":"INCORRECT_USERNAME_OR_PASSWORD","message":"用户名或密码输入错误"}

{"code":"NO_ACCESS","message":"无访问权限"}

{"code":"NO_LOGIN","message":"用户未登录"}

{"code":"ACCOUNT_LOCKED","message":"账户被锁定"}

{"code":"SYS_ERROR","message":"系统内部错误"}

七:请求头设置

 key:Authorization value:token值

八、获取当前登录用户信息

可通过工具类 org.edith.shiro.util.ShiroUtils 调用 getUserInfo() 方法进行获取
示例:SysUserDO user = ShiroUtils.getUserInfo();

九、代码讲解
工程结构如下
在这里插入图片描述
工程结构说明:

资源文件说明:
resources/MEATAiINF/spring.factories : Spring spi 机制必备文件,用于指定自动装配类

自动装配:
AutoConfig类:自动装配类,偷个懒,直接用的@ComponentScan ,没有写条件注解

源码包:
advice:
	GlobalExceptionHandler:统一异常处理,用于处理框架内部的 shiro 相关异常,AOP优先级配置为 @Order(0)
component :
	ShiroRealm :shiro的领域对象,含鉴权(doGetAuthorizationInfo)、登录认证逻辑(doGetAuthenticationInfo)
	ShiroSessionIdGenerator:自定义token生成器,没有使用JWT,仅使用UUID作为内容
		token格式如下:login_token_UUID内容_ip地址加密后的MD5值
	ShiroSessionManager:定义token的获取方式,指定从请求头进行获取
config:
	RedisDAOConfig:配置RedisSessionDAO 相关信息,以及Jredis连接池等信息
	ShiroConfig:shiro基础配置信息
constant:
	HttpConstant:请求头等常量信息
	RedisConstant:redis相关常量信息
	SHA256Constant:加密算法相关常量信息
	ShiroConstant:shiro常量信息
context:
	WebContext:当前线程web上下文环境,使用的类为 ThreadLocal
controller:
	UserLoginController:基础登录、注销接口
dao:
	SysMenuDAO:菜单表(权限表)相关持久层操作
	SysRoleDAO:用户角色表相关持久层操作
	SysRoleMenuDAO:角色与菜单的关联表相关持久层操作
	SysUserDAO:用户基础信息表相关操作
	SysUserRoleDAO:用户与角色关联表持久层操作
dataobject:
	SysMenuDO:菜单表(权限表)持久层对象
	SysRoleDO:用户角色表持久层对象
	SysRoleMenuDO:角色与菜单的关联表持久层对象
	SysUserDO:用户基础信息表持久层对象
	SysUserRoleDO:用户与角色关联表持久层对象
enums:
	ResponseEnum:返回错误信息枚举类
	UserStatusEnum:用户状态信息枚举
filter:
	ShiroLoginFilter:用于处理shiro在检测到用户未登录时,自动跳转到 login 界面,而非返回JSON
	WebContextFilter:用于设置当前线程的web上下文环境
need:implement:此包下的接口是用户需要实现的
	UrlFilterInterface:shiro的过滤配置信息,用户可自定义,如果无需配置,将方法空实现即可
	UserStatusHandler:用户状态自定义处理机制,实现此接口需要用户自行处理账号状态,可选方案:throw RunTimeException + 全局异常处理
response:
	CommonResponse:返回值JSON规范
service:
	SysMenuService:菜单表(权限表)基础业务,仅支持一个接口方法(根据角色ID查询用户权限)
	SysRoleService:角色表业务,仅支持一个接口方法(通过用户ID查询角色集合)
	SysUserService:用户业务接口,仅支持一个接口方法(根据用户名查询实体)
	SysUserRoleService:用户和角色表基础业务,不支持任何方法
	SysRoleMenuService:角色与权限业务,不支持任何方法
service:impl
	SysMenuServiceImpl:实现类、具有自动创建表功能
	SysRoleMenuServiceImpl:实现类、具有自动创建表功能
	SysRoleServiceImpl:实现类、具有自动创建表功能
	SysUserRoleServiceImpl:实现类、具有自动创建表功能
	SysUserServiceImpl:实现类、具有自动创建表功能
util:
	RequestUtils:支持方法,request获取IP地址
	SHA256Util:Sha-256加密工具
	ShiroUtils:常用的shiro工具,包括(获取当前用户Session、用户登出、获取当前用户信息、删除用户缓存信息)
	SpringUtil:Spring上下文工具类
	UserPasswordUtils:用户密码加密工具类
vo:
	SysUserVO:用户基本信息VO类(仅包含用户ID、用户名、状态)
	

重要代码详解:
org.edith.shiro.advice.GlobalExceptionHandler@ControllerAdvice是spring的异常处理注解,使用方法很简单

import org.apache.shiro.authz.UnauthenticatedException;
import org.edith.shiro.enums.ResponseEnum;
import org.edith.shiro.response.CommonResponse;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.core.annotation.Order;
/**
 * 统一异常处理
 */
@ControllerAdvice
@Order(0)
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    /**
     * 无访问权限
     * @return 基础JSON数据
     */
    @ResponseBody
    @ExceptionHandler(value = AuthorizationException.class)
    public CommonResponse authorizationExceptionHandler(){
        return CommonResponse.failure(ResponseEnum.NO_ACCESS);
    }

    /**
     * 用户不存在
     * @return 基础JSON数据
     */
    @ResponseBody
    @ExceptionHandler(value = AuthenticationException.class)
    public CommonResponse authenticationExceptionHandler(){
        return CommonResponse.failure(ResponseEnum.INCORRECT_USERNAME_OR_PASSWORD);
    }

    /**
     * 用户不存在或者密码错误
     * @return 基础JSON数据
     */
    @ResponseBody
    @ExceptionHandler(value = LockedAccountException.class)
    public CommonResponse lockedAccountExceptionHandler(){
        return CommonResponse.failure(ResponseEnum.INCORRECT_USERNAME_OR_PASSWORD);
    }

    /**
     * 用户被锁定
     * @return 基础JSON数据
     */
    @ResponseBody
    @ExceptionHandler(value = IncorrectCredentialsException.class)
    public CommonResponse incorrectCredentialsExceptionHandler(){
        return CommonResponse.failure(ResponseEnum.ACCOUNT_LOCKED);
    }


    /**
     * 用户未登录
     * @return 基础JSON数据
     */
    @ResponseBody
    @ExceptionHandler(value = UnauthenticatedException.class)
    public CommonResponse unauthenticatedExceptionHandler(){
        return CommonResponse.failure(ResponseEnum.NO_LOGIN);
    }

}

org.edith.shiro.component.ShiroRealm


import org.apache.shiro.authz.UnauthenticatedException;
import org.edith.shiro.constant.RedisConstant;
import org.edith.shiro.context.WebContext;
import org.edith.shiro.dataobject.SysMenuDO;
import org.edith.shiro.dataobject.SysRoleDO;
import org.edith.shiro.dataobject.SysUserDO;
import org.edith.shiro.enums.UserStatusEnum;
import org.edith.shiro.need.implement.UserStatusHandler;
import org.edith.shiro.service.SysMenuService;
import org.edith.shiro.service.SysRoleService;
import org.edith.shiro.service.SysUserService;
import org.edith.shiro.util.SHA256Util;
import org.edith.shiro.util.ShiroUtils;
import org.apache.shiro.authc.*;
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.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 *  Shiro权限匹配和账号密码匹配
 *
 */
@Component("edithShiroRealm")
public class ShiroRealm extends AuthorizingRealm {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private RedisSessionDAO redisSessionDAO;
    @Autowired
    private RedisTemplate<String,String>  redisTemplate;
    @Autowired(required = false)
    private UserStatusHandler userStatusHandler;

    /**
     * 授权权限
     * 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

        // 如果能执行到这里,token一定有值,且用户一定已登录
        String token = WebContext.getThreadLocal().get().get( "token" );

        String ipAddress = WebContext.getThreadLocal().get().get( "ipAddress" );

        String[] arr = token.split( "_" );
        // 获取最后一位加密值
        String code = arr[arr.length-1];
        String temp = DigestUtils.md5DigestAsHex( SHA256Util.sha256( ipAddress, RedisConstant.REDIS_PREFIX_LOGIN ).getBytes() );
        /*
         * 这么做的目的是为了防止盗用token,将token与客户端的IP地址进行绑定,
         * 虽然不能完全杜绝盗用token,至少也能将用户的token做一层安全保障
         * 注意:弱网环境和频繁变换IP环境 慎用,可将此代码注释调即可
         */
        if(!code.equals( temp )){
            throw new UnauthenticatedException();
        }


        //获取用户ID
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUserDO sysUserDO = (SysUserDO) principalCollection.getPrimaryPrincipal();
        Long userId = sysUserDO.getUserId();
        //这里可以进行授权和处理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查询角色和权限(这里根据业务自行查询)
        List<SysRoleDO> sysRoleDOList = sysRoleService.selectSysRoleByUserId(userId);
        for (SysRoleDO sysRoleDO : sysRoleDOList) {
            rolesSet.add(sysRoleDO.getRoleName());
            List<SysMenuDO> sysMenuDOList = sysMenuService.selectSysMenuByRoleId(sysRoleDO.getRoleId());
            for (SysMenuDO sysMenuDO : sysMenuDOList) {
                permsSet.add(sysMenuDO.getPerms());
            }
        }
        //将查到的权限和角色分别传入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        redisTemplate.expire( token,30, TimeUnit.MINUTES );
        return authorizationInfo;
    }

    /**
     * 身份认证
     *
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //获取用户的输入的账号.
        String username = (String) authenticationToken.getPrincipal();
        //通过username从数据库中查找 User对象,如果找到进行验证
        //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
        SysUserDO user = sysUserService.selectUserByName(username);
        //判断账号是否存在
        if (user == null) {
            throw new AuthenticationException();
        }
        
        if(userStatusHandler == null){
            //判断账号是否被冻结
            if (user.getStatus()==null|| UserStatusEnum.PROHIBIT.name().equals(user.getStatus())){
                throw new LockedAccountException();
            }
        }else{
       	 // 如果存在用户的自定义处理流程
            userStatusHandler.statusHandler( user );
        }
        //进行验证
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用户名
                user.getPassword(),                    //密码
                ByteSource.Util.bytes(user.getSalt()), //设置盐值
                getName()
        );
        //验证成功开始踢人(清除缓存和Session)
        ShiroUtils.deleteCache(username,true);
        return authenticationInfo;
    }
}

org.edith.shiro.component.ShiroSessionIdGenerator 自定义SessionId生成器


import org.edith.shiro.constant.RedisConstant;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.edith.shiro.context.WebContext;
import org.edith.shiro.util.SHA256Util;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;

import java.io.Serializable;
import java.util.UUID;

/**
 * 自定义SessionId生成器
 */
@Component("edithSessionIdGenerator")
public class ShiroSessionIdGenerator implements SessionIdGenerator {
    /**
     * 实现SessionId生成
     */
    @Override
    public Serializable generateId(Session session) {
        // 使用UUID + IP地址 sha256 加密之后,在进行MD5加密
        return String.format( RedisConstant.REDIS_PREFIX_LOGIN, UUID.randomUUID().toString()+"_"+getCode() );
    }
    /**
     * 获取加密后的IP地址
     * @return 加密后的IP地址
     */
    private String getCode() {
        String ipAddress = WebContext.getThreadLocal().get().get( "ipAddress" );
        return DigestUtils.md5DigestAsHex( SHA256Util.sha256( ipAddress, RedisConstant.REDIS_PREFIX_LOGIN ).getBytes() );
    }
}

org.edith.shiro.config.RedisDAOConfig redis缓存管理器等配置信息


import org.edith.shiro.component.ShiroSessionIdGenerator;
import org.edith.shiro.constant.ShiroConstant;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

@Configuration
public class RedisDAOConfig {
    /**
     * 配置 redis 相关默认值
     */
 
    @Value("${spring.redis.host:127.0.0.1}")
    private String host;
    @Value("${spring.redis.port:6379}")
    private int port;
    @Value("${spring.redis.timeout:6000}")
    private int timeout;
    @Value("${spring.redis.password:}")
    private String password;
    @Value("${spring.redis.pool.max-active:1000}")
    private int maxActive;
    @Value("${spring.redis.pool.max-idle:10}")
    private int maxIdle;
    @Value("${spring.redis.pool.min-idle:5}")
    private int minIdle;
    @Value("${spring.redis.pool.max-wait:-1}")
    private long maxWaitMillis;

    @Autowired
    @Qualifier("edithSessionIdGenerator")
    private ShiroSessionIdGenerator sessionIdGenerator;

    @Bean
    public JedisPool redisPoolFactory(){
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxIdle(maxIdle);
        jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        jedisPoolConfig.setMaxTotal(maxActive);
        jedisPoolConfig.setMinIdle(minIdle);
        return  new JedisPool(jedisPoolConfig,host,port,timeout,password);
    }
    /**
     * 配置Redis管理器,使用的是shiro-redis开源插件
     */
    @Bean("edithRedisManager")
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        redisManager.setTimeout(timeout);
        redisManager.setPassword(password);
        redisManager.setJedisPool(redisPoolFactory());
        return redisManager;
    }

    /**
     * 配置RedisSessionDAO
     * 使用的是shiro-redis开源插件
     */
    @Bean("edithRedisSessionDAO")
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        redisSessionDAO.setSessionIdGenerator(sessionIdGenerator);
        redisSessionDAO.setKeyPrefix(ShiroConstant.SESSION_KEY);
        redisSessionDAO.setExpire(ShiroConstant.EXPIRE);
        return redisSessionDAO;
    }

    /**
     * 配置Cache管理器
     * 用于往Redis存储权限和角色标识,使用的是shiro-redis开源插件
     */
    @Bean("edithRedisCacheManager")
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        redisCacheManager.setKeyPrefix(ShiroConstant.CACHE_KEY);
        // 配置缓存的话要求放在session里面的实体类必须有个id标识
        redisCacheManager.setPrincipalIdFieldName("username");
        return redisCacheManager;
    }
}

org.edith.shiro.config.ShiroConfig shiro 基础配置


import org.edith.shiro.component.ShiroRealm;
import org.edith.shiro.component.ShiroSessionManager;
import org.edith.shiro.constant.SHA256Constant;
import org.edith.shiro.filter.ShiroLoginFilter;
import org.edith.shiro.need.implement.UrlFilterInterface;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;

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

/**
 * Shiro配置类
 */
@Configuration
public class ShiroConfig {
    @Autowired(required = false)
    private UrlFilterInterface urlFilterInterface;

    @Autowired
    @Qualifier("edithRedisSessionDAO")
    private RedisSessionDAO redisSessionDAO;

    @Autowired
    @Qualifier("edithRedisCacheManager")
    private RedisCacheManager redisCacheManager;

    @Autowired
    @Qualifier("edithShiroRealm")
    private ShiroRealm shiroRealm;

    @Autowired
    @Qualifier("edithShiroSessionManager")
    private ShiroSessionManager shiroSessionManager;

    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 自定义session管理
        shiroSessionManager.setSessionDAO(redisSessionDAO);
        securityManager.setSessionManager(shiroSessionManager);
        // 自定义Cache实现
        securityManager.setCacheManager(redisCacheManager);
        // 自定义Realm验证
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        securityManager.setRealm(shiroRealm);
        return securityManager;
    }

    /**
     * 开启Shiro-aop注解支持,使用代理方式所以需要开启代码支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro基础配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        if(urlFilterInterface == null){
            Map<String, String> filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
            Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
            if(CollectionUtils.isEmpty(filterChainDefinitionMap) ||CollectionUtils.isEmpty(filters) ){
                throw new RuntimeException("请配置shiro过滤器");
            }
        }else{
            Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
            Map<String,Filter> filterMap = shiroFilterFactoryBean.getFilters();
            // 配置过滤:不会被拦截的链接
            if(!CollectionUtils.isEmpty(urlFilterInterface.getAnonUrlSet())){
                urlFilterInterface.getAnonUrlSet().forEach(url->filterChainDefinitionMap.put(url,"anon"));
            }
            if(!CollectionUtils.isEmpty(urlFilterInterface.getAnonFilters())) {
                urlFilterInterface.getAnonFilters().forEach(filter -> filterMap.put("anon", filter));
            }

            // 配置需要认证的URL
            if(!CollectionUtils.isEmpty(urlFilterInterface.getAuthcUrlSet())){
                urlFilterInterface.getAuthcUrlSet().forEach(url->filterChainDefinitionMap.put(url,"authc"));
            }
            if(!CollectionUtils.isEmpty(urlFilterInterface.getAuthcFilters())){
                urlFilterInterface.getAuthcFilters().forEach(filter->filterMap.put("authc",filter));
            }else{
                filterMap.put("authc",new ShiroLoginFilter() );
            }
            shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

        }
        return shiroFilterFactoryBean;
    }


    /**
     * 凭证匹配器
     * 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        shaCredentialsMatcher.setHashAlgorithmName(SHA256Constant.HASH_ALGORITHM_NAME);
        shaCredentialsMatcher.setHashIterations(SHA256Constant.HASH_ITERATIONS);
        return shaCredentialsMatcher;
    }
}

org.edith.shiro.constant.HttpConstant http 相关常量


public interface HttpConstant {
    /**
     * token 请求头 key
     */
    String AUTHORIZATION = "Authorization";
    String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
}

org.edith.shiro.constant.RedisConstant redis常量


/**
 *  redis常量类
 *
 */
public interface RedisConstant {
    /**
     * TOKEN前缀
     */
    String REDIS_PREFIX_LOGIN = "login_token_%s";
    /**
     * 过期时间2小时
     */
    Integer REDIS_EXPIRE_TWO = 7200;
    /**
     * 过期时间15分
     */
    Integer REDIS_EXPIRE_EMAIL = 900;
    /**
     * 过期时间5分钟
     */
    Integer REDIS_EXPIRE_KAPTCHA = 300;
    /**
     * 暂无过期时间
     */
    Integer REDIS_EXPIRE_NULL = -1;
}

org.edith.shiro.constant.SHA256Constant 加密用的一些常量

/**
 * 加密算法常量
 */
public interface SHA256Constant {
    /**
     * 加密算法
     */
    String HASH_ALGORITHM_NAME = "SHA-256";
    /**
     * 循环次数
     */
    int HASH_ITERATIONS = 15;
}

org.edith.shiro.constant.ShiroConstant shiro 相关常量

/**
 * shiro 常量值
 */
public interface ShiroConstant {
    /**
     * redis 缓存前缀
     */
    String CACHE_KEY = "shiro:cache:";
    String SESSION_KEY = "shiro:session:";
    /**
     * 默认过期时间,单位:秒
     */
    int EXPIRE = 1800;
}

org.edith.shiro.context.WebContext web上下文环境


import java.util.Map;

/**
 * 2020-04-16 12:54
 *
 * @author LCY
 */
public class WebContext {
    private static ThreadLocal<Map<String,String>> threadLocal = new ThreadLocal<>();

    public static synchronized ThreadLocal<Map<String, String>> getThreadLocal() {
        return threadLocal;
    }
}

org.edith.shiro.controller.UserLoginController 登录、注销WEB接口


import org.edith.shiro.dataobject.SysUserDO;
import org.edith.shiro.response.CommonResponse;
import org.edith.shiro.util.ShiroUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 用户登录
 */
@RestController
@RequestMapping("/user")
public class UserLoginController {

    /**
     * 登录
     */
    @PostMapping("/login")
    public CommonResponse login(@RequestBody SysUserDO sysUserDO) {
        Map<String, Object> map = new HashMap<>();

        //验证身份和登陆
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(sysUserDO.getUsername(), sysUserDO.getPassword());
        //进行登录操作
        subject.login(token);
        map.put("token", ShiroUtils.getSession().getId().toString());
        return CommonResponse.success(map);
    }

    /**
     * 注销
     */
    @PostMapping("/logout")
    public CommonResponse logout() {
        SysUserDO userInfo = ShiroUtils.getUserInfo();
        ShiroUtils.deleteCache(userInfo.getUsername(),true);
        ShiroUtils.logout();
        return CommonResponse.success();
    }
}

org.edith.shiro.dao.SysMenuDAO 菜单(权限)DAO

import org.apache.ibatis.annotations.*;
import org.edith.shiro.dataobject.SysMenuDO;

import java.util.List;

/**
 * 权限DAO
 */
public interface SysMenuDAO {

    /**
     * 根据角色查询用户权限
     *
     * @param roleId 角色ID
     *
     * @return List<SysMenuDO> 权限集合
     */
    @Results(value = {
                @Result(column = "menu_id", property = "menuId"),
                @Result(column = "parent_id", property = "parentId"),
                @Result(column = "name", property = "name"),
                @Result(column = "url", property = "url"),
                @Result(column = "perms", property = "perms"),
                @Result(column = "icon", property = "icon"),
            @Result(column = "remark", property = "remark"),
            @Result(column = "create_time", property = "createTime")
            }
    )
    @Select("select * from sys_menu where perms is not null and menu_id in (select menu_id from sys_role_menu where role_id =#{roleId})")
    List<SysMenuDO> selectSysMenuByRoleId(Long roleId);

    @Insert("        create table  if not exists sys_menu(\n" +
            "          menu_id     bigint auto_increment comment '权限ID' primary key,\n" +
            "          parent_id   bigint null comment '父级菜单ID',\n" +
            "          name  varchar(50) null comment '权限名称',\n" +
            "          perms varchar(500) null comment '权限标识',\n" +
            "          url varchar(256) null comment 'url地址',\n" +
            "          create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',\n" +
            "          icon varchar(256) null comment '图标'\n" +
            "          remark varchar(256) null comment '备注'\n" +
            "        ) comment '权限表' charset = utf8;")
    void createTable();
}

org.edith.shiro.dao.SysRoleDAO 角色DAO


    /**
     * 通过用户ID查询角色集合
     *
     * @param userId 用户ID
     * @return List<SysRoleDO> 角色集合
     */
    @Results(value = {
            @Result(column = "role_id",property = "roleId"),
            @Result(column = "role_name",property = "roleName"),
            @Result(column = "remark",property = "remark"),
            @Result(column = "create_time",property = "createTime")
    })
    @Select( "        SELECT sr.* FROM sys_role as sr\n" +
            "        LEFT JOIN sys_user_role as se ON sr.role_id=se.role_id\n" +
            "        WHERE se.user_id = #{userId}" )
    List<SysRoleDO> selectSysRoleByUserId(Long userId);

    @Insert( "        create table  if not exists sys_role\n" +
            "        (\n" +
            "          role_id   bigint auto_increment comment '角色ID' primary key,\n" +
            "          role_name varchar(50) not null comment '角色名称',\n" +
            "          remark    varchar(256) null comment '备注信息',\n" +
            "          create_time  datetime default CURRENT_TIMESTAMP null comment '创建时间'\n" +
            "        ) comment '角色表' charset = utf8;" )
    void createTable();
	

org.edith.shiro.dao.SysRoleMenuDAO 角色和菜单关联表DAO

import org.apache.ibatis.annotations.Insert;

/**
 *  角色权限关系DAO
 *
 */
public interface SysRoleMenuDAO  {
    @Insert( "        create table  if not exists sys_role_menu\n" +
            "        (\n" +
            "          id      bigint auto_increment comment 'ID' primary key,\n" +
            "          role_id bigint null comment '角色ID',\n" +
            "          menu_id bigint null comment '权限ID'\n" +
            "        ) comment '角色与权限关系表' charset = utf8;" )
    void createTable();
}

org.edith.shiro.dao.SysUserDAO 用户信息DAO


import org.apache.ibatis.annotations.*;
import org.edith.shiro.dataobject.SysUserDO;

/**
 * 系统用户DAO
 */
public interface SysUserDAO {
    @Insert("create table  if not exists sys_user\n" +
            "        (\n" +
            "          user_id     bigint auto_increment comment '用户ID' primary key,\n" +
            "          username    varchar(50) not null comment '用户名',\n" +
            "          password    varchar(100)  null comment '密码',\n" +
            "          salt        varchar(50)   null comment '盐值',\n" +
            "          status      varchar(50) null DEFAULT 'NORMAL' comment '状态:NORMAL正常  PROHIBIT禁用',\n" +
            "          create_time datetime default CURRENT_TIMESTAMP null comment '创建时间',\n" +
            "          create_user varchar(256) null comment '创建该账号的操作者',\n" +
            "          modify_time datetime default CURRENT_TIMESTAMP null comment '修改时间',\n" +
            "          modify_user varchar(256) null comment '修改该账户的操作者',\n" +
            "          remark      varchar(256)  null comment '备注信息'\n" +
            "        ) comment '系统用户表' charset = utf8;")
    void createTable();

    @Results(value = {
            @Result(column = "user_id", property = "userId"),
            @Result(column = "username", property = "username"),
            @Result(column = "password", property = "password"),
            @Result(column = "salt", property = "salt"),
            @Result(column = "status", property = "status"),
            @Result(column = "create_time", property = "createTime"),
            @Result(column = "create_user", property = "createUser"),
            @Result(column = "modify_time", property = "modifyTime"),
            @Result(column = "modify_user", property = "modifyUser"),
            @Result(column = "remark", property = "remark")
    })
    @Select("select * from sys_user where username = #{username}")
    SysUserDO selectUserByName(@Param("username") String username);
}

org.edith.shiro.dao.SysUserRoleDAO 角色和用户表关联


import org.apache.ibatis.annotations.Insert;

/**
 *  用户与角色关系DAO
 *
 */
public interface SysUserRoleDAO {
	@Insert( " create table if not exists sys_user_role\n" +
			"        (\n" +
			"          id      bigint auto_increment comment 'ID' primary key,\n" +
			"          user_id bigint null comment '用户ID',\n" +
			"          role_id bigint null comment '角色ID'\n" +
			"        ) comment '用户与角色关系表' charset = utf8;" )
	void createTable();
}

org.edith.shiro.filter.ShiroLoginFilter shiro返回login处理过滤器


import com.alibaba.fastjson.JSONObject;
import org.edith.shiro.enums.ResponseEnum;
import org.edith.shiro.response.CommonResponse;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ShiroLoginFilter extends FormAuthenticationFilter {

    /**
     * 如果isAccessAllowed返回false 则执行onAccessDenied
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @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);
    }
    /**
     * 在访问controller前判断是否登录,返回json,不进行重定向。
     *
     * @param request
     * @param response
     * @return true-继续往下执行,false-该filter过滤器已经处理,不继续执行其他过滤器
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
        httpServletResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        httpServletResponse.getWriter().write(JSONObject.toJSONString(CommonResponse.failure(ResponseEnum.NO_LOGIN)));
        return false;
    }
}

org.edith.shiro.filter.WebContextFilter web上下文过滤器


/**
 * 2020-04-16 12:56
 *
 * @author LCY
 */
@WebFilter(urlPatterns="/*",filterName = "webContextFilter")
public class WebContextFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void destroy() {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        String token = request.getHeader( HttpConstant.AUTHORIZATION);
        String ipAddress = RequestUtils.getIPAddress(request);
        Map<String,String> webContext = new HashMap<>();
        webContext.put( "token", token);
        webContext.put( "ipAddress", ipAddress);
        WebContext.getThreadLocal().set( webContext );
        filterChain.doFilter(servletRequest, servletResponse);
    }
}

org.edith.shiro.response.CommonResponse 通用返回对象


import org.edith.shiro.enums.ResponseEnum;

import java.io.Serializable;

public class CommonResponse<T> implements Serializable {

    private boolean success;
    private String code;
    private String message;
    private T data;

    private CommonResponse(boolean success, ResponseEnum responseEnum, T data) {
        this.success = success;
        this.message = responseEnum.getMessage();
        this.code = responseEnum.getCode();
        this.data = data;
    }

    public static <T> CommonResponse success(T data){
        return new CommonResponse<T>(true, ResponseEnum.SUCCESS,data);
    }
    public static  CommonResponse success(){
        return new CommonResponse(true,ResponseEnum.SUCCESS,null);
    }

    public static  CommonResponse failure(){
        return new CommonResponse(false,ResponseEnum.FAILURE,null);
    }

    public static  CommonResponse failure(ResponseEnum responseEnum){
        return new CommonResponse(false,responseEnum,null);
    }

    public boolean isSuccess() {
        return success;
    }

    public void setSuccess(boolean success) {
        this.success = success;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public T getData() {
        return data;
    }

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

org.edith.shiro.AutoConfig 自动装配类

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.ComponentScan;

@MapperScan("org.edith.shiro.dao")
@ComponentScan("org.edith.shiro")
@ServletComponentScan("org.edith.shiro.filter")
public class AutoConfig {
}

代码执行流程
登录

org.edith.shiro.controller.UserLoginController#login  
						↓
org.edith.shiro.component.ShiroRealm#doGetAuthenticationInfo
						↓
用户账号状态处理(org.edith.shiro.need.implement.UserStatusHandler实现类)
						↓
				     登录成功

鉴权

示例:
	访问具有@RequiresPermissions 注解的web接口
						↓
	org.edith.shiro.component.ShiroRealm#doGetAuthorizationInfo(获取用户的权限列表)
						
;