SpringBoot整合Shiro权限控制实战

Shiro是Apache下的一个开源的,强大且易用的Java安全框架,可以执行身份验证、授权、密码和会话管理。相对于SpringSecurity简单的多,也没有SpringSecurity那么复杂。因为作者使用的前后端分离开发模式,引入SpringSecurity还会给前端开发人员一定的工作量和兼容性问题。结合实际情况,最终采用Shiro作为权限控制安全框架。

1.shiro官方架构图

官方架构图

2. 主要功能

2.1 三个核心

(1) Subject

即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。但考虑到大多数的目的和用途,你可以把它认为是Shiro的“用户”概念。Subject代表了当前用户的安全操作,而SecurityManager则管理所有用户的安全操作。

(2) SecurityManager

它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。

(3) Realm

Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。

从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。

Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现。

2.2 相关功能类

1. Authentication:身份认证/登录(账号密码验证)。

2. Authorization:授权,即角色或者权限验证。

3. Session Manager:会话管理,用户登录后的session相关管理。

4. Cryptography:加密,例如密码加密等。

5. Web Support:Web支持,集成Web环境。

6. Caching:缓存。把用户信息、角色、权限等信息缓存到如redis等缓存中。

7. Concurrency:多线程并发验证。在一个线程中开启另一个线程,可以把权限自动传播过去。

8.Web Integration web:系统集成。

9. Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问。

10. Remember Me:记住我,登录后,下次再来的话不用登录了。

11.Interations:集成其它应用,spring、缓存框架。

3. Spring Boot整合Shiro

3.1 pom.xml中添加依赖

        <!-- shiro相关依赖 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.3.2</version>
        </dependency>
        <!-- shiro+redis缓存插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>2.4.2.1-RELEASE</version>
        </dependency>

3.2 Shiro的配置类

@Configuration
public class ShiroConfig {

    /**
     * 配置Shiro核心 安全管理器 SecurityManager
     * SecurityManager安全管理器:所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;负责与后边介绍的其他组件进行交互。(类似于SpringMVC中的DispatcherServlet控制器)
     */
    @Bean
    public SecurityManager securityManager(UserRealm userRealm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //将自定义的realm交给SecurityManager管理
        securityManager.setRealm(userRealm);
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(SessionManager());
        // 使用记住我
        securityManager.setRememberMeManager(rememberMeManager());
        return securityManager;
    }

    /*
  自定义Realm
   */
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    /**
     * 配置Shiro的Web过滤器,拦截浏览器请求并交给SecurityManager处理
     *
     * @return
     */
    @Bean
    public ShiroFilterFactoryBean webFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        //设置securityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //配置拦截链 使用LinkedHashMap,因为LinkedHashMap是有序的,shiro会根据添加的顺序进行拦截
        // Map<K,V> K指的是拦截的url V值的是该url是否拦截
        Map<String, String> filterChainMap = new LinkedHashMap<String, String>(16);
        //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问,先配置anon再配置authc。这里可不配置。
        //filterChainMap.put("/debug/test1", "anon");
      //  filterChainMap.put("/debug/test", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 开启aop注解支持
     * 即在controller中使用 @RequiresPermissions("")
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor attributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        //设置安全管理器
        attributeSourceAdvisor.setSecurityManager(securityManager);
        return attributeSourceAdvisor;
    }


    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }

    /**
     * redisManager
     *
     * @return
     */
    public RedisManager redisManager() {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost("127.0.0.1");
        redisManager.setPort(6379);
        // 配置过期时间 一周
        redisManager.setExpire(604800);
        return redisManager;
    }

    /**
     * cacheManager
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * redisSessionDAO
     */
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

    /**
     * sessionManager
     */
    public DefaultWebSessionManager SessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setGlobalSessionTimeout(604800000L);
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * cookie对象;
     * @return
     */
    public SimpleCookie rememberMeCookie(){
        //这个参数是cookie的名称,对应前端的checkbox的name = rememberMe
        SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
        //cookie生效时间30天,单位秒;
        simpleCookie.setMaxAge(2592000);
        return simpleCookie;
    }


    /**
     * cookie管理对象;记住我功能
     * @return
     */
    public CookieRememberMeManager rememberMeManager(){
         CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
         cookieRememberMeManager.setCookie(rememberMeCookie());
        // cookieRememberMeManager.setCipherKey用来设置加密的Key,参数类型byte[], 字节数组长度要求16;     
  cookieRememberMeManager.setCipherKey(Base64.decode("3AvVhmFLUs0KTA3Kprsdag=="));
        return cookieRememberMeManager;
    }
}

3.3 Shiro的自定义Realm

