项目链接:
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(获取用户的权限列表)