SpringBoot框架下的Shiro使用方法

1. 简介

Apache 提供的一个 Java 安全框架,可以完成用户的认证、鉴权、加密、会话管理等操作。Shiro 就是用来解决安全管理的系统化框架。

2. 核心组件

权限赋给角色,角色赋给用户

1、UsernamePasswordToken,Shiro 用来封装用户登录信息,使用用户的登录信息来创建令牌 Token。

2、SecurityManager,Shiro 的核心部分,负责安全认证和授权。

3、Suject,Shiro 的一个抽象概念,Subject 代表了当前用户,这个用户不一定是具体的人,与当前应用交互的任何东西都是Subject;与 Subject 的所有交互都会委托给 SecurityManager。

4、Realm,开发者自定义的模块,根据项目的需求,验证和授权的逻辑全部写在 Realm 中。就是说如果 SecurityManager 要进行验证,需要从 Realm 中获取相应的用户信息。

5、AuthenticationInfo,用户的角色信息集合,认证时使用。

6、AuthorzationInfo,角色的权限信息集合,授权时使用。

7、DefaultWebSecurityManager,安全管理器,开发者自定义的 Realm 需要注入到 DefaultWebSecurityManager 进行管理才能生效。

8、ShiroFilterFactoryBean,过滤器工厂,Shiro 的基本运行机制是开发者定制规则,Shiro 去执行,具体的执行操作就是由 ShiroFilterFactoryBean 创建的一个个 Filter 对象来完成。

Shiro 运行流程

Shiro 运行流程

3. Spring Boot整合 Shiro

1、创建 Spring Boot 应用,集成 Shiro 及相关组件,pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

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

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.3.1.tmp</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

2、自定义 Shiro 过滤器

认证流程:
  1. 获取当前的Subject ,调用 SecurityUtils.getSubject()

  2. 测试当前的用户是否已经被认证,调用 Subject 的 isAuthenticatied()

  3. 若没有被认证,则把用户名和密码封装为 UsernamePasswoedToken 对象

  4. 执行登录,调用 Subject 的 login(AuthenticationToken)

  5. 自定义 Realm 的方法,从数据库中获取对应的记录,返回给 Shiro

    • 继承 org.apache.shiro.realm.AuthenticatingRealm 类
    • 实现 doGetAuthenticationInfo(AuthenticationToken)方法
  6. 由 Shiro 完成对密码的比对

    通过 AccoutRealm的 credentialsMatcher 属性进行密码比对

授权流程:
  1. 首先调用 Subject.isPermitted*/hasRole* 接口,其会委托给 SecurityManager,而 SecurityManager 接着会委托给 Authorizer;
  2. Authorizer是真正的授权者,如果调用如 isPermitted(“user:view”),其首先会通过PermissionResolver 把字符串转换成相应的 Permission 实例;
  3. 在进行授权之前,其会调用相应的 Realm 获取 Subject 相应的角色/权限用于匹配传入的角色/权限;
  4. Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断,只要有一个匹配 isPermitted*/hasRole* 会返回true,否则返回false表示授权失败
public class AccoutRealm extends AuthorizingRealm {

    @Autowired
    private AccountService accountService;

    /**
     * 授权
     * @param principalCollection
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        //获取当前登录的用户信息
        Subject subject = SecurityUtils.getSubject();
        Account account = (Account) subject.getPrincipal();

        //设置角色
        Set<String> roles = new HashSet<>();
        roles.add(account.getRole());
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roles);

        //设置权限
        info.addStringPermission(account.getPerms());
        return info;
    }


    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) 
      	throws AuthenticationException {
        
      	//1. 把 AuthenticationToken 转换为 UsernamePasswordToken 
				UsernamePasswordToken token = (UsernamePasswordToken) token;
      
      	//2. 调用数据库的方法, 从数据库中查询 username 对应的用户记录
        Account account = accountService.findByUsername(token.getUsername());
      
      	//3. 根据用户的情况, 来构建 AuthenticationInfo 对象并返回. 
        if(account != null){
          	//通常使用的实现类为: SimpleAuthenticationInfo
            //1). principal: 认证的实体信息. 可以是 username, 也可以是数据表对应的用户的实体类对象. 
            Object principal = account;
            //2). credentials: 密码.
            Object credentials = account.getPassword(); 
            //3). realmName: 当前 realm 对象的 name. 调用父类的 getName() 方法即可
            String realmName = getName();
            //4). 盐值. 需要唯一: 一般使用随机字符串或 user id
            ByteSource credentialsSalt = ByteSource.Util.bytes(account.getUsername());

            SimpleAuthenticationInfo info = null; 
            info = new SimpleAuthenticationInfo(principal, credentials, credentialsSalt, realmName);
            return info;
        }
      	
      	//4. 若用户不存在, 则可以抛出 UnknownAccountException 异常
        return null;
    }
}

3、配置类

@Configuration
public class ShiroConfig {

  	@Bean
    public AccoutRealm accoutRealm(){
        return new AccoutRealm();
    }

  	/*
  		Realm 对象已经注入到IOC容器中了,所以,Realm 注入到 DefaultWebSecurityManager 时,
  		直接在 IOC 容器中拿就行,使用@Qualifier("accoutRealm")注解定位到对应的Bean
  		一般来说,Bean名就是方法名,也可以自己指定@Bean(name="")
  	*/
    @Bean
    public DefaultWebSecurityManager securityManager(@Qualifier("accoutRealm") AccoutRealm accoutRealm){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
      	// Realm 注入到 DefaultWebSecurityManager
        manager.setRealm(accoutRealm);
        return manager;
    }

