SpringBoot集成Shiro,认证和授权,全局异常处理

项目版本:

官网配置:shiro.apach.org/spring-boot.html

springboot2.x
shiro:1.5.3

Maven配置:

		 <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.5.3</version>
        </dependency>

集成shiro配置的核心:

springboot中集成shiro需要两个类:shiroConfig类和CustonRealm类。

ShiroConfig类:
顾名思义就是对shiro的一些配置,相对于之前的xml配置。包括:过滤的文件和权限,密码加密的算法,其用注解等相关功能。

CustonRealm类:
自定义Realm类继承(AuthorizingRealm)类,重写里面该父类里面的方法。doGetAuthorizationInfo(权限相关)、doGetAuthenticationInfo(身份认证)这两个方法。

基本的配置:

shiroConfig配置:

package com.cy.pj.common.config;

import com.cy.pj.sys.service.realm.ShiroUserRealm;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.mgt.RememberMeManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * @Configuration 注解描述的类为一个配置对象,
 * 此对象也会交给spring管理
 */

@Configuration
public class SpringShiroConfig {
    
    


    //配置自定义的Realm
    @Bean
    public ShiroUserRealm shiroUserRealm() {
    
    
        ShiroUserRealm shiroUserRealm = new ShiroUserRealm();
        return shiroUserRealm;
    }
    
    /**
     * 配置advisor对象,开启@RequirePermission注解的配置,进行权限控制
     * 在需要进行授权访问的业务层(Service)方法上,添加执行此方法需要的权限标识,参考代码@RequiresPermissions(“sys:user:update”)
     *
     * @param securityManager
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
    
    
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }


    /**
     * 配置shiro中缓存管理器对象 (对象的名字不能写cacheManager,因为spring容器中已经存在一个名字为cacheManager的对象了)
     *
     * 作用:缓存授权信息,当第一次访问授权时,会调用自定义Realm的获取授权数据的方法,从数据库中查询授权数据,并将其以登录者的Principal为键,存储在缓存中。
     * 以后的每次访问授权,就直接从缓存中获取,而不再从数据库中获取。
     */
    @Bean
    public CacheManager shiroCacheManager() {
    
    

        return new MemoryConstrainedCacheManager();
    }

    /**
     * 配置记住我管理对象:底层同cookie对象将用户信息写到客户端
     */
    @Bean
    public RememberMeManager rememberMeManager() {
    
    
        CookieRememberMeManager cManager = new CookieRememberMeManager();
        //配置cookie
        SimpleCookie cookie = new SimpleCookie("rememberMe");
        //设置cookie执行时长(7天)
        cookie.setMaxAge(7 * 24 * 60 * 60);
        cManager.setCookie(cookie);
        return cManager;

    }


    //配置安全管理器
    @Bean
    public SecurityManager securitManager(Realm realm,
                                          CacheManager cacheManager,
                                          RememberMeManager rememberMeManager) {
    
    
        DefaultWebSecurityManager sManager = new DefaultWebSecurityManager();
        //注入realm对象
        sManager.setRealm(realm);
        //注入缓存管理器对象
        sManager.setCacheManager(cacheManager);
        //注入记住我管理对象
        sManager.setRememberMeManager(rememberMeManager);
        return sManager;
    }

    //配置过滤器工厂
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager) {
    
    
        ShiroFilterFactoryBean sfBean = new ShiroFilterFactoryBean();
        //设置权限管理器
        sfBean.setSecurityManager(securityManager);
        //设置认证页面
        sfBean.setLoginUrl("/doLoginUI");
        sfBean.setSuccessUrl("/doLoginUI");
        //定义map指定请求过滤规则(哪些资源允许匿名访问,哪些必须认证访问)
        Map<String, String> map = new LinkedHashMap<>();
        //静态资源允许匿名访问:"anon"
        map.put("/bower_components/**", "anon");
        map.put("/build/**", "anon");
        map.put("/dist/**", "anon");
        map.put("/plugins/**", "anon");
        map.put("/user/doLogin", "anon");
        map.put("/doLogout", "logout");  //logout对应着shiro框架中退出过滤器
        //除了匿名访问的资源,其它都要认证("authc")后访问
        // map.put("/**", "authc");    //假如需要实现记住我功能,这里的过滤器标识使用user
        map.put("/**", "user");
        sfBean.setFilterChainDefinitionMap(map);
        return sfBean;
    }
}


shiroConfig 也不复杂,基本就三个方法。再说这三个方法之前,我想给大家说一下shiro的三个核心概念:

1.Subject: 代表当前正在执行操作的用户,但Subject代表的可以是人,也可以是任何第三方系统帐号。当然每个subject实例都会被绑定到SercurityManger上。
2.SecurityManger:SecurityManager是Shiro核心,主要协调Shiro内部的各种安全组件,这个我们不需要太关注,只需要知道可以设置自定的Realm。
3.Realm:用户数据和Shiro数据交互的桥梁。比如需要用户身份认证、权限认证。都是需要通过Realm来读取数据。

