实际开发中必要加入缓存机制,这里使用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;
}
}