Shiro学习笔记(三)Spring Boot + Shiro

本文介绍了如何在Spring Boot项目中整合Shiro框架,包括自定义ShiroConfig配置类,配置bean,创建自定义Realm类进行认证与授权,使用Redis作为缓存管理器,以及自定义过滤器。详细阐述了 Realm 中的认证和授权逻辑,以及如何通过注解简化权限管理。此外,还展示了如何自定义ShiroAuthcFilter和ShiroPermsFilter以增强功能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实际开发中必要加入缓存机制,这里使用redis作为缓存,主要使用的依赖包如下(redis):

<dependency>
    <groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>
<dependency>
	<groupId>org.crazycake</groupId>
	<artifactId>shiro-redis</artifactId>
	<version>3.2.3</version>
</dependency>

 

1.自定义一个ShiroConfig类

@Configuration
public class RedisShiroConfig {

}

注解@Configuration是Spirng Boot采用配置类的方式所必须的,类的作用相当于xml配置文件中的<beans>

 

2.在配置类里添加bean

Spring Boot项目中,配置类相当于<beans>,而类中的方法就相当于<bean>,并且只需要在方法上加一个注解@Bean就可以了。而在官网中的文档我们已知,普通Spring的项目中applicationContext.xml配置文件的shiro核心配置是一个名为shiroFilter的bean,它的依赖的类是org.apache.shiro.spring.web.ShiroFilterFactoryBean,因此定义一个返回值类型是ShiroFilterFactoryBean的方法。

@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
	// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
	// shiroFilterFactoryBean.setLoginUrl("/login");
	// 登录成功后要跳转的链接
	// shiroFilterFactoryBean.setSuccessUrl("/index");
	// 未授权界面;
	// shiroFilterFactoryBean.setUnauthorizedUrl("/403");
	// 设置过滤方法
	// Map<String, Filter> filters = new HashMap<String, Filter>();
	// filters.put("authc", new ShiroAuthcFilter());
	// filters.put("perms", new ShiroPermsFilter());
	// shiroFilterFactoryBean.setFilters(filters);

    // 拦截器.
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
    // 配置不会被拦截的链接 顺序判断
    filterChainDefinitionMap.put("/**", "anon");

    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

这里有很多的配置,包括指定特定操作的的URL、过滤器、拦截器等,并且还包含了一个shiro的核心控制器SecurityManager,因为这个安全管理器是shiro协调内部组件的关键。而我这里直接将其作为方法的参数,也是利用了Spring的注入的便利,只要声明了这个Bean,它会自动作为参数被注入进来。所以接下来要在配置类中声明一个安全管理器。

 

我这里使用默认的安全管理器org.apache.shiro.web.mgt.DefaultWebSecurityManager。需要声明一个返回值类型为org.apache.shiro.mgt.SecurityManage的方法。

@Bean
public SecurityManager securityManager(MyShiroRealm myShiroRealm, RedisCacheManager cacheManager,
			SessionManager sessionManager) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(myShiroRealm);
    securityManager.setCacheManager(cacheManager);
    securityManager.setSessionManager(sessionManager);
    return securityManager;
}

其中我只配置有三个组件,Realm、CacheManager和SessionManager。Realm是连接shiro和数据库的一个桥梁,主要进行授权和认证的操作,而这个需要根据自身的业务需求重写里面的方法,所以首先还是说明一下缓存和session。

 

这里的CacheManager主要是针对shiro的一些需要经常使用的数据来进行快速的存取,需要声明一个返回值类型为org.apache.shiro.cache.CacheManager的方法。其实这部分是需要进行redis存储的一些自定义的,但我这里已开始引用shiro-redis的包,所以直接使用里面写好的类org.crazycake.shiro.RedisCacheManager。这是一个实现类,实现的是shiro框架中的这个org.apache.shiro.cache.CacheManager接口。

