项目开发扩展-4.Shiro框架
项目开发扩展-Shiro框架
[TOC]
1.Shiro框架介绍
2.Springboot项目整合Shiro框架
参考链接:
https://blog.csdn.net/weixin_41012481/article/details/103822835
http://www.360doc.com/content/17/0903/22/16915_684424553.shtml
Springboot+Shiro-项目搭建参考:https://www.jianshu.com/p/35ee0ff8f969
https://cloud.tencent.com/developer/article/1499013
https://zhidao.baidu.com/question/245542149740345244.html
a.Springboot整合Shiro
【1】单类型用户登录整合说明
步骤说明:
<1>自定义Realm(针对单类型用户)
<2>自定义Session管理器(可额外扩展rememberMe、redis等)
<3>定义Shiro全局配置类:统一引入相关配置
<4>自定义工具类获取登录用户信息
<5>Controller登录校验
自定义ShiroRealm配置:
/**
* 单类型用户登录实现-自定义认证规则
**/
public class ShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class);
@Value("${custom.login.loginMode}")
private String loginMode;
/**
* 在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
* 解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
*/
// @Lazy
@Autowired
private LoginService loginService;
/**
* 授权查询回调函数-进行鉴权但缓存中无用户的授权信息时调用
* 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
LoginUser loginUser = (LoginUser) principalCollection.getPrimaryPrincipal();
// 自定义授权处理(角色、权限设定),此处对管理员做分级处理
Set<String> roleSet = new HashSet<>();
// adminType:superAdmin(超级管理员)、subAdmin(子系统管理员)
roleSet.add(loginUser.getRole());
logger.info("shiro-{}-授权认证:{}-{}-角色列表{}", getName(),
loginUser.getNum(),loginUser.getName(), roleSet);
authorizationInfo.setRoles(roleSet);
return authorizationInfo;
}
/**
* 认证回调函数-用户身份认证,在登录时触发调用
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 如果需要配置自定义Token需要相应配置注入,否则报数据转换类型错误
// (可通过查看UsernamePasswordToken的类定义,了解每个方法的作用含义)
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String loginAccount = token.getUsername(); // 或者用token.getPrincipal()替代
String loginPassword = new String(token.getPassword()); // token.getCredentials()(返回数据Object)
// 调用本地方法进行登录验证
LoginUser loginUser = null;
try {
// 校验登录模式
if (LoginModeEnum.LOCAL.getMode().equals(loginMode)) {
// 调用本地方法进行登录验证
loginUser = loginService.loginByLocal(loginAccount);
} else if (LoginModeEnum.REMOTE.getMode().equals(loginMode)) {
// 调用远程登录方法进行登录验证
loginUser = loginService.loginByRemote(loginAccount, loginPassword);
} else {
throw new BoxingException("指定登录模式参数校验异常");
}
} catch (BoxingException e) {
// 异常处理转化,使异常信息正常并拦截并返回给前端(在自定义异常处理器中拦截处理)
throw new AccountException(e.getMessage());
}
logger.info("shiro-{}-登录认证:{}-{}", getName(),
loginAccount,loginUser.getName());
// 处理重复登录的session
SessionsSecurityManager securityManager = (SessionsSecurityManager)
SecurityUtils.getSecurityManager();
DefaultSessionManager sessionManager = (DefaultSessionManager)
securityManager.getSessionManager();
// 获取当前已登录的用户session列表
Collection<Session> sessions = sessionManager.getSessionDAO()
.getActiveSessions();
// 清除该用户以前登录时保存的session
for (Session session : sessions) {
// 如果和当前session是同一个session,则不剔除
if (SecurityUtils.getSubject().getSession().getId().equals(session.getId())) {
break;
}
LoginUser findUser = (LoginUser)(session.getAttribute("loginCache"));
if (findUser != null) {
// 远程登录(校验登录账号:UASS或ID相同则认为是同一个用户)
if (loginUser.getLoginAccount().equals(findUser.getLoginAccount())) {
logger.info("{}已登录,剔除中...",findUser.getLoginAccount() );
sessionManager.getSessionDAO().delete(session);
}
}
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
loginUser, // 传入整个登录用户实体对象随后权限认证的时候便可通过(T)principalCollection.getPrimaryPrincipal()访问
loginPassword, // 密码
// ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
return authenticationInfo;
}
}
自定义session管理器
public class CustomSessionManager extends DefaultWebSessionManager {
// 定义常量(token对应的key):请求头中使用的标识key,用来传递token
private static final String AUTH_TOKEN = "authToken";
private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
// 重写构造器
public CustomSessionManager() {
super();
// 设定session失效时间(默认是30分钟)
setGlobalSessionTimeout(MILLIS_PER_MINUTE * 100);
this.setDeleteInvalidSessions(true);
}
/**
* @return java.io.Serializable
* @MethodName getSessionId
* @Description 重写方法实现从请求头获取Token便于接口统一
* (每次请求进来, Shiro会去从请求头找Authorization这个key对应的Value ( Token))
* @Param [request, response]
**/
@Override
public Serializable getSessionId(ServletRequest request, ServletResponse response) {
String token = WebUtils.toHttp(request).getHeader(AUTH_TOKEN);
/**
* 获取请求头中的AUTH_TOKEN的值,如果请求头中有 AUTH_TOKEN
* 则其值为sessionId.shiro就是通过sessionId来控制
**/
if (!StringUtils.isEmpty(token)) {
// 请求头中如果有 authToken, 则其值为sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
// sessionId
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
return token;
} else {
// 如果没有携带id参数则按照父类的方式在cookie进行获取sessionId
return super.getSessionId(request, response);
}
}
}
Shiro全局配置类:统一引入相关配置
@Configuration
public class SingleConfig {
private static final Logger logger = LoggerFactory.getLogger(SingleConfig.class);
@Bean(name = "single-lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
// getDefaultAdvisorAutoProxyCreator、authorizationAttributeSourceAdvisor对注解权限起作用有很大的关系(放置在配置的最上面)
@Bean(name = "single-defaultAdvisorAutoProxyCreator")
public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
/**
* 匹配所有加了Shiro 认证注解的方法(如果不添加则权限注解不生效)
* 开启Shiro-aop注解支持:使用代理方式所以需要开启代码支持
*/
@Bean(name = "single-authorizationAttributeSourceAdvisor")
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/**
* 自定义身份验证器-将自身的验证方式载入容器
**/
@Bean(name = "single-shiroRealm")
public ShiroRealm shiroRealm() {
ShiroRealm shiroRealm = new ShiroRealm();
// shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return shiroRealm;
}
/**
* Shiro基础配置-Filter工厂,设置对应的过滤条件和跳转条件
**/
@Bean(name = "single-shiroFilterFactory")
public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
// 注意过滤器配置顺序不能颠倒(如果颠倒可能导致访问路径拦截效果差强人意)
// 配置过滤:不会被拦截的链接(如果加了接口访问失败,考虑拦截过滤的先后顺序)
// filterChainDefinitionMap.put("/framework/rest/common/noLogin", "loggerout");
/**
* shiro拦截配置说明:
* anon:匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤、登录入口
* authc:如果没有登录会跳到相应的登录页面登录
* user:用户拦截器,用户已经身份验证/记住我登录的都可访问
**/
filterChainDefinitionMap.put("/login/toLogin", "anon");
filterChainDefinitionMap.put("/login/toLogout", "anon");
// 配置静态资源过滤
filterChainDefinitionMap.put("/static/**", "anon");
//配置退出过滤器,Shiro定义退出代码
filterChainDefinitionMap.put("/logout", "logout");
// 可以自定义登录退出 url,anon 拦截器定义
// filterChainDefinitionMap.put("/afterlogout", "logout");
// 主页访问
// filterChainDefinitionMap.put("/login/index", "user");
// 对所有用户进行验证(所有url必须认证通过之后才可访问,一般将/**放置在拦截的最下方)
filterChainDefinitionMap.put("/**", "authc");
// 配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据
shiroFilterFactoryBean.setLoginUrl("/login/noLogin");
// 错误页面,认证不通过跳转(未授权页面跳转)
shiroFilterFactoryBean.setUnauthorizedUrl("/login/loginFail");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 安全管理器-权限管理,配置主要是Realm的管理认证
**/
@Bean(name = "single-securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义的shiro session缓存管理器
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(ehCacheManager());
// 自定义Cookie(实现rememberMe)
// securityManager.setRememberMeManager(cookieRememberMeManager());
// 针对单个Realm认证(放到最后)
securityManager.setRealm(shiroRealm());
return securityManager;
}
/**
* 凭证匹配器
* 将密码校验交给Shiro的SimpleAuthenticationInfo进行处理,在这里做匹配配置
*/
// @Bean(name = "single-hashedCredentialsMatcher")
// public HashedCredentialsMatcher hashedCredentialsMatcher() {
// HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
// // 散列算法:这里使用SHA256算法;
// shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
// // 散列的次数,比如散列两次,相当于 md5(md5(""));
// shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
// return shaCredentialsMatcher;
// }
/**
* SessionID生成器 - 自定义session生成器
**/
@Bean(name = "single-sessionIdGenerator")
public CustomSessionIdGenerator sessionIdGenerator() {
return new CustomSessionIdGenerator();
}
/**
* 自定义配置Session管理器
**/
@Bean(name = "single-sessionManager")
public SessionManager sessionManager() {
// 注入自定义的shiro session管理器
CustomSessionManager shiroSessionManager = new CustomSessionManager();
// 如果后续考虑多tomcat部署应用,则可使用shiro-redis开源插件作session的控制,或是nginx负载均衡
// shiroSessionManager.setSessionDAO(redisSessionDAO()); sessionDAO中设定自定义sessionId
return shiroSessionManager;
}
/**
* shiro缓存管理器配置
**/
@Bean(name = "single-ehCacheManager")
public EhCacheManager ehCacheManager() {
// 注入自定义的shiro缓存管理器(指定配置文件存储位置)
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return ehCacheManager;
}
/**
* rememberMe配置相关:设置cookie对象
**/
@Bean(name = "single-rememberMeCookie")
public SimpleCookie rememberMeCookie() {
logger.info("设置Cookie对象(rememberMeCookie)");
// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("cfa-admin-single-rememberMe");
// 记住我cookie生效时间30天 ,单位秒
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* rememberMe配置相关:cookie管理对象
**/
@Bean(name = "single-cookieRememberMeManager")
public CookieRememberMeManager cookieRememberMeManager() {
logger.info("Cookie管理对象:cookieRememberMeManager()");
CookieRememberMeManager manager = new CookieRememberMeManager();
manager.setCookie(rememberMeCookie());
return manager;
}
}
自定义工具类获取登录用户信息
public class SingleShiroUtil {
/**
* 私有构造器
**/
private SingleShiroUtil() {
}
/**
* getSubject() : SecurityUtils.getSubject()
**/
public static Subject getSubject() {
return SecurityUtils.getSubject();
}
/**
* 获取当前用户Session
**/
public static Session getSession() {
return getSubject().getSession();
}
/**
* 获取当前登录用户信息:getPrincipal() : SecurityUtils.getSubject(),用Object接收
**/
public static Object getPrincipal() {
return SecurityUtils.getSubject().getPrincipal();
}
/**
* 用户登出
**/
public static void logout() {
getSubject().logout();
}
/**
* 获取当前用户信息(与ShiroRealm中存入的内容相对应)
**/
public static LoginUser getCurrentUser() {
return (LoginUser) getSubject().getPrincipal();
}
/**
* 获取当前登录用户id
**/
public static String getCurrentUserId() {
return getCurrentUser().getUserId();
}
/**
* 删除用户缓存信息
**/
public static void deleteCache(String username, boolean isRemoveSession) {
//从缓存中获取Session
Session session = null;
Object attribute = null;
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);
}
/**
* 获取当前登录ip数据
**/
public static String getIp() {
return getSubject().getSession().getHost();
}
/**
* 获取sessionId
**/
public static String getSessionId() {
return String.valueOf(getSubject().getSession().getId());
}
/**
* 生成随机盐
*/
public static String randomSalt() {
// 一个Byte占两个字节,此处生成的3字节,字符串长度为6
SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator();
String hex = secureRandom.nextBytes(3).toHex();
return hex;
}
}
LoginController定义
@RestController("loginController")
@RequestMapping("/login")
public class LoginController {
@PostMapping("/toLogin")
public AjaxResult toLogin(@RequestBody JSONObject jsonObject) {
String loginAccount = jsonObject.getString("loginAccount");
String loginPassword = jsonObject.getString("loginPassword");
if (StringUtils.isEmpty(loginAccount)) {
throw new BoxingException("登录账号不能为空");
}
// shiro验证登录(身份验证)
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(loginAccount, loginPassword);
// token.setRememberMe(false); 设定rememberMe
subject.login(token);
// 根据不同的登录类型将登录数据存储到相应session容器
Object obj = subject.getPrincipal();
SecurityUtils.getSubject().getSession().setAttribute("loginCache", obj);
Serializable tokenId = subject.getSession().getId();
Map<String, Object> map = new HashMap<String, Object>();
map.put("tokenId", tokenId);
// 获取登录后的用户信息
Object currentUser = SingleShiroUtil.getPrincipal();
map.put("currentUser", currentUser);
return AjaxResultUtil.success("data", map);
}
/**
* 自定义登录退出操作
**/
@PostMapping("/toLogout")
public AjaxResult toLogout() {
// ShiroUtil.deleteCache();
Subject subject = SecurityUtils.getSubject();
if (subject.isAuthenticated()) {
// 销毁SESSION(清理权限缓存)
subject.logout();
}
// 执行登录退出操作
return AjaxResultUtil.success();
}
@GetMapping("/noLogin")
public AjaxResult noLogin() {
return AjaxResultUtil.error(ResultEnum.NO_LOGIN_ERROR);
}
@GetMapping("/loginFail")
public AjaxResult loginFail() {
return AjaxResultUtil.error(ResultEnum.LOGIN_FAIL_ERROR);
}
@GetMapping("/index")
public AjaxResult index() {
return AjaxResultUtil.success();
}
}
【2】多类型用户登录整合说明
参考链接:
SpringBoot 整合shiro实现多Realm的控制:https://blog.csdn.net/yuemmm/article/details/104562419?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2
参考:
https://blog.csdn.net/sinat_35626559/article/details/94553393
https://www.itfuyun.com/posts/shiro-multiple-realm.html
https://blog.csdn.net/cckevincyh/article/details/79629022?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.nonecase
https://msd.misuland.com/pd/3255817963235705136
多realm登录认证与多realm授权认证:
https://blog.csdn.net/u013294097/article/details/90053299
https://www.cnblogs.com/skyLogin/p/10871347.html
整合步骤
步骤说明:
<1>自定义Token(针对多类型用户)
<2>自定义Realm、自定义扩展ModularRealmAuthorizer、ModularRealmAuthenticator
<3>自定义Session管理器(可额外扩展rememberMe、redis等)
<4>定义Shiro全局配置类:统一引入相关配置
<5>自定义工具类获取登录用户信息
<6>Controller登录校验
自定义Token(针对多类型用户)
/** 定义登录类型枚举 **/
public enum LoginTypeEnum {
USER("user"), ADMIN("admin");
private String type;
private LoginTypeEnum(String type) {
this.type = type;
}
public String getType() {
return type;
}
@Override
public String toString() {
return this.type.toString();
}
}
// 多类型用户登录实现-自定义Token实现不同类型用户登录管理
public class CustomToken extends UsernamePasswordToken {
// 登录类型(user-普通用户登录;admin-后台管理员登录)
private String loginType;
public CustomToken(final String username, final String password,String loginType) {
super(username,password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
自定义Realm(多Realm针对不同登录类型的用户)
自定义扩展ModularRealmAuthorizer、ModularRealmAuthenticator
/**
* AdminRealm:多类型用户认证实现-自定义认证规则
**/
public class AdminRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(AdminRealm.class);
// @Value("${cfa-admin.loginMode}")
private String loginMode;
/**
* 在自定义Realm中注入的Service声明中加入@Lazy注解即可解决@cacheble注解无效问题
* 解决同时使用Redis缓存数据和缓存shiro时,@cacheble无效的问题
*/
// 重载getName()方法使其正确映射到对应的Realm
@Override
public String getName() {
return LoginTypeEnum.ADMIN.getType();
}
/**
* 授权查询回调函数-进行鉴权但缓存中无用户的授权信息时调用
* 用户进行权限验证时候Shiro会去缓存中找,如果查不到数据,会执行这个方法去查权限,并放入缓存中
**/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// -------- 授权认证逻辑处理 ----------
return authorizationInfo;
}
/**
* 认证回调函数-用户身份认证,在登录时触发调用
**/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// -------- 登录认证逻辑处理 ----------
return authenticationInfo;
}
}
Realm对应doGetAuthenticationInfo方法调用分析:
a.从UsernamePasswordToken中获取相应的登录请求参数
b.调用Service层完成登录用户信息验证,并将验证后的用户信息注入
Shiro相关的异常如果需要被全局异常处理器捕获需要借助try catch捕获并转化异常处理,使异常信息正常并拦截并返回给前端
try {
// 校验登录模式进行相应的业务逻辑处理
if (LoginModeEnum.LOCAL.getMode().equals(loginMode)) {
...业务逻辑处理...
}
} catch (BoxingException e) {
// 异常处理转化,使异常信息正常并拦截并返回给前端(自定义AccountException用于接收Shiro相关异常信息)
throw new AccountException(e.getMessage());
}
c.处理重复登录的session数据
SessionsSecurityManager securityManager = (SessionsSecurityManager)
SecurityUtils.getSecurityManager();
DefaultSessionManager sessionManager = (DefaultSessionManager)
securityManager.getSessionManager();
// 获取当前已登录的用户session列表
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
// 清除该用户以前登录时保存的session
for (Session session : sessions) {
// 如果和当前session是同一个session,则不剔除
if (SecurityUtils.getSubject().getSession().getId().equals(session.getId())) {
break;
// -----------------------------------------------------------------------
User currentUser = (User) (session.getAttribute("currentUser"));
if (currentUser != null) {
// 校验同一浏览器条件下是否登录不同用户的情况:用户编号或ID相同则认为是同一个用户
if (!currentUser.getUserNum().equals(loginUser.getUserNum())) {
// log.info("当前检测到新的用户登入"+currentUser.getUserName() + "被踢出...");
log.info("当前检测到新的用户登入:"+loginUser.getUserName());
// sessionManager.getSessionDAO().delete(session);
}
}
// -------------------------------------------------------------------------
}
// Admin findAdmin = (Admin) (session.getAttribute("currentUser")); 类型转化异常(Admin\User)
Map findAdmin = JSON.parseObject((String) session.getAttribute("currentAdmin"), Map.class);
if (findAdmin != null) {
// 远程登录(校验登录账号:用户编号或ID相同则认为是同一个用户)
if (loginAdmin.getAdminNum().equals(findAdmin.get("adminNum"))) {
System.out.println(findAdmin.get("adminName") + "已登录,剔除中...");
sessionManager.getSessionDAO().delete(session);
}
}
}
d.定义SimpleAuthenticationInfo对象存放登录用户信息并返回结果
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
loginAdmin,// 传入整个登录用户实体对象随后权限认证的时候便可通过(T)principalCollection.getPrimaryPrincipal()访问
loginPassword, // 密码
// ByteSource.Util.bytes(user.getSalt()), //设置盐值
getName()
);
CustomModularRealmAuthorizer
public class CustomModularRealmAuthorizer extends ModularRealmAuthorizer {
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
assertRealmsConfigured();
Set<String> realmNames = principals.getRealmNames();
//获取realm的名字
String realmName = realmNames.iterator().next();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) {
continue;
}
//匹配名字
if (realmName.equals(LoginTypeEnum.ADMIN.getType())) {
if (realm instanceof AdminRealm) {
return ((AdminRealm) realm).isPermitted(principals, permission);
}
}
//匹配名字
if (realmName.equals(LoginTypeEnum.USER.getType())) {
if (realm instanceof UserRealm) {
return ((UserRealm) realm).isPermitted(principals, permission);
}
}
}
return false;
}
@Override
public boolean hasRole(PrincipalCollection principals, String roleIdentifier) {
assertRealmsConfigured();
Set<String> realmNames = principals.getRealmNames();
//获取realm的名字
String realmName = realmNames.iterator().next();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) {
continue;
}
//匹配名字
if (realmName.equals(LoginTypeEnum.ADMIN.getType())) {
if (realm instanceof AdminRealm) {
return ((AdminRealm) realm).hasRole(principals, roleIdentifier);
}
}
//匹配名字
if (realmName.equals(LoginTypeEnum.USER.getType())) {
if (realm instanceof UserRealm) {
return ((UserRealm) realm).hasRole(principals, roleIdentifier);
}
}
}
return false;
}
}
CustomModularRealmAuthenticator
public class CustomModularRealmAuthenticator extends ModularRealmAuthenticator {
private static final Logger log = LoggerFactory.getLogger(CustomModularRealmAuthenticator.class);
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 强制转换回自定义的CustomToken
CustomToken customToken = (CustomToken) authenticationToken;
// 登录类型
String loginType = customToken.getLoginType();
// 获取所有Realm
Collection<Realm> realms = getRealms();
// 登录类型对应的所有Realm
Collection<Realm> typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if (realm.getName().contains(loginType)){
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == 1) {
log.info("doSingleRealmAuthentication() execute ");
return doSingleRealmAuthentication(typeRealms.iterator().next(), customToken);
} else {
log.info("doMultiRealmAuthentication() execute ");
return doMultiRealmAuthentication(typeRealms, customToken);
}
}
}
自定义Session管理器(可额外扩展rememberMe、redis等)- 可参考单类型登录用户
定义Shiro全局配置类:统一引入相关配置
基本配置和单类型登录用户差不多,但针对多类型用户登录需要额外配置相关的登录和权限认证配置
/**
* 自定义身份验证器-将自身的验证方式载入容器(可根据不同登录类型实现自定义)
**/
@Bean(name = "multi-adminRealm")
public AdminRealm adminRealm() {
AdminRealm adminRealm = new AdminRealm();
// shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
// 指定类型,标志为admin登陆(可以在AdminRealm类中重写getName方法实现name属性指定
adminRealm.setName(LoginTypeEnum.ADMIN.getType());
return adminRealm;
}
/**
* 自定义身份验证器-将自身的验证方式载入容器(可根据不同登录类型实现自定义)
**/
@Bean(name = "multi-userRealm")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
// shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
// 指定类型,标志为user登陆(可以在UserRealm类中重写getName方法实现name属性指定
userRealm.setName(LoginTypeEnum.USER.getType());
return userRealm;
}
/**
* 针对多realm 认证实现Realm管理:用于登录身份认证
**/
@Bean(name = "multi-customModularRealmAuthenticator")
public ModularRealmAuthenticator customModularRealmAuthenticator() {
// 重写ModularRealmAuthenticator自定义实现多Realm登录身份认证配置
CustomModularRealmAuthenticator customModularRealmAuthenticator = new CustomModularRealmAuthenticator();
customModularRealmAuthenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return customModularRealmAuthenticator;
}
/**
* 针对多realm 认证实现Realm管理:用于用户权限认证
**/
@Bean(name = "multi-customModularRealmAuthorizer")
public ModularRealmAuthorizer customModularRealmAuthorizer() {
// 重写ModularRealmAuthorizer自定义实现多Realm权限认证配置
CustomModularRealmAuthorizer authorizer = new CustomModularRealmAuthorizer();
return authorizer;
}
/**
* 安全管理器-权限管理,配置主要是Realm的管理认证
**/
@Bean(name = "multi-securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义的shiro session缓存管理器
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(ehCacheManager());
// 自定义Cookie(实现rememberMe)
// securityManager.setRememberMeManager(cookieRememberMeManager());
/**
* 配置说明:
* 单Realm安全管理器配置
* a.加载自定义Realm,不需要重写登录认证和授权认证配置(使用默认)
* --------------------------------------------
* 多Realm安全管理器配置:
* 自定义Realm验证(针对多Realm认证)
* a.加载自定义的多Realm登录认证可授权认证配置(此步骤需要在setRealms之前执行)
* b.加载自定义的多Realm列表
**/
// 多Realm登录认证、授权认证配置加载
securityManager.setAuthenticator(customModularRealmAuthenticator());
securityManager.setAuthorizer(customModularRealmAuthorizer());
// 多Realm配置加载
List<Realm> realms = new ArrayList<>();
realms.add(adminRealm());
realms.add(userRealm());
securityManager.setRealms(realms);
return securityManager;
}
自定义工具类获取登录用户信息 - 可参考单类型登录用户
根据不同登录类型用户进行匹配转换
Controller登录校验
/**
* 自定义登录操作
**/
@PostMapping("/toLogin")
public AjaxResult toLogin(@RequestBody JSONObject jsonObject) {
String loginAccount = jsonObject.getString("loginAccount");
String loginPassword = jsonObject.getString("loginPassword");
String loginType = jsonObject.getString("loginType");
if (StringUtils.isEmpty(loginAccount)) {
throw new BoxingException("登录账号不能为空");
} else if (StringUtils.isEmpty(loginType)) {
throw new BoxingException("登录类型不能为空");
}
// shiro验证登录(身份验证)
Subject subject = SecurityUtils.getSubject();
CustomToken token = new CustomToken(loginAccount, loginPassword, loginType);
// 设定remember
// token.setRememberMe(false);
subject.login(token);
// 将数据存入session
// 根据不同的登录类型将登录数据存储到相应容器
if (LoginTypeEnum.ADMIN.getType().equals(loginType)) {
SecurityUtils.getSubject().getSession().setAttribute("currentAdmin", JSON.toJSONString(subject.getPrincipal()));
} else if (LoginTypeEnum.USER.getType().equals(loginType)) {
SecurityUtils.getSubject().getSession().setAttribute("currentUser", JSON.toJSONString(subject.getPrincipal()));
} else {
throw new BoxingException("指定登录类型错误");
}
Serializable tokenId = subject.getSession().getId();
Map<String, Object> map = new HashMap<String, Object>();
map.put("tokenId", tokenId);
// 获取登录后的用户信息
Object currentUser = SingleShiroUtil.getPrincipal();
map.put("currentUser", currentUser);
return AjaxResultUtil.success("data", map);
}
配置问题
CustomModularRealmAuthenticator:
配置完成之后测试:
如果用户登录没有指定loginType或者指定loginType为空,则默认查找所有的内容(所有满足条件的Realm)
如果用户登录指定了loginType,则按照指定规则去查找满足条件(必须保证不同的登录类型要指定不同),否则还是会验证多个匹配的数据
问题1:如何将不同的Realm和loginType关联起来?(从代码层面去理解shiro多用户类型登录的运行机制)
可以查看自定义的CustomModularRealmAuthenticator,可通过代码看到两者关联的依据是通过xxxRealm的name属性进行关联,因此需要在初始化xxxRealm的时候指定相应的name属性(和loginType相对应),或者是通过重写xxxRealm的getName属性(直接return xxx)
问题2:在哪里配置多Realm(关注ShiroConfig配置文件)
<1>定义多个xxxRealm
<2>在配置文件中注入自定义的CustomModularRealmAuthenticator
<3>在安全管理器securityManager中加载配置
// 自定义Realm验证(针对多Realm认证),加载自定义的Realm列表
List<Realm> realms = new ArrayList<>();
realms.add(adminRealm());
realms.add(userRealm());
securityManager.setAuthenticator(modularRealmAuthenticator()); // 需要realm定义之前
securityManager.setRealms(realms);
对比单Realm:securityManager.setRealm(adminRealm());
问题3:如何进行校验?
在登录入口中定义:
UsernamePasswordToken token = new CustomToken(loginAccount, loginPassword, loginType);
随后shiro注入认证的时候则会根据loginType调用相应的xxxRealm方法进行不同类型用户的登录认证
通过打断点的方式去测试shiro的运行机制和常见的问题处理
问题4:如何配置公共的部分(例如不同登录类型的用户,如何统一获取)
方式1:缓存中只存储userNum(用于唯一标识该用户的属性),随后提供相应的方法获取数据。这种方式则可考虑将不同的登录用户验证放在同一个方法中,随后返回一个String值用于存储到缓存中
方式2:缓存中存放当前登录用户的相关属性。如果不同登录类型的用户有公共的属性,则可参考方式一,唯一不同的是用一个自定义通用的实体类去接收用户属性(LoginUser)
如果不同登录类型的用户属性差异很大,且需要用不同的实体接收,则考虑拆分不同的登录验证方法,随后业务编写处理的时候自定义强制转化数据
问题5:多realm的认证问题
登录认证和授权认证是两种不同的概念,因此在配置登录认证之后,需要考虑结合实际需求匹配相应的授权认证。否则,默认走指定的realm而非对应的realm授权,导致问题错乱
在多类型用户整合配置中可考虑用JSONObject接收登录用户信息实体(亦可根据实际需求额外定义多个类型实体),使用JSONObject接收内容具备通用性
多realm授权认证说明:
<1>自定义CustomModularRealmAuthorizer
<2>xxxRealm中配置name属性:
@Override
public String getName() {
return LoginTypeEnum.ADMIN.getType();
}
<3>shiroConfig配置文件中SecurityManager管理器配置中加载自定义的授权认证配置
如果认证权限没有走xxxRealm,检查CustomModularRealmAuthorizer配置
检查分支配置是否正常
//匹配名字
if (realmName.equals(LoginTypeEnum.ADMIN.getType())){
if (realm instanceof AdminRealm) {
return ((AdminRealm) realm).isPermitted(principals, permission);
}
}
测试:使用不同的账号登录测试校验
如果出现问题:
{
"errCode": -1,
"errMsg": "未知的运行时异常:Configuration error: No realms have been configured! One or more realms must be present to execute an authorization operation.",
"extend": {}
}
必须保证配置顺序:否则出现上述错误
token配置说明
Token实现:
【3】测试校验
测试说明:登录的时候不指定auth_token
访问接口的时候指定auth_token,如果没有指定,则从session中获取sessionId进行匹配;如果指定了auth_token,则根据auth_token访问接口
跨域的时候每次请求的sessionId都不太一样
b.扩展说明
【1】缓存概念
缓存配置说明
<1>在resources文件夹下引入config/xxx.xml(ehcache-shiro.xml)配置文件(存放缓存配置)
<2>shiroConfig配置文件中的securityManager中引入缓存管理器配置
/**
* shiro缓存管理器配置
**/
@Bean(name = "multi-ehCacheManager")
public EhCacheManager ehCacheManager() {
// 注入自定义的shiro缓存管理器(指定配置文件存储位置)
EhCacheManager ehCacheManager = new EhCacheManager();
ehCacheManager.setCacheManagerConfigFile("classpath:config/ehcache-shiro.xml");
return ehCacheManager;
}
需要注意的是,如果变动了相应的角色和权限,则相应需要清空子系统的数据缓存,以实时同步更新数据,
用户角色权限未同步的原因可能有:
<1>用户账号信息更新未同步导致,在权限认证的时候以当前登录的用户信息为基础配置缓存数据
即当用户没有重新登录,导致权限认证的时候始终是以旧的账号信息和角色信息为验证基础
<2>权限缓存配置未更新
在没有引入缓存的时候,每次接口访问都需要调用方法校验权限缓存;引入缓存之后,会将权限认证的数据进行缓存,因此除非重新登录访问进行权限验证,或者是待缓存数据失效之后再进行访问
<3>需考虑问题
数据信息变更如何及时更新缓存,如果不在同一个项目中的工程如何去限制?(单点登录系统中具体内容待考虑、待完善)
shiro权限验证(此前每次访问都需要实时校验权限),引入缓存机制,当缓存失效的时候方进行权限校验(可通过打印日志或者是设置断点分析)
name:缓存名称。
maxElementsInMemory:缓存最大数目
maxElementsOnDisk:硬盘最大缓存个数。
eternal:对象是否永久有效,一但设置了,timeout将不起作用。
overflowToDisk:是否保存到磁盘,当系统当机时
timeToIdleSeconds:设置对象在失效前的允许闲置时间(单位:秒)。仅当eternal=false对象不是永久有效时使用,可选属性,默认值是0,也就是可闲置时间无穷大。
timeToLiveSeconds:设置对象在失效前允许存活时间(单位:秒)。最大时间介于创建时间和失效时间之间。仅当eternal=false对象不是永久有效时使用,默认是0.,也就是对象存活时间无穷大。
diskPersistent:是否缓存虚拟机重启期数据 Whether the disk store persists between restarts of the Virtual Machine. The default value is false.
diskSpoolBufferSizeMB:这个参数设置DiskStore(磁盘缓存)的缓存区大小。默认是30MB。每个Cache都应该有自己的一个缓冲区。
diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认是120秒。
memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。默认策略是LRU(最近最少使用)。你可以设置为FIFO(先进先出)或是LFU(较少使用)。
clearOnFlush:内存数量最大时是否清除。
memoryStoreEvictionPolicy:
Ehcache的三种清空策略;
FIFO,first in first out,这个是大家最熟的,先进先出。
LFU, Less Frequently Used,就是上面例子中使用的策略,直白一点就是讲一直以来最少被使用的。如上面所讲,缓存的元素有一个hit属性,hit值最小的将会被清出缓存。
LRU,Least Recently Used,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
配置参考
<?xml version="1.0" encoding="UTF-8"?>
<ehcache name="es">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
/>
<!-- 登录记录缓存锁定10分钟 -->
<cache name="passwordRetryCache"
maxEntriesLocalHeap="2000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
【2】remember设定
在Shiro配置类中引入rememberMe配置
// ------------------------------- rememberMe配置相关 ------------------------------
/**
* rememberMe配置相关:设置cookie对象
**/
@Bean(name = "single-rememberMeCookie")
public SimpleCookie rememberMeCookie() {
logger.info("设置Cookie对象(rememberMeCookie)");
// 这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
SimpleCookie simpleCookie = new SimpleCookie("cfa-admin-single-rememberMe");
// 记住我cookie生效时间30天 ,单位秒
simpleCookie.setMaxAge(259200);
return simpleCookie;
}
/**
* rememberMe配置相关:cookie管理对象
**/
@Bean(name = "single-cookieRememberMeManager")
public CookieRememberMeManager cookieRememberMeManager() {
logger.info("Cookie管理对象:cookieRememberMeManager()");
CookieRememberMeManager manager = new CookieRememberMeManager();
manager.setCookie(rememberMeCookie());
return manager;
}
// ------------------------------------ 安全管理器 ------------------------------------
/**
* 安全管理器-权限管理,配置主要是Realm的管理认证
**/
@Bean(name = "single-securityManager")
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 自定义的shiro session缓存管理器
securityManager.setSessionManager(sessionManager());
// 自定义Cache实现
securityManager.setCacheManager(ehCacheManager());
// 自定义Cookie(实现rememberMe)
securityManager.setRememberMeManager(cookieRememberMeManager());
// 针对单个Realm认证(放到最后)
securityManager.setRealm(shiroRealm());
return securityManager;
}
登录验证token定义
// param说明:username、password、rememberMe
UsernamePasswordToken token = new UsernamePasswordToken("admin", "0",true);
【3】shiro异常处理
shiro异常说明:https://blog.csdn.net/tyjlearning/article/details/91489174
try {
// 登录验证
user = loginService.loginByLocal(username,password);
}
catch (CaptchaException e)
{
throw new AuthenticationException(e.getMessage(), e);
}
catch (UserNotExistsException e)
{
throw new UnknownAccountException(e.getMessage(), e);
}
catch (UserPasswordNotMatchException e)
{
throw new IncorrectCredentialsException(e.getMessage(), e);
}
catch (UserPasswordRetryLimitExceedException e)
{
throw new ExcessiveAttemptsException(e.getMessage(), e);
}
catch (UserBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
catch (RoleBlockedException e)
{
throw new LockedAccountException(e.getMessage(), e);
}
问题说明:使用自定义异常,虽然抛出了BOXING异常,但是被SHIRO框架的异常拦截器拦截了,导致最终处理不管何种情况都抛出AuthenticationException作为处理结果,前端不便于验证何种异常情况,因此在登录验证的时候可以考虑不使用自定义异常,而是使用AuthenticationException的子类进行扩展。如果需要返回指定的说明,则可在全局异常拦截器中进行配置:针对不同的子类异常处理返回自定义的异常处理结果
service层负责返回查询数据,在xxxRealm中统一处理账号的异常情况(AuthenticationException) 捕获异常,统一处理为AccountException;在xxxRealm中捕获Shiro相关异常使用自定义异常器拦截进行处理:在调用service此方法中跑出自定义异常,此处捕获自定义异常随后抛出Shiro相关异常并在自定义异常配置中进行处理
// 在异常拦截器中处理;(替换相应的提示信息)GlobalExceptionHandler
/**
* shiro相关异常过滤:密码校验异常
**/
@ExceptionHandler(IncorrectCredentialsException.class)
public AjaxResult handleIncorrectCredentialsException(IncorrectCredentialsException e) {
log.error(e.getMessage(), e);
return AjaxResultUtil.error(ResultEnum.LOCAL_VALID_ERROR);
}
@ExceptionHandler(AuthenticationException.class)
public AjaxResult handleAuthenticationException(AuthenticationException e) {
// 如果是AccountException相关
if (e instanceof AccountException) {
// 打印日志信息
log.error(e.toString(), e);
return AjaxResultUtil.error(ResultEnum.AUTH_ERROR.getCode(),e.getMessage());
}else{
log.error(e.getMessage(), e);
return AjaxResultUtil.error(ResultEnum.LOGIN_FAIL_ERROR);
}
// return AjaxResultUtil.error(ResultEnum.SERVER_ERROR);
}
3.常见问题
a.认证特殊情况处理
(1)多类型用户登录认证出错
在同一个浏览器中登录不同类型的用户可能出现异常
如果是同一个浏览器中登录用户,由于Admin验证和User验证的类型是不一样的:当使用USER登录后在没有退出登录的情况下如果再次ADMIN登录验证则会报错(由于类型转换错误),正常退出或者是清理缓存之后再次尝试登录即可
调整代码逻辑,对不同类型用户进行不同的校验处理(使用JSONObject统一接收用户实体)
同一浏览器中不同用户登录控制:踢出上一登录用户
如果是使用token存储:每次登录返回不同的tokenId,以tokenId作为识别令牌,用户可以在统一浏览器中登录多个账号,数据信息互不干扰
如果是使用sessionId,在同一个浏览器中,始终共用的是一个内容,导致出现登录新的账号会将上一个账号信息给挤掉
**问题说明:**POSTMAN测试与实际浏览器测试结果不同-如果是使用token存储:不同用户每次登录还是返回相同的tokenId,但实际内容已经被替换(原有用户已被踢出)
postman测试举例说明:
A先登录返回tokenId:123456
随后B再次登录返回同样的tokenId:123456
随后C再次登录返回同样的tokenId:123456
每次不同用户登录均会覆盖上一次登录的用户信息
实际浏览器-如果是使用token存储:可能由于前端每次指定不同的sessionId,后台还是将其当做’不同浏览器‘登录处理,因此每次登录还是返回不同的tokenId,不同的tokenId限定下用户便可通过当前指定的tokenId进行数据交互
浏览器(前端控制)举例说明:
A先登录返回tokenId:123456
随后B再次登录返回不同的tokenId:234567
随后C再次登录返回不同的tokenId:345678
由于前端传入随机sessionId,每次登录成功都会返回不同的tokenId,因此在同一个浏览器中后台认为这是不同的三个用户,各自可以根据各自的tokenId与后台进行交互
为了验证这个猜想,可以在当前浏览器中再次登录A的账号:此时后台处理应该是校验重复登录的情况,因此可以看到前面登录的页面已经失效,后面登录的页面启用。进一步说明每次登录请求前端(与浏览器相同无关)均生成了不同的sessionId传入了后台,而tokenId是校验身份的唯一标准
多类型登录用户(Admin、User)共用session集合导致出错
当登录了Admin在没有指定正常退出操作的时候,登录USER账号会导致数据转换异常(数据强制转化报错),虽然根据不同类型判断调用不同的realm,但由于共用同一个session容器,导致循环遍历处理的时候报错。(Admin不能强制转化为User、User不能强制转化为Admin)
解决方式1:定义一个LoginUser将公共的属性封装起来所有登录用户均用这个实体类去接收处理;即如果有公共属性则定义一个公共的类去处理数据,可以将登录信息转化为json数据存储,随后根据不同的登陆类型处理相应的参数
LoginController:
SecurityUtils.getSubject().getSession().setAttribute("currentUser", JSON.toJSONString(subject.getPrincipal()));
// 不同浏览器登录:校验同一用户重复登录的情况
// User currentUser = (Map<String,Object>) (session.getAttribute("currentUser"));
Map currentUser =JSON.parseObject((String)session.getAttribute("currentUser"),Map.class);
if (currentUser != null) {
// 校验不同浏览器条件下同一用户是否重复登录的情况:用户编号或ID相同则认为是同一个用户
if (currentUser.get("userNum").equals(loginUser.getUserNum())) {
log.info(currentUser.get("userName") + "已在某处重复登录,剔除中...");
sessionManager.getSessionDAO().delete(session);
}
}
// 共用容器存储不同类型的登录用户信息,当转化后的实体没有userNum属性则会报空指针异常,因此需要调换顺序处理空指针异常
if (loginUser.getUserNum().equals(currentUser.get("userNum")))
解决方式2:将后台管理员登录信息存储和系统用户登录信息存储拆分开来,使用不同的容器进行存储,如此一来不会相互影响,但这种情况在统一浏览器登录后台管理员和系统用户的账号信息不会相互冲突挤掉(也符合系统设计逻辑),但需要注意数据功能拦截控制(毕竟是访问同一个应用系统)
在LoginController中根据不同的登录用户类型进行处理(将不同的登录用户放置在不同的容器中)
数据还是存储在session中,只是赋予不同的属性配置:不同的XXXRealm从不同的session属性中获取相应的登录用户数据
b.其他问题
(1)Shiro权限拦截失效问题
参考链接:https://www.jb51.net/article/166917.htm
排查aop拦截配置:spring-boot-starter-aop没有引入导致
排查权限注解定义
检查在Controller接口定义处检查shiro角色、权限的注解配置
(2)前后端跨域请求Shiro验证失败
参考链接:https://my.oschina.net/sprouting/blog/3059282
shiro下自定义过滤器失效问题:(例如跨域请求失效)
(3)No SecurityManager accessible
Shiro 报错 No SecurityManager accessible(汇总):
https://blog.csdn.net/happylee6688/article/details/43304247
考虑是shiro没有配置就直接引用了,查看ShiroConfig配置,加入@Configuration配置注解