Spring Boot「20」集成 Apache Shiro 实现认证功能

361 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 04 天,点击查看活动详情

Apache Shiro 是一款非常流行的开源安全框架,提供了认证、授权、会话管理和密码系统等功能。 安全问题历来是非常复杂,但却是系统开发者必须关注的问题之一。 Shiro 框架设计的主要目的之一就是帮助开发者解决系统中的安全问题。 今天,我将演示下如何把 Shiro 集成到 Spring Boot 应用中。

01-Shiro 中的基本概念

Shiro 主要特性可以分为四部分:

  1. 认证。简单理解就是提供登录功能,验证用户身份。
  2. 授权。简单理解就是控制用户可以访问系统中的哪些资源。
  3. 会话管理。管理用户会话。
  4. 密码系统。使用加密算法确保数据安全。

下图是 Apache Shiro 官网中给出的系统架构图。

img_shiro_spring-boot-application.png 我简单介绍下图中各个模块的主要作用:

  • Subject,指认证系统中的用户,可以是人类、其他服务器。
  • Security Manager,是 Shiro 框架的核心,负责认证、授权、会话管理、缓存等。
    • Authenticator,负责处理“用户”的登录请求。
    • Authorizer,负责确定“用户”的访问权限。
    • Session Manager & Session DAO,负责“用户”会话管理及持久化。 例如,在 Single Sign-On(SSO、单点登录)中,可通过 DAO 对象将会话持久化到 Redis 或数据库中。
    • Cache Manager,负责缓存。
    • Realms,可以理解为数据访问对象(DAO),它能够访问用户的相关信息,例如密码、角色、权限信息等。它是 Shiro 与业务之前的“桥梁”。
    • Cryptography,是 Shiro 中的密码系统,它封装了一系列的加密算法,并提供易用的 API。

注:Authenticator 和 Authorizer 都是通过一个或多个 Realms 来验证、获取用户相关的信息。

01.1-Shiro 中身份验证的步骤

在 Shiro 中,用户身份验证的主要步骤可以概括为:

  1. 收集可以识别用户(Subject)身份的属性(Principals)和加密数据(Credentials)。
  2. 将步骤1中收集的信息提交到认证系统(表现为各种 Realms)。
  3. 认证系统返回验证结果,例如通过、重试或拒绝访问等。

“用户”的身份属性由用户自己提供,例如他的登录名、密码。 用户的资格信息或者身份信息由 Realm 提供,即根据用户名或 ID 去数据库或其他第三方系统中查询。 Shiro 在收集完毕用户的登录信息和对应的资格信息后,会验证这些信息是否匹配。 然后,根据匹配结果,Shiro 确定是否拒绝访问或允许访问,或者是重定向用户到登录界面等操作。

下图是 Shiro 官网提供的一个简单认证流程。

img_shiro_authentication-sequence.png

接下来,我将在 Spring Boot 应用中实现这个流程。 实现步骤并不会严格按照图中标号的顺序进行,串联好后,可以运行一遍体会 Shiro 框架的工作原理。

02-Spring Boot 集成 Shiro 框架实现用户认证功能

首先,将 Shiro 引入到 Spring Boot 应用中。 通过 starter 工程,这一步骤是比较方便的。

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
</dependency>

接下来,我们要定义一个 UserRealm,用来存储用户的密码等信息(图中的步骤5)。

@Component
public class SimpleUserRealm extends SimpleAccountRealm {

    public SimpleUserRealm() {
        super();

        /**
         * 定义三个用户,user1\user2\user3
         */
        this.addAccount("user1", "password1");
        this.addAccount("user2", "password2");
        this.addAccount("user3", "password3");
    }
}

我通过继承 org.apache.shiro.realm.SimpleAccountRealm 类来实现了一个业务类 SimpleUserRealm,里面有三个用户。 在执行用户认证时,Realm 的主要功能是向 Shiro 提供用户的信息。换句话说,它知道去哪里读取用户的信息,比如密码。 在 SimpleUserRealm 中,用户的密码就写死在 Realm 的初始化函数中。 通常来说,系统中的 Realms 可以是从数据库中查找用户信息,也可以是从 LDAP 等系统中查找用户。

@Bean
@ConditionalOnMissingBean
@Override
protected SessionsSecurityManager securityManager(List<Realm> realms) {
    return super.securityManager(realms);
}

然后,需要初始化 Shiro 中的关键部分,即 SecurityManager(上图中的 2-4)。

@Configuration
public class AuthConfig {

    @Bean
    public SessionsSecurityManager securityManager(List<Realm> realms) {   // 能够从容器中拿到之前定义的 SimpleUserRealm
        DefaultSecurityManager securityManager = new DefaultSecurityManager();

        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        subjectDAO.setSessionStorageEvaluator(new DefaultSessionStorageEvaluator());

        securityManager.setSubjectDAO(subjectDAO);
        securityManager.setSubjectFactory(new DefaultSubjectFactory());

        ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
        authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        securityManager.setAuthenticator(authenticator);

        securityManager.setAuthorizer(new ModularRealmAuthorizer());

        securityManager.setRealms(realms);    // 设置到 securityManager 中

        DefaultSessionManager sessionManager = new DefaultSessionManager();
        sessionManager.setSessionDAO(new MemorySessionDAO());
        sessionManager.setSessionFactory(new SimpleSessionFactory());
        sessionManager.setDeleteInvalidSessions(true);
        securityManager.setSessionManager(sessionManager);

        securityManager.setEventBus(new DefaultEventBus());
        return securityManager;
    }
}

有了这些,接下来就可以将拼图完成,编写用户验证测试了。

@SpringBootApplication
public class AuthApplication implements CommandLineRunner {

    @Autowired
    private SimpleUserRealm userRealm;

    @Autowired
    private SessionsSecurityManager securityManager;

    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println(userRealm);

        UsernamePasswordToken token = new UsernamePasswordToken("user1", "password12");

        SecurityUtils.setSecurityManager(securityManager);  // 必须设置使用的 securityManager

        Subject subject = SecurityUtils.getSubject();
        subject.login(token);

        System.out.println(subject.isAuthenticated());   // 如果 token 中的密码是 password1 即我们在 realm 中设置的密码,此处打印 true
        
        subject.logout();  // 登出

    }
}

我创建了一个 Spring Boot 应用,并让其实现了 CommandLineRunner 接口。 这样,在容器启动后,会执行 run 方法。 在 run 方法中,我创建了一个 UsernamePasswordToken,它表示用户提交到 Shiro 系统中的用户信息,包括密码。 提交到 Shiro 中以后,SecurityManager 会去拿着 token 到所有的 Realms 中去查找对应用户的认证信息(subject.login(token);),如果验证不通过会抛异常出来。 验证通过后,subject.isAuthenticated() 的返回值为 true。

03-总结

以上就是Spring Boot 应用(非 Web 应用)如何使用 Shiro 框架进行安全认证的示例。 Web 应用于上述有点不太一样,我将在后面的文章中介绍。