SessionManager主要是进行一个会话所需的数据的快速存取,需要声明一个返回值类型为org.apache.shiro.session.mgt.SessionManager的方法。这里使用默认的session管理器org.apache.shiro.web.session.mgt.DefaultWebSessionManager来实现。SessionManager需要配置RedisSessionDao和SessionListeners,Dao可直接使用shiro-redis的org.crazycake.shiro.RedisSessionDAO这个类。Listeners需要自定义监听器来对session来进行监听操作,而最简单的定义方式就是自定义一个实现类来实现org.apache.shiro.session.SessionListener这个接口的方法。下面我就贴一下我自己的监听器的类和缓存、session的bean方法。

	/**
	 * session管理器(使用redis)
	 */
	@Bean
	public SessionManager sessionManager(RedisManager redisManager) {
		DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
		RedisSessionDAO redisSessionDao = new RedisSessionDAO();
		redisSessionDao.setRedisManager(redisManager);
		sessionManager.setSessionDAO(redisSessionDao);
		// 配置监听
		Collection<SessionListener> listeners = new ArrayList<SessionListener>();
		listeners.add(new ShiroSessionListener());
		sessionManager.setSessionListeners(listeners);
		return sessionManager;
	}

	/**
	 * 缓存管理器(使用redis)
	 */
	@Bean
	public RedisCacheManager cacheManager(RedisManager redisManager) {
		RedisCacheManager redisCacheManager = new RedisCacheManager();
		redisCacheManager.setRedisManager(redisManager);
		return redisCacheManager;
	}
package com.gnz48....;

import java.util.concurrent.atomic.AtomicInteger;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.SessionListener;

/**
 * shiro的session监听
 */
public class ShiroSessionListener implements SessionListener {

	private final AtomicInteger sessionCount = new AtomicInteger(0);

	@Override
	public void onStart(Session session) {
		// 会话创建,在线人数加一
		sessionCount.incrementAndGet();
		System.out.println("创建session会话,人数+1");
		System.out.println("总在线人数:" + sessionCount.get());
	}

	@Override
	public void onStop(Session session) {
		// 会话退出,在线人数减一
		sessionCount.decrementAndGet();
		System.out.println("关闭session会话,人数-1");
		System.out.println("总在线人数:" + sessionCount.get());
	}

	@Override
	public void onExpiration(Session session) {
		// 会话过期,在线人数减一
		sessionCount.decrementAndGet();
		System.out.println("session会话过期,人数-1");
		System.out.println("总在线人数:" + sessionCount.get());
	}

	/**
	 * 获取在线人数
	 * @return
	 */
	public Integer getSessionCount() {
		return this.sessionCount.get();
	}

}

监听session的操作不仅限于记录在线用户数,可自行编写多个监听器,进行不同的业务操作。

 

3.自定义Realm类

然后接下来是自定义的Realm,这里选择直接继承org.apache.shiro.realm.AuthorizingRealm来重写认证和授权的方法。代码如下:

package com.gnz48.zzt....;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.credential.AllowAllCredentialsMatcher;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import com.gnz48.zzt.entity.system.Permission;
import com.gnz48.zzt.entity.system.Role;
import com.gnz48.zzt.entity.system.User;
import com.gnz48.zzt.exception.shiro.ActivationAccountException;
import com.gnz48.zzt.repository.system.UserRepository;

/**
 * @Description: shiro自定义Realm
 *               <p>
 *               1、检查提交的进行认证的令牌信息。
 *               <p>
 *               2、根据令牌信息从数据源(通常为数据库)中获取用户信息。
 *               <p>
 *               3、对用户信息进行匹配验证。
 *               <p>
 *               4、验证通过将返回一个封装了用户信息的AuthenticationInfo实例。
 *               <p>
 *               5、验证失败则抛出异常信息。
 */
public class MyShiroRealm extends AuthorizingRealm {

	private Logger log = LoggerFactory.getLogger(MyShiroRealm.class);

