好用的安全框架——Shiro

3,283 阅读3分钟

好用的安全框架——Shiro

Shiro帮助我们完成了几大功能

  1. authentication.认证,确认用户的身份
  2. authorization, 授权,对用户访问资源的行为做控制

RBAC模型:Role Based Access Control

image-20230203230331026

Springboot中集成Shiro

1.1注入依赖

   <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-spring-boot-starter</artifactId>
                <version>1.10.0</version>
            </dependency>
            <dependency>
                <groupId>log4j</groupId>
                <artifactId>log4j</artifactId>
                <version>1.2.17</version>
            </dependency>

1.2配置三板斧

@Configuration
public class ShiroConfig {
​
    //Realm 代表资源
    @Bean
    public Realm getRealm(){
        return new MyRealm();
    }
​
    //SecurityManager 用作流程控制
    @Bean
    public DefaultWebSecurityManager mySecurityManager(Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }
​
    //ShirFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultSecurityManager mySecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(mySecurityManager);
        return shiroFilterFactoryBean;
    }
​
}

1.3.1实现登录认证功能

我们定义了一个自己的资源MyRealm,并且继承了AuthorizingRealm,去实现它的两个方法:doGetAuthorizationInfo和doGetAuthenticationInfo

我们在doGetAuthentication 方法里去写认证流程。

@Configuration
public class MyRealm extends AuthorizingRealm{
​
    private Logger logger = LoggerFactory.getLogger(ShiroConfig.class);
​
    @Resource
    private UserService userService;
​
    //授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
​
        logger.info("enter doGetAuthorizationInfo");
        return null;
    }
​
    //认证,主要是看你有没有记载我们数据库里,如果在了,信息是否正确
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        logger.info(">>>enter doGetAuthorizationInfo,开始认证");
        //获取到当前用户的信息
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;
        String username = usernamePasswordToken.getUsername();
        //从数据库中拿取比对
        UserBean userBean = userService.getUserByUsername(username);
​
        //没有查到就没有这个用户
        if(userBean == null){
            //抛出UnknownAccountException
            return null;
        }
        //返回AuthenticationToken,完成认证流程,无需接触密码敏感信息,让shiro帮我们做
        SimpleAuthenticationInfo simpleAuthenticationInfo =
                new SimpleAuthenticationInfo(userBean,
                        userBean.getUserPass(), "myRealm");
​
​
        return simpleAuthenticationInfo;
    }
}

1.3.2登录认证路径资源过滤

在shiro配置三板斧中的getShiroFilterFactoryBean方法里添加

@Configuration
public class ShiroConfig {
​
​
​
    //Realm 代表资源
//    @Bean
//    public Realm getRealm(){
//        return new MyRealm();
//    }
​
    //SecurityManager 用作流程控制
    @Bean
    public DefaultWebSecurityManager mySecurityManager(Realm realm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }
​
    //ShirFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultSecurityManager mySecurityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(mySecurityManager);
​
        //配置路径过滤器
        HashMap<String, String> filterMap = new HashMap<>();
        //key是ant路径,value是shiro默认配置过滤器的写法。
        // 如 anno,auth,authc,perms,role.
        filterMap.put("/mobile/**","authc");
        filterMap.put("/salary/**","authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
        return shiroFilterFactoryBean;
    }
​
}

注意:这里的"authc"取自shiro自定义的过滤策略。这里我们用的是非登录即过滤的策略。

且越宽泛的规则定义放后,越精细的定义放前,因为他是从上到下开始过滤

下面是它默认的策略,可选

image-20230204140503858

注意,这里我们把realm的配置注释掉了,因为,我们在myRealm类里加入@configuration注解了,无需重复配置bean

