SpringBoot整合shiro、自定义sessionManager

       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/login
    private String sysAuthPermission; //权限的的名称例如 user:login
    private 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的注解。如果有知道原因的可以留言。谢谢
    @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的关键部分

        这里要配置的是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 {
    @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;
    }

       这里@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);

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

        }

    }
}

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,最近最少使用的,缓存的元素有一个时间戳,当缓存容量满了,而又需要腾出地方来缓存新的元素的时候,那么现有缓存元素中时间戳离当前时间最远的元素将被清出缓存。
    -->
    <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

       传统结构项目中,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();
    }

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

猜你喜欢

转载自blog.csdn.net/qq_34996727/article/details/81133729