跳到主要内容

02、Shiro 速成:SpringBoot+Shiro 现用户身份认证功能

一、概述

前面一篇文章,我们已经总结了Shiro相关的一些概念以及架构知识,相信小伙伴们对Shiro安全框架都有了一定的认识。本篇文章我们将通过示例详细说明在日常工作中常见的-----用户身份认证功能。

什么是身份认证呢,简单理解,就是在应用中谁能证明他就是他本人。一般提供如他们的身份 ID 一些标识信息来表明他就是他本人,如提供身份证,用户名 / 密码来证明。

在shiro 中,用户需要提供 principals (身份)和 credentials(证明)给 shiro,从而应用能验证用户身份:

1、 principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。

2、 credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。

最常见的 principals 和 credentials 组合就是用户名 / 密码。

接下来,我们以SpringBoot为基础,通过整合Shiro来实现用户身份认证功能。

二、身份认证流程

在Spring Boot中集成Shiro进行用户的认证过程主要有以下三点:

1、 定义一个ShiroConfig,然后配置SecurityManager,SecurityManager是Shiro的安全管理器,管理着所有Subject;

2、 在ShiroConfig中配置ShiroFilterFactoryBean,其为Shiro过滤器工厂类,依赖于SecurityManager;

3、 自定义Realm实现;

接下来,我们创建一个SpringBoot项目:【springboot-shiro】,整体项目结构如下图所示:

 

前提:我们先创建一个 数据库shiro,然后创建一张用户表user并初始化一条用户信息,表sql如下:

CREATE TABLE user (
  id varchar(32) COLLATE utf8_bin NOT NULL COMMENT '用户id',
  username varchar(64) COLLATE utf8_bin NOT NULL COMMENT '用户名',
  password varchar(32) COLLATE utf8_bin NOT NULL COMMENT '用户密码',
  status char(1) COLLATE utf8_bin DEFAULT '1' COMMENT '用户状态',
  PRIMARY KEY (id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=COMPACT

并初始化一条用户信息:

INSERT INTO user VALUES ('1', 'admin', '123456', '1');

【a】引入一些相关的依赖:mysql、mybatis、thymeleaf、shiro等。具体pom.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.10.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wsh.springboot</groupId>
    <artifactId>springboot-shiro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-shiro</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.3</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </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>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

【b】定义User实体类,接口UserMapper外加上UserMapper.xml配置实现

public class User {
    private String id;
    private String username;
    private String password;
    private String status;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

UserMapper.java:

@Mapper
public interface UserMapper {

    /**
     * 根据用户名查找用户信息
     *
     * @param name 用户名
     * @return
     */
    User findUserByName(@Param("name") String name);
}

UserMapper.xml:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wsh.springboot.springbootshiro.mapper.UserMapper">

    <resultMap type="com.wsh.springboot.springbootshiro.entity.User" id="baseUser">
    <id column="id" property="id" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
    <id column="status" property="status" javaType="java.lang.String" jdbcType="VARCHAR"/>
  </resultMap>

  <select id="findUserByName" resultMap="baseUser" parameterType="String">
    SELECT * FROM USER t
    where t.username ={name}
  </select>
</mapper>

【c】配置文件 application.yml,配置数据源信息、mybatis扫描的包等

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://localhost:3306/shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: com.wsh.springboot.springbootshiro.entity
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

【d】自定义Realm,通过继承AuthenticatingRealm 实现

需要重写doGetAuthenticationInfo方法。

package com.wsh.springboot.springbootshiro.realm;

import com.wsh.springboot.springbootshiro.entity.User;
import com.wsh.springboot.springbootshiro.mapper.UserMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

public class MyShiroRealm extends AuthenticatingRealm {

    private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);

    @Autowired
    private UserMapper userMapper;

    /**
     * 认证相关方法
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
        //1.判断用户名, token中的用户信息是登录时候传进来的
        String username = usernamePasswordToken.getUsername();
        char[] password = usernamePasswordToken.getPassword();
        logger.info("username:" + username);
        logger.info("password:" + new String(password));

        //通过账号查找用户信息
        User user = userMapper.findUserByName(username);
        if (null == user) {
            logger.error("用户不存在..");
            throw new UnknownAccountException("用户不存在!");
        }
        if ("0".equals(user.getStatus())) {
            throw new LockedAccountException("账号已被锁定,请联系管理员!");
        }

        //数据库中查询的用户名
        Object principal = user.getUsername();
        //数据库中查询的密码
        Object credentials = user.getPassword();
        String realmName = getName();

        //2.判断密码
        return new SimpleAuthenticationInfo(principal, credentials, null, realmName);
    }
}

【e】创建Shiro的全局配置类

import com.wsh.springboot.springbootshiro.realm.MyShiroRealm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.DelegatingFilterProxy;

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

/**
 * @Description: Shiro全局配置类
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @Date: 2022/11/3 09:23
 * <p>
 * 三大组件:
 * 1. Subject: 用户主体(把操作交给SecurityManager)
 * 2. SecurityManager:安全管理器(关联Realm)
 * 3. Realm:Shiro连接数据的桥梁
 */
@Configuration
public class ShiroConfiguration {

    /**
     * 将Realm注册到securityManager中
     *
     * @return
     */
    @Bean("securityManager")
    public DefaultWebSecurityManager securityManager() {
        return new DefaultWebSecurityManager(myShiroRealm());
    }

    /**
     * 配置自定义的Realm
     */
    @Bean
    public MyShiroRealm myShiroRealm() {
        return new MyShiroRealm();
    }

    /**
     * 如果没有此name,将会找不到shiroFilter的Bean
     * <p>
     * Shiro内置过滤器,可以实现权限相关的拦截器
     * 常用的过滤器:
     * anon: 无需认证(登录)可以访问
     * authc: 必须认证才可以访问
     * user: 如果使用rememberMe的功能可以直接访问
     * perms: 该资源必须得到资源权限才可以访问
     * role: 该资源必须得到角色权限才可以访问
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilter(@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //表示指定登录页面
        shiroFilterFactoryBean.setLoginUrl("/userLogin");
        //登录成功后要跳转的链接
        shiroFilterFactoryBean.setSuccessUrl("/success");
        //未授权页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
        //拦截器, 配置不会被拦截的链接 顺序判断
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        //所有匿名用户均可访问到Controller层的该方法下
        filterChainDefinitionMap.put("/index", "anon");
        filterChainDefinitionMap.put("/userLogin", "anon");
        //authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * SpringShiroFilter首先注册到spring容器
     * 然后被包装成FilterRegistrationBean
     * 最后通过FilterRegistrationBean注册到servlet容器
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean delegatingFilterProxy() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        DelegatingFilterProxy proxy = new DelegatingFilterProxy();
        proxy.setTargetFilterLifecycle(true);
        proxy.setTargetBeanName("shiroFilter");
        filterRegistrationBean.setFilter(proxy);
        return filterRegistrationBean;
    }
}

【f】在resource/templates文件夹下创建几个html文件:

index.html:

<!doctype html>

<!--注意:引入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
<h3 style="color: red" th:text="${msg}"></h3><br/>
<form method="post" action="/userLogin">
    用户名: <input type="text" name="username"><br/>
    密码: <input type="password" name="password"><br/>
    <input type="submit" name="submit"><br/>
</form>
</body>
</html>

success.html:

<!doctype html>

<!--注意:引入thymeleaf的名称空间-->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
你好,这是登录成功的页面<br/>
</div>
</body>
</html>

unauthorized.html:

<!doctype html>

<!--注意:引入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
抱歉,你暂未授权访问此页面
</body>
</html>

【g】编写UserController

package com.wsh.springboot.springbootshiro.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class UserController {
    private static final Logger logger = LoggerFactory.getLogger(UserController.class);

    @GetMapping("/index")
    public String index() {
        //返回index.html
        return "index";
    }
    
    @GetMapping("/success")
    public String success() {
        return "success";
    }

    @RequestMapping(value = "/userLogin", method = RequestMethod.POST)
    public String toLogin(String username, String password, Model model) {
        //1.获取Subject
        Subject subject = SecurityUtils.getSubject();
        //2.封装用户数据
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        try {
            //3.执行登录方法
            subject.login(token);
            //4.登录成功,然后跳转到success.html
            return "redirect:/success";
        } catch (UnknownAccountException e) {
            logger.error("msg:该账号不存在");
            model.addAttribute("msg", "该账号不存在");
            return "index";
        } catch (IncorrectCredentialsException e) {
            logger.error("msg: 密码错误,请重试");
            model.addAttribute("msg", "密码错误,请重试");
            return "index";
        } catch (LockedAccountException e) {
            logger.error("msg:该账户已被锁定,请联系管理员");
            model.addAttribute("msg", "该账户已被锁定,请联系管理员");
            return "index";
        } catch (Exception e) {
            model.addAttribute("msg", "登录失败");
            logger.error("msg: 登录失败");
            return "index";
        }
    }

    @RequestMapping("/unauthorized")
    public String unauthorized() {
        return "unauthorized";
    }

}

【h】主启动类

@SpringBootApplication
@MapperScan("com.wsh.springboot.springbootshiro.mapper")
public class SpringbootShiroApplication {

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

}

【i】启动项目,进行测试

浏览器访问:http://localhost:8080/index,输入admin/123456:

 

可以看到,因为此时用户名和密码跟数据库中是一致的,所以登录成功,并跳转到success.html中。

 

重新访问http://localhost:8080/index,输入admin11111/123456,如下图:

 

可以看到,由于用户表中不存在此用户,所以提示账号不存在。

重新访问http://localhost:8080/index,输入admin/111111,如下图:

 

可见,当输入错误的密码时,shiro提示密码错误。

三、总结

以上就是关于Shiro提供了用户身份认证功能,总结一下大体步骤:

  • 自定义Realm,继承AuthenticatingRealm ,重写方法doGetAuthenticationInfo(AuthenticationToken authenticationToken),通过subject.login方法传入的token,去数据库中查询用户数据,将用户密码传入shiro进行比对;
  • 自定义Shiro配置,注入SecurityManager安全管理器、ShiroFilterFactoryBean过滤器工厂等对象;

存在问题:在实际工作中,密码不可能明文传输,为了演示方便,上面的示例使用的明文密码比对,实际上Shiro也提供了加密功能,下一篇文章我们将优化成密文比对。