shiroFilter方法:
这个方法看名字就知道了:shiro的过滤器,可以设置登录页面(setLoginUrl)、权限不足跳转页面(setUnauthorizedUrl)、具体某些页面的权限控制或者身份认证。
注意:这里是需要设置SecurityManager(setSecurityManager)。
默认的过滤器还有:anno、authc、authcBasic、logout、noSessionCreation、perms、port、rest、roles、ssl、user过滤器。
具体的大家可以查看package org.apache.shiro.web.filter.mgt.DefaultFilter。这个类,常用的也就authc、anno。
securityManager 方法:
查看源码可以知道 securityManager是一个接口类,我们可以看下它的实现类:
在这里插入图片描述.

具体怎么实现的,感兴趣的同学可以看下。由于项目是一个web项目,所以我们使用的是DefaultWebSecurityManager ,然后设置自己的Realm。
ShiroUserRealm 方法:
将 ShiroUserRealm的实例化交给spring去管理,当然这里也可以利用注解的方式去注入。

ShiroUserRealm配置:

package com.cy.pj.sys.service.realm;

import com.cy.pj.sys.mapper.SysMenuMapper;
import com.cy.pj.sys.mapper.SysRoleMenusMapper;
import com.cy.pj.sys.mapper.SysUserDeptMapper;
import com.cy.pj.sys.mapper.SysUserRoleMapper;
import com.cy.pj.sys.pojo.SysUser;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
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.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.HashSet;
import java.util.List;

@Component
public class ShiroUserRealm extends AuthorizingRealm {
    
    
    @Autowired
    private SysUserDeptMapper sysUserDeptMapper;
    @Autowired
    private SysUserRoleMapper sysUserRoleMapper;
    @Autowired
    private SysRoleMenusMapper sysRoleMenusMapper;
    @Autowired
    private SysMenuMapper sysMenuMapper;

    /**
     * 负责授权信息的获取和封装
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    
    
        System.out.println("访问数据库查询授权标识");
        //1.获取登录用户
        SysUser user = (SysUser) principalCollection.getPrimaryPrincipal();
        //2.基于用户id查询角色id并校验
        Integer[] roleIds = sysUserRoleMapper.findRolesByUserId(user.getId());
        if (roleIds == null || roleIds.length == 0) {
    
    
            throw new AuthorizationException("没有权限");
        }

        //3.基于角色id查询菜单id并校验
        List<Integer> menuIdS = sysRoleMenusMapper.findMenuIdsByRoleIds(roleIds);
        if (menuIdS == null || menuIdS.size() == 0) {
    
    
            throw new AuthorizationException();
        }
        //4.基于菜单id查询授权标示并校验
        Integer[] s = {
    
    };
        List<String> permission = sysMenuMapper.findPermissionById(menuIdS.toArray(s));
        if (permission == null || permission.size() == 0) {
    
    
            throw new AuthorizationException();
        }
        //5.封装查询结果并返回
        HashSet<String> set = new HashSet<>(); //有可能角色的权限会重复,所有需要存入set集合进行去重
        for (String per : permission) {
    
    
            if (!StringUtils.isEmpty(per)) {
    
    
                set.add(per);
            }
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setStringPermissions(set);
        return info;//返回给授权管理器
    }

    /**
     * 负责认证信息的获取和封装
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    
        //1.获取登录时输入的账户信息(从token中获取)
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();
        //2.基于用户名查询用户信息
        SysUser user = sysUserDeptMapper.findUserByUserName(username);
        //3.判定用户是否存在
        if (user == null) {
    
    
            throw new UnknownAccountException("用户不存在");
        }
        //4.判定用户是否已被禁用。(被禁用不允许登陆)
        if (user.getValid() == 0) {
    
    
            throw new LockedAccountException("账户被禁用");
        }
        //5.封装认证信息并返回
        ByteSource credentialsSalt = ByteSource.Util.bytes(user.getSalt());
        //记住:构建什么对象要看方法的返回值
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(
                user,//principal (身份)
                user.getPassword(),//hashedCredentials已加密的密码
                credentialsSalt, //credentialsSalt 盐
                this.getName());//realName
        return info;//返回给授权管理器
    }

    /**
     * 设置凭证匹配器(与用户添加操作使用相同的加密算法)
     * //用户添加  shiro框架MD5加密
     * String newPassword = new SimpleHash(
     * "MD5",                  //algorithmName 算法
     * password,               //原密碼
     * salt,                   //盐
     * 1                       //hashIterations表示加密次数
     * ).toHex();                      //转16进制
     * <p>
     * 也可以用set方法设置加密
     */
    @Override
    public CredentialsMatcher getCredentialsMatcher() {
    
    
        //构建加密匹配器对象
        HashedCredentialsMatcher cMatcher = new HashedCredentialsMatcher();
        //设置加密算法
        cMatcher.setHashAlgorithmName("MD5");
        //设置加密次数
        cMatcher.setHashIterations(1);
        return cMatcher;
    }
}