	@Autowired
	private UserRepository userRepository;

	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
		User user = (User) principals.getPrimaryPrincipal();
//		User user = userRepository.findByUsername(username);
		for (Role role : user.getRoles()) {
			authorizationInfo.addRole(role.getRole());
			for (Permission p : role.getPermissions()) {
				authorizationInfo.addStringPermission(p.getPermission());
			}
		}
		return authorizationInfo;
	}

	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
		// 获取用户的输入的账号.
		String username = (String) token.getPrincipal();
		// 通过username从数据库中查找 User对象,如果找到,没找到.
		// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
		User user = null;
		try {
			user = userRepository.findByUsername(username);
		} catch (Exception e) {
			log.info("查询用户异常:{}", e.getMessage());
		}
		log.info("认证用户:{}", username);
		Subject subject = SecurityUtils.getSubject();
		Session session = subject.getSession();
		session.setAttribute("loginName", username);
		if (user == null) {
			// 用户不存在
			throw new UnknownAccountException();
		} else if (user.getState() == 2) {
			// 用户被锁定
			throw new LockedAccountException();
		} else if (user.getState() == 0) {
			// 用户未激活
			throw new ActivationAccountException();
		} else {
			SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, // 用户名
					user.getPassword(), // 密码
					//ByteSource.Util.bytes(user.getUsername() + user.getSalt()), // salt=username+salt
					getName() // realm name
			);
			return authenticationInfo;
		}
	}

}

由此可见,授权方法主要是对用户所持有的资源进行查询,然后存储到SimpleAuthorizationInfo中,包括Role和Permission,这些资源会被缓存到先前配置redis中,而后每次判断用户权限时,就不必再到数据库执行一边select了,增加了系统响应的效率。

认证主要是对用户进行登录所携带的token进行匹配验证,这里使用的是账号和密码构成的token进行验证。当有验证不通过时,直接抛出对应异常,在登录的Controller中执行的subject.login(token)进行异常捕捉,返回对应的结果即可。

在Realm中,还能自定义密码的加密方式、token的匹配方式等,都是在以上基础进行继承和重写等方式来自定义的,可以说非常容易将shiro框架进行高度定制。

 

4.注解

针对一些业务,需要给每个接口赋一个资源,如不想在过滤器中配置大量的URL,可以启用注解,在Controller中的URL映射方法上添加注解也可达到一样的效果。

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
	AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
	authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
	return authorizationAttributeSourceAdvisor;
}

注解包括:@RequiresRoles、@RequiresPermissions、@RequiresAuthentication、@RequiresUser和@RequiresGuest。

 

5.自定义过滤器

在shiroFilter的Bean中,我注释掉了一个名为filters的Map,这个Map的作用就是用来存放自定义过滤器的。shiro的过滤器主要就是对你配置了拦截的URL进行过滤处理的类。

例1:

声明一个名为authc的过滤器

// 设置过滤方法
Map<String, Filter> filters = new HashMap<String, Filter>();
filters.put("authc", new ShiroAuthcFilter());
shiroFilterFactoryBean.setFilters(filters);

拦截的URL

// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/index", "authc");// 首页跳转url
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

如上配置,当你访问/index的时候,请求会进入到我自定义的ShiroAuthcFilter这个类中,执行其中的操作。

 

例2:

声明一个名为perms的过滤器

// 设置过滤方法
Map<String, Filter> filters = new HashMap<String, Filter>();
filters.put("perms", new ShiroPermsFilter());
shiroFilterFactoryBean.setFilters(filters);

拦截的URL

// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/member/update/room-monitor", "perms[member:update]");// 修改成员房间监控状态接口
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);

如上配置,当你访问/member/update/room-monitor的时候,请求会进入到我自定义的ShiroPermsFilter这个类中,执行其中的操作。perms[]为固定值,[]中的参数才是变量,而这个变量来源就是Realm中授权时添加的值。