  	// DefaultWebSecurityManager 注入到 ShiroFilterFactoryBean 的过程与上面的一样
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        factoryBean.setSecurityManager(securityManager);
        return factoryBean;
    }
}

编写认证和授权规则:

认证过滤器:

  • anon:无需认证
  • authc:必须认证
  • authcBasic:需要通过HTTPBasic认证
  • user:不一定通过认证,只要曾经被Shiro记录即可,比如:记住我

授权过滤器:

  • perms:必须拥有某个权限才能访问
  • role:必须拥有某个角色才能访问
  • port:请求的端口必须是指定值才可以
  • rest:请求必须基于RESTful,POST、PUT、GET、DELETE
  • ssl:必须是安全的URL请求,协议HTTPS

Demo演示

1、创建 3 个页面,main.html、cheak.html、manager.html

访问权限如下:

1.1 必须登录才能访问 main.html

1.2 当前用户必须拥有 cheak权限才能访问 cheak.html

1.3 当前用户必须拥有 manager角色才能访问 manager.html

2、自定义认证、授权流程,Realm 类

3、注册到IOC容器,配置类

3.1 自定义权限规则

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    //权限设置
    Map<String,String> map = new Hashtable<>();
    map.put("/main","authc");
    map.put("/manage","perms[cheak]");
    map.put("/administrator","roles[manager]");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    //设置登录页面
    shiroFilterFactoryBean.setLoginUrl("/login");
    //设置未授权页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauth");
    return shiroFilterFactoryBean;
}

4、控制层逻辑

@Controller
public class AccountController {

    @GetMapping("/{url}")
    public String redirect(@PathVariable("url") String url){
        return url;
    }

    @PostMapping("/login")
    public String login(String username, String password, Model model){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        try {
            subject.login(token);
            Account account = (Account) subject.getPrincipal();
            subject.getSession().setAttribute("account",account);
            return "index";
        } catch (UnknownAccountException e) {
            // 捕获用户不存在异常
            e.printStackTrace();
            model.addAttribute("msg","用户名错误!");
            return "login";
        } catch (IncorrectCredentialsException e){
            // 捕获密码错误异常
            model.addAttribute("msg","密码错误!");
            e.printStackTrace();
            return "login";
        }
    }

    @GetMapping("/unauth")
    @ResponseBody
    public String unauth(){
        return "未授权,无法访问!";
    }

    @GetMapping("/logout")
    public String logout(){
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "login";
    }
}

Demo完整代码


4. 几个注意的点

多 Realm 认证策略

Authenticator 的职责是验证用户帐号,是 Shiro API 中身份验证核心的入口点:如果验证成功,将返回AuthenticationInfo 验证信息;此信息中包含了身份及凭证;如果验证失败将抛出相应的 AuthenticationException 异常。

SecurityManager 接口继承了 Authenticator,另外还有一个ModularRealmAuthenticator 实现,其委托给多个Realm 进行验证,验证规则通过 AuthenticationStrategy 接口指定。

  • FirstSuccessfulStrategy :只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略
  • AtLeastOneSuccessfulStrategy :只要有一个Realm验证成功即可,和 FirstSuccessfulStrategy 不同,将返回所有Realm身份验证成功的认证信息
  • AllSuccessfulStrategy :所有Realm验证成功才算成功,且返回所有 Realm身份验证成功的认证信息,如果有一个失败就失败了

ModularRealmAuthenticator 默认是 AtLeastOneSuccessfulStrategy策略

多 Realm 授权策略

Authorizer 会判断 Realm 的角色/权限是否和传入的匹配,如果有多个Realm,会委托给 ModularRealmAuthorizer 进行循环判断,只要有一个匹配 isPermitted*/hasRole* 会返回true,否则返回false表示授权失败。

权限注解

  • @RequiresAuthentication 表示当前Subject已经通过login 进行了身份验证;即 Subject. isAuthenticated() 返回 true
  • @RequiresUser 表示当前 Subject 已经身份验证或者通过记住我登录的
  • @RequiresGuest 表示当前Subject没有身份验证或通过记住我登录过,即是游客身份
  • @RequiresRoles(value={“admin”, “user”}, logical=Logical.AND) 表示当前 Subject 需要角色 admin 和user
  • @RequiresPermissions (value={“user:a”, “user:b”},logical= Logical.OR) 表示当前 Subject 需要权限 user:a 或user:b

引入thymeleaf模板引擎

1、pom.xml 引入依赖

<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

2、配置类添加 ShiroDialect

@Bean
public ShiroDialect shiroDialect(){
    return new ShiroDialect();
}

3、index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="shortcut icon" href="#"/>
</head>
<body>
    <h1>index</h1>
    <div th:if="${session.account != null}">
        <span th:text="${session.account.username}+'欢迎回来!'"></span><a href="/logout">退出</a>
    </div>
    <a href="/main">main</a> <br/>
    <div shiro:hasPermission="manage">
        <a href="manage">manage</a> <br/>
    </div>
    <div shiro:hasRole="administrator">
        <a href="/administrator">administrator</a>
    </div>
</body>
</html>
原文地址:https://www.cnblogs.com/chaozhengtx/p/14435376.html