当前位置: 代码迷 >> 综合 >> SpringBoot整合shiro、自定义sessionManager
  详细解决方案

SpringBoot整合shiro、自定义sessionManager

热度:53   发布时间:2023-10-25 16:09:27.0

       Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。相比较Spring Security,shiro有小巧、简单、易上手等的优点。所以很多框架都在使用shiro。而springboot作为一个开源框架,必然提供了和shiro整合的功能!接下来就用springboot整合shiro完成对于用户登录的判定和权限的验证.

1.基础数据

公司项目采用的spring-boot框架。在做用户权限功能的时候准备采用shiro权限框架。前面也考虑过spring家族的spring security安全框架。但是经过网上查询对比最终选择了shiro。因为shiro含有基本的安全控制功能,并且配置更为简单,使用也更加简洁。 
首先引入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>

本次shiro插件缓存功能实现采用的是ehcahe。前面也尝试过使用Redis。但是在配置数据源那块报错,无法解决数据源的问题。所以直接改用了ehcahe。后面如果解决了数据源问题。再发整合Redis的教程。 
首先我在系统建立用户权限关系 
这里我们需要三张表: 
SysUser: 用来存储用户的密码,用户名等等信息。 
SysRole: 角色表,存放所有的角色信息 
SysAuth:权限表,定义了一些操作访问权限信息。 
还有两张关联表(这里我们用JPA自动生成。): 
SysUserRole: SysUser和SysRole的关联表。 
SysRoleAuth:SysRole和SysAuth的关联表。 
这里贴三张表的字段设计

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/loginprivate String sysAuthPermission; //权限的的名称例如 user:loginprivate Byte sysAuthAva; //权限是否有效private Byte sysAuthType; //权限类型。菜单还是按钮private String sysAuthDes; //权限描述
}

1.1 配置Realms 

       Realm是一个Dao,通过它来验证用户身份和权限。这里Shiro不做权限的管理工作,需要我们自己管理用户权限,只需要从我们的数据源中把用户和用户的角色权限信息取出来交给Shiro即可。Shiro就会自动的进行权限判断。在项目包下建一个ShiroRealm类,继承AuthorizingRealm抽象类。

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的注解。如果有知道原因的可以留言。谢谢@Resourceprivate UserService userService;@Resourceprivate SysRoleService sysRoleService;@Resourceprivate SysAuthService authService;/*** 配置权限 注入权限* @param principals* @return*/@Overrideprotected 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 根据账户数据查询账户。根据账户状态抛出对应的异常*/@Overrideprotected 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的关键部分

        这里要配置的是ShiroConfig类,Apache Shiro 核心通过 Filter 来实现,就好像SpringMvc 通过DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。

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 {@Beanpublic 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*/@Beanpublic ShiroRealm shiroRealm(){ShiroRealm shiroRealm = new ShiroRealm();return shiroRealm;}/*** 配置管理层。即安全控制层* @return*/@Beanpublic 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*/@Beanpublic 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;}

       这里@RequestMapping之所以没加method是因为如果用户没登录,Shiro会调用get方法请求/login,而后面我们在login页面会用post请求发送form表单,所以这里就没设置method(默认支持所有请求)。 
配置完成了就可以运行起来了。

1.4 开启接口权限

@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;}
}

       在进行权限校验的时候发现。当用户进行权限判定时。如果用户没有权限则会抛出UnauthorizedException异常,而如我们之前设置的那样进行跳转

shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");

2.异常、缓存

     经过查找发现定义的filter必须满足filter instanceof AuthorizationFilter,只有perms,roles,ssl,rest,port才是属于AuthorizationFilter,而anon,authcBasic,auchc,user是AuthenticationFilter,所以unauthorizedUrl设置后页面不跳转 
解决方法要么就使用perms,roles,ssl,rest,port,要么自己配置异常处理,进行页面跳转。 
这里选择自定义异常处理。处理全局异常。

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);@Overridepublic 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;}}
}

2.2 将自定义的异常处理注入Shiro中 

在前面配置的ShiroConfig添加如下代码块

    /*** 注册全局异常处理* @return*/@Bean(name = "exceptionHandler")public HandlerExceptionResolver handlerExceptionResolver(){return new GlobalExceptionResolver();}

       如果我们登录之后多次访问的话,会发现权限验证会每次都执行一次。这是有问题的,因为像用户的权限这些我们提供给shiro一次就够了。所以我们进行缓存配置。前面已经引入了缓存依赖。所以我们直接贴代码 
首先在项目配置包中写如缓存配置文件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,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。--><defaultCachemaxElementsInMemory="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*/@Beanpublic EhCacheManager ehCacheManager() {System.out.println("ShiroConfiguration.getEhCacheManager()");EhCacheManager ehCacheManager = new EhCacheManager();ehCacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml");return ehCacheManager;}//修改securityManager方法。/*** 配置管理层。即安全控制层* @return*/@Beanpublic SecurityManager securityManager(){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(shiroRealm());//自定义缓存实现securityManager.setCacheManager(ehCacheManager());return  securityManager;}

现在主流的缓存插件为Redis。但是我进行配置的时候总是会报数据源异常。因为网上用的连接池大部分都是阿里的druid。而我的项目使用的是springboot默认的连接池,配置不同。

3.自定义sessionManager

       传统结构项目中,shiro从cookie中读取sessionId以此来维持会话,在前后端分离的项目中(也可在移动APP项目使用),我们选择在ajax的请求头中传递sessionId,因此需要重写shiro获取sessionId的方式。自定义ShiroSessionManager类继承DefaultWebSessionManager类,重写getSessionId方法,

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();}@Overrideprotected 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 则其值为sessionIdrequest.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*/@Beanpublic SessionManager sessionManager(){ShiroSessionManager shiroSessionManager = new ShiroSessionManager();//这里可以不设置。Shiro有默认的session管理。如果缓存为Redis则需改用Redis的管理shiroSessionManager.setSessionDAO(new EnterpriseCacheSessionDAO());return shiroSessionManager;}//修改securityManager()方法/*** 配置管理层。即安全控制层* @return*/@Beanpublic SecurityManager securityManager(){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(shiroRealm());//自定义session管理securityManager.setSessionManager(sessionManager());//自定义缓存实现securityManager.setCacheManager(ehCacheManager());return  securityManager;}