**
 * 自定义Realm
 * (1)AuthenticatingRealm:shiro中的用于进行认证的领域,实现doGetAuthentcationInfo方法实现用户登录时的认证逻辑;
 * (2)AuthorizingRealm:shiro中用于授权的领域,实现doGetAuthrozitionInfo方法实现用户的授权逻辑,AuthorizingRealm继承了AuthenticatingRealm,
 * 所以在实际使用中主要用到的就是这个AuthenticatingRealm类;
 * (3)AuthenticatingRealm、AuthorizingRealm这两个类都是shiro中提供了一些线程的realm接口
 * (4)在与spring整合项目中,shiro的SecurityManager会自动调用这两个方法,从而实现认证和授权,可以结合shiro的CacheManager将认证和授权信息保存在缓存中,
 * 这样可以提高系统的处理效率。    
 *
 */
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private RedissonClient redissonClient;

    @Autowired
    private UserService userService;

    @Override
    /**
     * 认证
     */
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        //编写shiro判断逻辑,判断用户名和密码
        //1.判断用户名  token中的用户信息是登录时候传进来的
        UsernamePasswordToken  usernamePasswordToken = (UsernamePasswordToken)token;
        //在自己数据库找当前用户
        User user = userSerivce.findByName(usernamePasswordToken.getUsername());
        if(user == null){
            //用户名不存在
            return null;//shiro底层会抛出UnKnowAccountException
        }
        //2.判断密码
        //第二个字段是user.getPassword(),注意这里是指从数据库中获取的password。第三个字段是realm,即当前realm的名称。
        //这块对比逻辑是先对比username,但是username肯定是相等的,所以真正对比的是password。
        //从这里传入的password(这里是从数据库获取的)和token(filter中登录时生成的)中的password做对比,如果相同就允许登录,
        // 不相同就抛出IncorrectCredentialsException异常。
        //如果认证不通过,就不会执行下面的授权方法了
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,user.getPassword, getName());

        //3.返回身份处理对象
        return simpleAuthenticationInfo;
    }
    @Override
    /**
     * 授权
     */
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
        //1.获取当前登录的用户
        User user = (User) principal.getPrimaryPrincipal();
        //通过SimpleAuthenticationInfo做授权
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        //2.添加角色
        //这里从缓存获取当前用户的角色信息,并赋予simpleAuthorizationInfo
        RMapCache<String, Set<String>>  userRoleCodeCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
        Set<String> roleCode = roleCache.get(user.getId());
        if (!CollectionUtils.isEmpty(roleCode)) {
            simpleAuthorizationInfo.addRoles(roleCode);
        }
        //3.添加权限
       //这里从缓存获取当前用户的权限信息,并赋予simpleAuthorizationInfo
        RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_PERMISSION);
        Set<String> userPermissionCode = userPermissionCodeCache.get(user.getId());
        if (!CollectionUtils.isEmpty(userFunPermissionCode)) {
            simpleAuthorizationInfo.addStringPermissions(userFunPermissionCode);
        }
        return simpleAuthorizationInfo;
    }
}

3.4 自定义Shiro异常拦截

当Shiro抛出UnauthorizedException,表明当前用户没有权限。
当Shiro抛出UnauthenticatedException,表明当前用户没有被Shiro管理,也就是没有登录,让用户重新登录。
我们在这统一拦截异常并封装返回异常结果。

@ControllerAdvice
public class ExceptionController extends BaseApp {
    @ResponseBody
    @ExceptionHandler(value = {UnauthorizedException.class,UnauthenticatedException.class})
    public Map<String, Object> handleClientException(HttpServletRequest req, HttpServletResponse resp, Exception e) {
        if (e instanceof UnauthorizedException) {
            resp.setStatus(HttpStatus.FORBIDDEN.value());
            return buildResponse(EBusinessCode.NOT_PERMISSION, null);
           } else if (e instanceof UnauthenticatedException) {
            resp.setStatus(HttpStatus.FORBIDDEN.value());
            return buildResponse(EBusinessCode.RE_LOGIN, null);
        } 
    }
}

3.5 测试

  //将当前用户交给Shiro管理,这里的处理逻辑可加在原有项目的登录逻辑里面。
   @RequestMapping(value = "/test1", method = RequestMethod.GET)
    public @ResponseBody Map<String, Object> Test1() throws BaseException {
        User user = new User();
        user.setId("123456");
        user.setName("123456");
        user.setPassword("123456");

        //1.将user交给shiro。
        UsernamePasswordToken token = new UsernamePasswordToken(user.getName, user.getPassword());
        token.setRememberMe(true);
        Subject currentUser = SecurityUtils.getSubject();
        //主体提交登录请求到SecurityManager
        currentUser.login(t);
        //2.查找用户角色和权限,并将用户的角色和权限放到缓存。
        //添加角色
        RMapCache<String, Set<String>> roleCache = redissonClient.getMapCache(CacheKey.USER_ROLE);
        Set<String> roleCode =  new HashSet<>();
        roleCode.add("superMan");
        roleCache.put(user.getId(),roleCode)
        //添加权限
        RMapCache<String, Set<String>> userPermissionCodeCache = redissonClient.getMapCache(CacheKey.USER_FUNCTION_PERMISSION);
        Set<String> permissionCode =  new HashSet<>();
        permissionCode.add("1000");
        userPermissionCodeCache.put(user.getId(),permissionCode)
        return buildResponse();
    }

//对这个接口实行权限控制,前提需要把当前用户交给Shiro管理,如果Shiro识别不到当前用户,则会抛出UnauthenticatedException异常,让用户重新登录。
//这里表明需要当前用户同时拥有角色为superMan和root,且拥有代号为1000或1001的权限才能访问,否则抛出UnauthorizedException,表明当前用户没有权限访问该接口。
//logical = Logical.OR表示或的关系,logical = Logical.AND表示且的关系
 @RequestMapping(value = "/test", method = RequestMethod.GET)
 @RequiresRoles(value = {"superMan","root"}, logical = Logical.AND)
 @RequiresPermissions(value = {"1000", "1001"}, logical = Logical.OR) public @ResponseBody
    Map<String, Object> Test() throws Exception {
        return buildResponse();
    }

4. Shiro认证过程

shiro认证过程
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351