1.基础数据
首先引入shiro依赖jar包
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.4.0</version> </dependency> <!--shiro缓存插件--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
这里贴三张表的字段设计
public class SysUser { private Integer userId; private String userAccount;//用户账号 private String userPassword;//用户密码 }
public class SysRole { private Integer sysRoleId; private Byte sysRoleAva; //角色是否生效 private String sysRoleDes;//角色描述 private String sysRoleName;//角色名称 }
public class SysAuth { private Integer sysAuthId; private String sysAuthCode; //权限编号 private String sysAuthName; //权限名称 private String sysAuthUrl; //权限请求的url 例如: user/login private String sysAuthPermission; //权限的的名称例如 user:login private Byte sysAuthAva; //权限是否有效 private Byte sysAuthType; //权限类型。菜单还是按钮 private String sysAuthDes; //权限描述 }
import com.lingjiugis.ocr.domain.SysAuth; import com.lingjiugis.ocr.domain.SysRole; import com.lingjiugis.ocr.domain.SysUser; import com.lingjiugis.ocr.service.SysAuthService; import com.lingjiugis.ocr.service.SysRoleService; import com.lingjiugis.ocr.service.UserService; import org.apache.commons.lang.exception.ExceptionUtils; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Resource; import java.util.List; public class ShiroRealm extends AuthorizingRealm { private static Logger logger = LoggerFactory.getLogger(ShiroRealm.class); //这里尝试过使用@Autowired 但是发现会报错。这个是spring的注解。如果有知道原因的可以留言。谢谢 @Resource private UserService userService; @Resource private SysRoleService sysRoleService; @Resource private SysAuthService authService; /** * 配置权限 注入权限 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals){ System.out.println("--------权限配置-------"); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); SysUser user = (SysUser) principals.getPrimaryPrincipal(); try { //注入角色(查询所有的角色注入控制器) List<SysRole> list = sysRoleService.selectRoleByUser(user.getUserId()); for (SysRole role: list){ authorizationInfo.addRole(role.getSysRoleName()); } //注入角色所有权限(查询用户所有的权限注入控制器) List<SysAuth> sysAuths = authService.queryByUserId(user.getUserId()); for(SysAuth sysAuth:sysAuths){ authorizationInfo.addStringPermission(sysAuth.getSysAuthPermission()); } }catch (Exception e){ e.printStackTrace(); logger.error(ExceptionUtils.getFullStackTrace(e)); } return authorizationInfo; } /** * 用户验证 * @param token 账户数据 * @return * @throws AuthenticationException 根据账户数据查询账户。根据账户状态抛出对应的异常 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户的输入的账号 String username = (String) token.getPrincipal(); //这里需注意。看别人的教程有人是这样写的String password = (String) token.getCredentials(); //项目运行的时候报错,发现密码不正确。后来进源码查看发现将密码注入后。Shiro会进行转义将字符串转换成字符数组。 //源码:this(username, password != null ? password.toCharArray() : null, false, null); //不晓得是否是因为版本的原因,建议使用的时候下载源码进行查看 String password = new String((char[]) token.getCredentials()); //通过username从数据库中查找 User对象,如果找到,没找到. //实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法 SysUser user = userService.selectByAccount(username); if(null == user){ throw new UnknownAccountException(); }else { if(password.equals(user.getUserPassword())){ if(0 == user.getUserState()){ throw new LockedAccountException(); }else if (2 == user.getUserState()){ throw new DisabledAccountException(); }else{ SimpleAuthenticationInfo authorizationInfo = new SimpleAuthenticationInfo(user,user.getUserPassword().toCharArray(),getName()); return authorizationInfo; } } else { throw new IncorrectCredentialsException(); } } } }
1.2 接下来配置Shiro的关键部分
import com.lingjiugis.ocr.config.GlobalExceptionResolver; import com.lingjiugis.ocr.filter.ShiroSessionManager; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.CookieRememberMeManager; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.servlet.SimpleCookie; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerExceptionResolver; import java.util.LinkedHashMap; import java.util.Map; @Configuration public class ShiroConfig { @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) { System.out.println("--------------------shiro filter-------------------"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); shiroFilterFactoryBean.setSecurityManager(securityManager); Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>(); //注意过滤器配置顺序 不能颠倒 //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了,登出后跳转配置的loginUrl // 配置不会被拦截的链接 顺序判断 filterChainDefinitionMap.put("/static/**", "anon"); filterChainDefinitionMap.put("/favicon.ico", "anon"); //拦截其他所以接口 filterChainDefinitionMap.put("/**", "authc"); //配置shiro默认登录界面地址,前后端分离中登录界面跳转应由前端路由控制,后台仅返回json数据 shiroFilterFactoryBean.setLoginUrl("/user/unlogin"); // 登录成功后要跳转的链接 自行处理。不用shiro进行跳转 // shiroFilterFactoryBean.setSuccessUrl("user/index"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * shiro 用户数据注入 * @return */ @Bean public ShiroRealm shiroRealm(){ ShiroRealm shiroRealm = new ShiroRealm(); return shiroRealm; } /** * 配置管理层。即安全控制层 * @return */ @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); return securityManager; } public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){ DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); advisorAutoProxyCreator.setProxyTargetClass(true); return advisorAutoProxyCreator; } /** * 开启shiro aop注解支持 使用代理方式所以需要开启代码支持 * 一定要写入上面advisorAutoProxyCreator()自动代理。不然AOP注解不会生效 * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } }
1.3 修改我们的Controller中的登录请求
// 这里如果不写method参数的话,默认支持所有请求,如果想缩小请求范围,还是要添加method来支持get, post等等某个请求。 @RequestMapping("/login") public String login(HttpServletRequest request, Map<String, Object> map) throws Exception { BaseResponse<String> baseResponse = new BaseResponse<>(); Subject subject = SecurityUtils.getSubject(); //数据库的密码我进行了Md5加密。如果没有进行加密的无需这个 user.setUserPassword(MD5Util.getPwd(user.getUserPassword())); UsernamePasswordToken token = new UsernamePasswordToken(user.getUserAccount(),user.getUserPassword()); try { subject.login(token); //System.out.println(getSession().getId()); baseResponse.success(getSession().getId()); } catch (UnknownAccountException e){ baseResponse.setMsg("用户名不存在"); } catch (IncorrectCredentialsException e){ e.printStackTrace(); baseResponse.setMsg("密码错误"); } catch (LockedAccountException e){ baseResponse.setCode(CodeField.ACCOUNT_NOT_ACTIVAT); baseResponse.setMsg(CodeField.ACCOUNT_NOT_ACTIVAT_MSG); }catch (DisabledAccountException e){ baseResponse.setCode(CodeField.ACCOUNT_BAN); baseResponse.setMsg(CodeField.ACCOUNT_BAN_MSG); } catch (Exception e){ e.printStackTrace(); logger.error(ExceptionUtils.getFullStackTrace(e)); } return baseResponse; }
配置完成了就可以运行起来了。
@RestController @RequestMapping("user") public class UserController(){ /** * 测试 * @return */ @RequestMapping("/test") //拥有此权限的才可以访问 @RequiresPermissions("user:test") //拥有此角色的才可以访问 @RequiresRoles("admin") public BaseResponse test() { BaseResponse baseResponse = new BaseResponse(); baseResponse.setMsg("用户拥有该权限"); return baseResponse; } }
shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
2.异常、缓存
这里选择自定义异常处理。处理全局异常。
2.1 自定义全局异常处理
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.support.spring.FastJsonJsonView; import com.lingjiugis.ocr.response.base.BaseResponse; import org.apache.commons.lang.exception.ExceptionUtils; import org.apache.shiro.authz.UnauthorizedException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.HashMap; import java.util.Map; /** * Description: 全局异常处理 * * @author zlp * @create 2018-05-24 11:13 **/ public class GlobalExceptionResolver implements HandlerExceptionResolver { private static Logger logger = LoggerFactory.getLogger(GlobalExceptionResolver.class); @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { ModelAndView mv; //进行异常判断。如果捕获异常请求跳转。 if(ex instanceof UnauthorizedException){ mv = new ModelAndView("/user/unauth"); return mv; }else { mv = new ModelAndView(); FastJsonJsonView view = new FastJsonJsonView(); BaseResponse baseResponse = new BaseResponse(); baseResponse.setMsg("服务器异常"); ex.printStackTrace(); logger.error(ExceptionUtils.getFullStackTrace(ex)); Map<String,Object> map = new HashMap<>(); String beanString = JSON.toJSONString(baseResponse); map = JSON.parseObject(beanString,Map.class); view.setAttributesMap(map); mv.setView(view); return mv; } } }
在前面配置的ShiroConfig添加如下代码块
/** * 注册全局异常处理 * @return */ @Bean(name = "exceptionHandler") public HandlerExceptionResolver handlerExceptionResolver(){ return new GlobalExceptionResolver(); }
首先在项目配置包中写如缓存配置文件ehcache-shiro.xml
<!--配置文件来源于网络。具体实际配置要参照配置文档。进行合理配置--> <?xml version="1.0" encoding="UTF-8"?> <ehcache name="es"> <diskStore path="java.io.tmpdir"/> <!-- 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,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。 --> <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>
然后修改ShiroConfig
//添加方法 /** * 开启缓存 * shiro-ehcache实现 * @return */ @Bean public EhCacheManager ehCacheManager() { System.out.println("ShiroConfiguration.getEhCacheManager()"); EhCacheManager ehCacheManager = new EhCacheManager(); ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return ehCacheManager; } //修改securityManager方法。 /** * 配置管理层。即安全控制层 * @return */ @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); //自定义缓存实现 securityManager.setCacheManager(ehCacheManager()); return securityManager; }
现在主流的缓存插件为Redis。但是我进行配置的时候总是会报数据源异常。因为网上用的连接池大部分都是阿里的druid。而我的项目使用的是springboot默认的连接池,配置不同。
3.自定义sessionManager
import org.apache.shiro.web.servlet.ShiroHttpServletRequest; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.apache.shiro.web.util.WebUtils; import org.springframework.util.StringUtils; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.Serializable; /** * Description:shiro框架 自定义session获取方式 * 可自定义session获取规则。这里采用ajax请求头authToken携带sessionId的方式 * * @author zlp * @create 2018-05-24 10:04 **/ public class ShiroSessionManager extends DefaultWebSessionManager { private static final String AUTHORIZATION = "authToken"; private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request"; public ShiroSessionManager(){ super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response){ String id = WebUtils.toHttp(request).getHeader(AUTHORIZATION); System.out.println("id:"+id); if(StringUtils.isEmpty(id)){ //如果没有携带id参数则按照父类的方式在cookie进行获取 System.out.println("super:"+super.getSessionId(request, response)); return super.getSessionId(request, response); }else{ //如果请求头中有 authToken 则其值为sessionId request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,REFERENCED_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,id); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,Boolean.TRUE); return id; } } }
然后修改ShiroConfig 类。将自定义的ShiroSessionManager 注入管理器中
//添加bean /** * 自定义sessionManager * @return */ @Bean public SessionManager sessionManager(){ ShiroSessionManager shiroSessionManager = new ShiroSessionManager(); //这里可以不设置。Shiro有默认的session管理。如果缓存为Redis则需改用Redis的管理 shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); return shiroSessionManager; } //修改securityManager()方法 /** * 配置管理层。即安全控制层 * @return */ @Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(shiroRealm()); //自定义session管理 securityManager.setSessionManager(sessionManager()); //自定义缓存实现 securityManager.setCacheManager(ehCacheManager()); return securityManager; }