SpringBoot整合shiro

详细教程: [点我跳转]

SpringBoot整合shiro

Shiro是apache旗下一个开源安全框架(http://shiro.apache.org/),它将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。使用shiro就可以非常快速的完成认证、授权等功能的开发,降低系统成本。

简介

执行流程

用户在进行资源访问时,要求系统要对用户进行权限控制,其具体流程如图所示:

三大核心对象(重点)

在概念层面,Shiro 架构包含三个主要的理念,如图所示:

其中:

  1. Subject :主体对象,负责提交用户认证和授权信息。(每个用户对应一个Subject)
  2. SecurityManager:安全管理器,负责认证,授权等业务实现。(用于管理Subject)
  3. Realm:领域对象,负责从数据层获取业务数据。(用于实现授权和认证的方法)

详细架构

Shiro框架进行权限管理时,要涉及到的一些核心对象,主要包括:认证管理对象,授权管理对象,会话管理对象,缓存管理对象,加密管理对象以及Realm管理对象(领域对象:负责处理认证和授权领域的数据访问题)等,其具体架构如图所示:

其中:

  • Subject(主体):与软件交互的一个特定的实体(用户、第三方服务等)。
  • SecurityManager(安全管理器) :Shiro 的核心,用来协调管理组件工作。
  • Authenticator(认证管理器):负责执行认证操作。
  • Authorizer(授权管理器):负责授权检测。
  • SessionManager(会话管理):负责创建并管理用户 Session 生命周期,提供一个强有力的 Session 体验。
  • SessionDAO:代表 SessionManager 执行 Session 持久(CRUD)动作,它允许任何存储的数据挂接到 session 管理基础上。
  • CacheManager(缓存管理器):提供创建缓存实例和管理缓存生命周期的功能。
  • Cryptography(加密管理器):提供了加密方式的设计及管理。
  • Realms(领域对象):是shiro和你的应用程序安全数据之间的桥梁。

拦截实现

1.导入依赖

依赖地址: https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring

实用spring整合shiro时,需要在pom.xml中添加如下依赖:

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

2. Shiro核心对象配置

先配置两个bean

@Configuration
public class ShiroConfig {

    // ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean filterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 注入SecurityManager
        factoryBean.setSecurityManager(securityManager);

        // 拦截实现
        Map<String, String> map = new LinkedHashMap<>();
        // authc代表无法直接访问
        map.put("/user/update", "authc");
        map.put("/user/add", "authc");
        // map.put("/user/*", "authc"); // user/* 表示user下的所有请求, 就不用了一个一个配置了
        factoryBean.setFilterChainDefinitionMap(map);
        // 假设/toLogin是拦截后跳转到的登录页面请求
        factoryBean.setLoginUrl("/toLogin");
        return factoryBean;
    }

    // SecurityManager
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        return securityManager;
    }

}

现在无法直接访问/user/add 和 /user/update 请求了

关于拦截方式, 有如下定义:

  • anon: 无需认证
  • authc: 必须认证才能访问(例如登录后可访问)
  • user: 必须拥有记住我功能才能用
  • perms: 拥有对某个资源的权限才能访问
  • role: 拥有某个角色权限才能访问

Realm

(领域对象):是shiro和你的应用程序安全数据之间的桥梁。

自己手动编写, 需要继承AuthorizingRealm类, 还需要交给spring来管理

继承这个类后需要重写两个方法, 一个方法负责自定义的授权, 一个方法负责自定义的认证

public class UserRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("授权");
        return null;
    }

    // 认证, token参数相当于令牌
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("认证");
        return null;
    }
}

交给spring管理

可以在Realm类上加注解, 或者用下面这种方式配置(在shiro的配置类中)

@Bean
public UserRealm realm() {
    return new UserRealm();
}

配置好后, 我们需要将这个对象注入到SecurityManagersecurityManager.setRealm(userRealm);

@Bean
public SecurityManager securityManager(UserRealm userRealm) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm);
    return securityManager;
}

认证的实现

身份认证即判定用户是否是系统的合法用户,用户访问系统资源时的认证(对用户身份信息的认证)

其中认证流程分析如下:

  1. 系统调用subject的login方法将用户信息提交给SecurityManager
  2. SecurityManager将认证操作委托给认证器对象
  3. AuthenticatorAuthenticator将用户输入的身份信息传递给Realm。
  4. Realm访问数据库获取用户信息然后对信息进行封装并返回。
  5. Authenticator 对realm返回的信息进行身份认证。

1. 控制器controller

这里实现了2个请求处理, 一个跳转到登录页面, 一个实现登录操作

// 跳转登录页面
@RequestMapping("/toLogin")
public String toLogin() {
    return "login";
}