像是例1的authc和例2的perms这两种过滤,其实都是shiro的特殊过滤,就算不用新建自定义也能正常实现URL的拦截,只是我这里在其中增加了一些自定义的功能,于是对其进行了继承重写。shiro提供了非常多的基本过滤器供开发者进行定制开发,继承后重写其中的onAccessDenied和isAccessAllowed两个方法即可。

我自定义的ShiroAuthcFilter类继承了org.apache.shiro.web.filter.AccessControlFilter,它的作用是对游客身份的URL请求进行拦截,只有登录后才会放行。

ShiroPermsFilter类继承了org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter,它的作用是对资源的许可权进行拦截,只有当你在Realm的授权中获取到了这个资源的参数,才能匹配成功放行你的请求。

 

ShiroAuthcFilter:

package com.gnz48.zzt....;

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;
import com.gnz48.zzt.vo.ResultVO;

/**
 * @Description: Authc的自定义过滤器
 */
public class ShiroAuthcFilter extends AccessControlFilter {

	@SuppressWarnings("unused")
	private Logger log = LoggerFactory.getLogger(ShiroAuthcFilter.class);

	@Override
	protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse)
			throws IOException {
		// log.info("----->> shiro-Authc过滤");
		Subject subject = SecurityUtils.getSubject();
		// 判断subject当前会话是否能提供证书认证
		if (subject.isAuthenticated()) {
			return true;
		}
		HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
		HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
		String requestedWith = httpServletRequest.getHeader("X-Requested-With");
		// 判断是否ajax请求
		if (requestedWith != null && requestedWith.equals("XMLHttpRequest")) {// 如果是ajax请求返回指定数据
			httpServletResponse.setCharacterEncoding("UTF-8");
			httpServletResponse.setContentType("application/json");
			ResultVO result = new ResultVO();
			result.setStatus(403);
			result.setCause("请先登录后再进行该操作");
			httpServletResponse.getWriter().write(JSONObject.toJSONString(result));
		} else {// 不是ajax进行重定向处理
			httpServletResponse.sendRedirect("/login");
		}
		return false;
	}

	@Override
	protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue)
			throws Exception {
		return false;
	}

}

ShiroPermsFilter:

package com.gnz48.zzt...;

import java.io.IOException;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSONObject;
import com.gnz48.zzt.vo.ResultVO;

/**
 * @Description: Perms的自定义过滤器
 */
public class ShiroPermsFilter extends PermissionsAuthorizationFilter {

	@SuppressWarnings("unused")
	private Logger log = LoggerFactory.getLogger(ShiroPermsFilter.class);

	protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
		// log.info("----->> shiro-Perms过滤");
		HttpServletRequest httpServletRequest = (HttpServletRequest) request;
		HttpServletResponse httpServletResponse = (HttpServletResponse) response;
		String requestedWith = httpServletRequest.getHeader("X-Requested-With");
		Subject subject = SecurityUtils.getSubject();
		// 判断是否ajax请求
		if (requestedWith != null && requestedWith.equals("XMLHttpRequest")) {// 如果是ajax请求返回指定数据
			httpServletResponse.setCharacterEncoding("UTF-8");
			httpServletResponse.setContentType("application/json");
			ResultVO result = new ResultVO();
			result.setStatus(403);
			// 判断subject当前会话是否能提供证书认证
			if (subject.isAuthenticated()) {
				result.setCause("无权进行该操作");
			} else {
				result.setCause("请先登录后再进行该操作");
			}
			httpServletResponse.getWriter().write(JSONObject.toJSONString(result));
		} else {// 不是ajax进行重定向处理
			// 判断subject当前会话是否能提供证书认证
			if (subject.isAuthenticated()) {
				httpServletResponse.sendRedirect("/403");
			} else {
				httpServletResponse.sendRedirect("/login");
			}
		}
		return false;
	}

}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值