说明:
自定义的Realm类继承AuthorizingRealm类,并且重载doGetAuthorizationInfo和doGetAuthenticationInfo两个方法。
doGetAuthorizationInfo: 权限认证,即登录过后,每个身份不一定,对应的所能看的页面也不一样。
doGetAuthenticationInfo:身份认证。即登录通过账号和密码验证登陆人的身份信息。

controller类:
新建一个HomeIndexController类,加入如下代码:

 @RequestMapping(value = "/login", method = RequestMethod.GET)
    @ResponseBody
    public String defaultLogin() {
    
    
        return "首页";
    }

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    @ResponseBody
    public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
    
    
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 在认证提交前准备 token(令牌)
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 执行认证登陆
        try {
    
    
            subject.login(token);
        } catch (UnknownAccountException uae) {
    
    
            return "未知账户";
        } catch (IncorrectCredentialsException ice) {
    
    
            return "密码不正确";
        } catch (LockedAccountException lae) {
    
    
            return "账户已锁定";
        } catch (ExcessiveAttemptsException eae) {
    
    
            return "用户名或密码错误次数过多";
        } catch (AuthenticationException ae) {
    
    
            return "用户名或密码不正确!";
        }
        if (subject.isAuthenticated()) {
    
    
            return "登录成功";
        } else {
    
    
            token.clear();
            return "登录失败";
        }
    }

测试:

我们可以使用postman进行测试:
在这里插入图片描述
ok 身份认证是没问题了,我们再来考虑如何加入权限。

可以将shiro异常处理封装到全局异常处理

package com.cy.pj.common.web;

import com.cy.pj.common.vo.JsonResult;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authz.AuthorizationException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandling {
    
    
    /**
     * 处理shiro框架异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(ShiroException.class)
    public JsonResult doHandleShiroException(
            ShiroException e) {
    
    
        JsonResult r = new JsonResult();
        r.setState(0);
        if (e instanceof UnknownAccountException) {
    
    
            r.setMessage("账户不存在");
        } else if (e instanceof LockedAccountException) {
    
    
            r.setMessage("账户已被禁用");
        } else if (e instanceof IncorrectCredentialsException) {
    
    
            r.setMessage("密码不正确");
        } else if (e instanceof AuthorizationException) {
    
    
            r.setMessage("没有此操作权限");
        } else {
    
    
            r.setMessage("系统维护中");
        }
        e.printStackTrace();
        return r;
    }

  //定义全局异常处理
    @ExceptionHandler(value = RuntimeException.class)
    public JsonResult topException(RuntimeException e) {
    
    
        e.printStackTrace();
        return new JsonResult(e);
    }

}

可以将获取登陆输入的用户信息封装成一个API

package com.cy.pj.common.util;

import com.cy.pj.sys.pojo.SysUser;
import org.apache.shiro.SecurityUtils;

public class ShiroUtils {
    
    

    //返回登陆用户信息
    public static SysUser getUser(){
    
    
      return (SysUser) SecurityUtils.getSubject().getPrincipal();
    }
    //返回登陆用户名
    public static   String getUsername(){
    
    
        return getUser().getUsername();
    }
}

利用注解配置权限:

其实,我们完全可以不用注解的形式去配置权限,因为在之前已经加过了:DefaultFilter类中有perms(类似于perms[user:add])这种形式的。但是试想一下,这种控制的粒度可能会很细,具体到某一个类中的方法,那么如果是配置文件配,是不是每个方法都要加一个perms?但是注解就不一样了,直接写在方法上面,简单快捷。
很简单,主需要在config类中加入如下代码,就能开启注解:

@Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    
    
        return new LifecycleBeanPostProcessor();
    }
    
 /**
     * *
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * *
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * * @return
     */
    @Bean
    @DependsOn({
    
    "lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
    
    
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
    
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
    
    
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
        return authorizationAttributeSourceAdvisor;
    }

新建一个UserController类。如下:

@RequestMapping("/user")
@Controller
public class UserController {
    
    
    @RequiresPermissions("user:list")
    @ResponseBody
    @RequestMapping("/show")
    public String showUser() {
    
    
        return "这是学生信息";
    }
}

重复刚才的登录步骤,登录成功后,postman 输入localhost:8080/user/show
在这里插入图片描述

确实是没有权限。方法上是 @RequiresPermissions(“user:list”),而customRealm中是 user:show、user:admin。我们可以调整下方法上的权限改为user:show。调试一下,发现成功了。
这里有一个问题:当没有权限时,系统会报错,而没有跳转到对应的没有权限的页面,也就是setUnauthorizedUrl这个方法没起作用,这个问题,下一篇会给出解决方案-。-

猜你喜欢

转载自blog.csdn.net/m0_49353216/article/details/108991300