// 进行登录操作, 假设用户只输入username和passowrd进行登录
@RequestMapping("/doLogin")
public String doLogin(String username, String password, Model model) {
    System.out.println("登录操作");
    // 获取用户数据
    Subject subject = SecurityUtils.getSubject();
    // 封装用户的登录数据
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    try {
        subject.login(token);
        return "index";
    } catch (UnknownAccountException e) { // 用户不存在
        e.printStackTrace();
        model.addAttribute("msg", "用户不存在");
        return "login";
    } catch (IncorrectCredentialsException e) { // 密码错误
        e.printStackTrace();
        model.addAttribute("msg", "密码错误");
        return "login";
    } catch ( LockedAccountException e ) { // 账户已锁定
        e.printStackTrace();
        model.addAttribute("msg", "账户已锁定");
        return "login";
    }
}

2. 编写realm

在正式写认证之前, 确定是否配置了Realm [配置Realm]

配置好之后, 我们编辑认证的方法doGetAuthenticationInfo

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("认证");

    // 假设这里的username和password是从数据库中获取到的
    String username = "root";
    String password = "123";

    // 类型转换
    UsernamePasswordToken userToken = (UsernamePasswordToken) token;
    if (!userToken.getUsername().equals(username)) {
                   
    }

    SimpleAuthenticationInfo info = new SimpleAuthenticationInfo("", password, "");
    return info; //返回值会传递给SecurityManager,此对象基于认证信息进行认证。
}

到这里, 就写好了, 用户名错误和密码错误都会抛出对应的异常

关于SimpleAuthenticationInfo构造方法, 当然这是参数比较多的一个构造方法

new SimpleAuthenticationInfo(
      //(user为数据库中查询出来的用户对象)
      user,//principal 用户身份(传什么,将来取出来就是什么)
      user.getPassword(),//hashedCredentials (已加密的密码)
      credentialsSalt,//credentialsSalt 盐值(加密的盐值, 一般从数据库表中取出来)
      this.getName());//realmName

扩展: Shiro加密验证

和以上案例无关

当我们保存密码的时候可以使用shiro进行md5加密, 如下:

String pwd = "123456"; // 密码
String salt = UUID.randomUUID().toString(); // 盐
// MD5加密方式, 加密5次
SimpleHash sh = new SimpleHash("MD5", pwd, salt, 5); // 定义simpleHash对象
String hashedPwd = sh.toHex(); // 生成16进制密文
// ...把hashedPwd保存到数据库

这时我们取出数据库中的密码就是被加密过的, 那么我们怎么进行验证输入的密码呢?

看我的方法:如下

// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    //1.获取用户提交的认证信息
    UsernamePasswordToken upToken=(UsernamePasswordToken)token;
    //2.基于用户名查找用户信息
    SysUser user=sysUserDao.findUserByUserName(upToken.getUsername());
    //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());//realmName
    return info;//返回值会传递给SecurityManager,此对象基于认证信息进行认证。
}

这样还不够, shiro还不知道我们是怎么加密的, 所以我们需要在Realm中重写一个方法

@Override
public CredentialsMatcher getCredentialsMatcher() {
    HashedCredentialsMatcher cMatcher = new HashedCredentialsMatcher("MD5"); // 加密方式
    cMatcher.setHashIterations(5); // 加密次数
    return cMatcher;
}

授权的实现

授权即对用户资源访问的授权(是否允许用户访问此资源),用户访问系统资源时的授权流程

授权流程分析:

  1. 系统调用subject相关方法将用户信息(例如isPermitted)递交给SecurityManager。
  2. SecurityManager将权限检测操作委托给Authorizer对象。
  3. Authorizer将用户信息委托给realm。R
  4. ealm访问数据库获取用户权限信息并封装。
  5. Authorizer对用户授权信息进行判定。

首先来实现简单的授权, 还以上面的案例为例

例如我们现在需要 /user/add请求需要授权才能访问

1. 配置shiro

在我们的shiro配置ShiroFilterFactoryBean的方法中添加如下代码:

map.put("/user/add", "perms[user:add]");   // 表明/user/add需要授权才能访问, 授权字符串为: 'user:add'
factoryBean.setUnauthorizedUrl("/unauthorized"); // 没有权限时执行的请求或跳转的url

写到这里, 我们的/user/add 请求就无法访问了, 会提示没有权限, 哪么我们怎么才能授权呢, 我们往下接着写

2. 编写realm

我们来编写realm中的AuthorizationInfo方法, 即编写授权的方法

// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println("授权");

    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.addStringPermission("user:add"); // 为用户添加授权
    
    // info.addStringPermissions( [set集合] ); 如果有多个权限, 则使用String类型的set集合进行传参

    return info;
}

这样, 我们的user/add就可以访问了, 注意:因为我这里user:add是写死的, 所以说任何用户都会被授权, 对正常来业务来说, 这个字符串应从数据库中取出, 然后添加到授权

@RequiresPermissions注解

一般作用域Service中的方法上, 如果作用于类上, 则类中的所有方法陪授权后才能访问. 使用此注解可不用上面map.put("/user/add", "perms[user:add]");这样一个一个的put

例如: 例如: @RequiresPermissions({"user:add", "user:update"})

即要求subject中有注解中的参数权限时, 才能执行被此注解描述的方法, 否则抛出AuthorizationException异常

posted @ 2020-07-29 18:57  zpk-aaron  阅读(192)  评论(0编辑  收藏  举报