1.4.controller层login方法

    @PostMapping("/login")
    public Object login(@RequestBody UserBean userBean){
        Map<String,String> msg = new HashMap<>();
​
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(userBean.getUserName(), userBean.getUserPass());
        try {
            currentUser.login(token);
            msg.put("ok","登录成功");
        }catch (UnknownAccountException e){
            logger.info("There is no user with username of" + token.getPrincipal());
            msg.put("errMsg","用户不存在");
        }catch (IncorrectCredentialsException e){
            logger.info("Password for account " + token.getPrincipal() + "is wrong");
            msg.put("errMsg","密码不正确");
        }catch (LockedAccountException e){
            logger.info("The account " + token.getPrincipal() + "is locked");
            msg.put("errMsg","账户已锁定");
        }catch (AuthenticationException e){
            logger.info("登录失败",e);
            msg.put("errMsg","登录失败");
        }
        return msg;
​
    }

经过测试发现,两个资源路径salary和mobile需要登录才能访问,否则跳到login.jsp页面

1.5修复登录认证错误访问的情况

当前问题:不登录就跳到login.jsp页面,我们需要自定义一个页面

登出:

​
    @GetMapping("/logout")
    public void logout(){
        Subject currentUser = SecurityUtils.getSubject();
        currentUser.logout();
    }

或者在shiroFilterFactoryBean方法里面加上

filterMap.put("/common/logout","logout");

采用它默认的过滤器也可。

实际上它的底层也是调用了这个方法currentUser.logout()

image-20230204142320213

1.6总结

  1. 入口是在currentUser.login(token)方法
  2. 在shiroFilterFactoryBean中定义一些资源访问的过滤器策略,来限制资源的访问
  3. 在MyRealm,也就是资源里,获取到登录信息,且在doGetAuthenticationInfo方法中实现登录的认证,且在认证的时候,无需管密码,shiro会帮我们校验,只需要获取username即可

2.1实现授权

目标:给予用户相应角色以及相应权限。

第一步

在MyRealm资源类里面实现doGetAuthorizationInfo方法

  //授权
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        logger.info("enter doGetAuthorizationInfo");
​
        //获取当前用户,是当前的UserBean
        UserBean userBean = (UserBean) principalCollection.getPrimaryPrincipal();
​
        //将你的自己顶一个的permission 和 roles交给 框架取处理。
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        authorizationInfo.setRoles((Set<String>) userBean.getUserRoles());
        authorizationInfo.setStringPermissions((Set<String>) userBean.getUserPerms());
​
        return authorizationInfo;
    }

注意:这里的userBean之所以能获取,是因为我们在doGetAuthenticationInfo方法中

SimpleAuthenticationInfo simpleAuthenticationInfo =new SimpleAuthenticationInfo(userBean,userBean.getUserPass(), "myRealm");

将userRealm存放了起来

然后我们就将userRealm中的权限,角色交给SimpleAuthorizationInfo,并且返回它的实例交给框架处理。

第二步

在controller类里面去验证是否有某一个权限,如果有就给他相应资源。

@RestController
@RequestMapping("/mobile")
public class MobileController {
​
    @GetMapping("/query")
    public String query(){
        Subject currentUser = SecurityUtils.getSubject();
        if(currentUser.isPermitted("mobile")){
            return "mobile";
        }
        return "error";
    }
}

注意:这里之所以可以用urrentUser.isPermitted()该方法,是因为我们把自定义的MyRealm对象里的角色,权限交给了shiro,所以我们可以使用。

但是这样的硬编码方法会影响到业务逻辑,不太推荐。

2.2注解实现授权

所以我们更常用注解方式。我们用注解来实现方法级别的权限控制

常用的注解有五个

  1. @RequiresAuthentication 需要完成用户登录
  2. @RequireGuest 未登录用户可以访问,登录用户不能访问
  3. @RequirePermissions 需要有对应资源权限
  4. @RequireRoles 需要有对应角色才能访问
  5. @RequiresUser 需要用户完成登录并且实现了记住我功能

Demo

    @RequiresPermissions("mobile")
    @GetMapping("/query")
    public String query(){
​
        return "mobile";
    }

有mobile权限的才可以访问