Spring Boot学习篇(九)

时间:2023-01-14 18:52:42

Spring Boot学习篇(九)

shiro安全框架使用篇(一)

1.shiro初始了解

Apache Shiro 是一个强大易用的 Java 安全框架,提供了认证、授权、加密和会话管理等功能:
①认证 - 用户身份识别,常被称为用户“登录”;
②授权 - 访问控制;
③密码加密 - 保护或隐藏数据防止被偷窥;
④会话管理 - 每用户相关的时间敏感的状态。
对于任何一个应用程序,Shiro 都可以提供全面的安全管理服务。并且相对于其他安全框架,Shiro 要简单的多。

2.使用shiro实现简单的登录

2.1 准备工作

2.1.1 导入依赖
a shiro相关依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-starter</artifactId>
    <version>1.9.1</version>
</dependency>
b 完整版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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>boot-shiro</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!--①继承springboot项目-->
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.7.2</version>
    </parent>
    <!--② 导入相关依赖-->
    <dependencies>
        <!--2.1 导入springboot所需要的依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--2.2 导入mysql数据库依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--2.3 导入lombok依赖,便于写实体类-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!--2.4 导入mybatisPlus所需依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--2.5 导入springboot项目中单元测试所需依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>

        <!--2.6 导入代码生成器所需要的依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.1</version>
        </dependency>
        <!--thymeleaf模板引擎,freemarker 前后端分离不会用 普通的html格式-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!--热部署依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <!--redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--shiro依赖-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-starter</artifactId>
            <version>1.9.1</version>
        </dependency>
    </dependencies>
</project>
2.1.2 在java文件夹创建com包.zlz包,并创建ShiroStart类(springboot项目的启动类),其内容如下所示
package com.zlz;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;

@SpringBootApplication
public class ShiroStart {
    public static void main(String[] args) {
        SpringApplication.run(ShiroStart.class);
    }
}
2.1.3 在数据库里面运行如下的sql语句(建表和插入数据)
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission`  (
  `roleid` int NOT NULL DEFAULT 0,
  `perid` int NOT NULL DEFAULT 0,
  PRIMARY KEY (`roleid`, `perid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES (1, 9);
INSERT INTO `sys_role_permission` VALUES (2, 1);
INSERT INTO `sys_role_permission` VALUES (2, 2);
INSERT INTO `sys_role_permission` VALUES (2, 3);
INSERT INTO `sys_role_permission` VALUES (2, 4);
INSERT INTO `sys_role_permission` VALUES (2, 5);
INSERT INTO `sys_role_permission` VALUES (2, 6);
INSERT INTO `sys_role_permission` VALUES (2, 7);
INSERT INTO `sys_role_permission` VALUES (2, 8);
INSERT INTO `sys_role_permission` VALUES (3, 5);
INSERT INTO `sys_role_permission` VALUES (3, 6);
INSERT INTO `sys_role_permission` VALUES (3, 7);
INSERT INTO `sys_role_permission` VALUES (3, 8);


DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` int NOT NULL,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  `salt` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES (1, 'admin', '52663b302f9af774ed8f72186e750549', 'q1');
INSERT INTO `sys_user` VALUES (2, 'aaa', '5cae9116958db501061ac45449bdebe6', 'q2');
INSERT INTO `sys_user` VALUES (3, 'bbb', '46078596764bcea6cf58d1c770a26689', 'q3');


DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` int NOT NULL,
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, '经理');
INSERT INTO `sys_role` VALUES (2, '组长');
INSERT INTO `sys_role` VALUES (3, '组员');




DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission`  (
  `id` int NOT NULL,
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES (1, '添加商品');
INSERT INTO `sys_permission` VALUES (2, '删除商品');
INSERT INTO `sys_permission` VALUES (3, '修改商品');
INSERT INTO `sys_permission` VALUES (4, '查询商品');
INSERT INTO `sys_permission` VALUES (5, '添加订单');
INSERT INTO `sys_permission` VALUES (6, '删除订单');
INSERT INTO `sys_permission` VALUES (7, '修改订单');
INSERT INTO `sys_permission` VALUES (8, '查询订单');
INSERT INTO `sys_permission` VALUES (9, '*');




DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role`  (
  `userid` int NOT NULL DEFAULT 0,
  `roleid` int NOT NULL DEFAULT 0,
  PRIMARY KEY (`userid`, `roleid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (1, 1);
INSERT INTO `sys_user_role` VALUES (2, 2);
INSERT INTO `sys_user_role` VALUES (3, 3);
2.1.4 运行完成后数据库里应该有五张表

Spring Boot学习篇(九)

2.1.5 在sys_user表中将密码改成如下图所示的数据

Spring Boot学习篇(九)

2.1.6 在sys_user表增加锁定字段suo(数字0代表正常用户,数字1代表该用户被锁定了)

Spring Boot学习篇(九)

2.1.7 在zlz包下创建好代码生成器MyGenerator类并运行该类
package com.zlz;

import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.config.rules.DateType;

import java.util.Collections;

public class MyGenerator {
    public static void main(String[] args) {
        //不需要写一个重复的实体类,把模块也覆盖掉 注释掉,一般是只开一次
        FastAutoGenerator.create("jdbc:mysql://127.0.0.1:3305/db0618", "root", "root")
                .globalConfig(builder -> {
                    builder.author("zlz") // 设置作者
                            .dateType(DateType.ONLY_DATE) // 时间策略,日期以什么样子的类型去生成
                            .outputDir("F:\\boot\\boot-shiro\\src\\main\\java"); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent("com.zlz") // 设置父包名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, "F:\\boot\\boot-shiro\\src\\main\\resources\\mapper")); // 设置mapperXml生成路径,
                    // 注意:这个mapper文件夹是可以不存在的,运行该main方法会自动创建的
                })
                .strategyConfig(builder -> {
                    //开启lombok
                    builder.entityBuilder().enableLombok();
                    //覆盖现有文件
//                    builder.entityBuilder().fileOverride();
                    builder.addInclude("sys_user"); // 设置需要生成的表名,想一次性生成多张表,就用逗号隔开,如:addInclude("songs","music")
                })
                .execute();
    }
}
2.1.8 变更实体类SysUser类的内容
package com.zlz.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;

import lombok.*;
//①改动位置一,把注解改成@AllArgsConstructor、@NoArgsConstructor、@Data三个注解
@AllArgsConstructor
@NoArgsConstructor
@Data
/*@TableName里面注解的表名就是数据库中的表名,当然这里也可以省略不写,因为会自动执行驼峰转下划线的操作*/
@TableName("sys_user")
public class SysUser implements Serializable {

    private static final long serialVersionUID = 1L;
    //②改动位置2加上@Tableid注解
    @TableId(type = IdType.AUTO )
    private Integer id;

    private String username;

    private String password;

    private String salt;//盐
    private Integer suo;//账户是否锁定
}
2.1.9 在zlz包下的config包下创建Mybatis配置类PlusConfig
package com.zlz.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.zlz.mapper")//mapper接口扫描,@MapperScan也可以放在启动类PlusStart上面(启动类也算配置类)
public class PlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        //设置分页插件,别的拦截器也是addInnerInterceptor加入进去
        PaginationInnerInterceptor pi = new PaginationInnerInterceptor();
        pi.setDbType(DbType.MYSQL);//设置数据库类型为MySQL
        pi.setOverflow(true);//溢出分页处理,默认是false不处理,需要设置成true,保证分页合理化
        interceptor.addInnerInterceptor(pi);
        //加上乐观锁插件
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}
2.1.10 配置application.yml文件,其内容如下所示
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource #连接池类型
    url: jdbc:mysql://127.0.0.1:3305/db0618
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  thymeleaf:
    cache: false #开发阶段关闭缓存 上线后 开启
mybatis-plus:
  type-aliases-package: com.zlz.entity #实体别名扫描
#配置日志 查看具体的sql执行语句
logging:
  level:
    com.zlz.mapper: debug

2.2 在SysUserMapper接口中写findUser方法,对应的代码如下所示

package com.zlz.mapper;

import com.zlz.entity.SysUser;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import java.util.List;
public interface SysUserMapper extends BaseMapper<SysUser> {
   //根据用户名查询用户
    SysUser findUser(String username);
}

2.3 在SysUserMapper.xml写findUser方法所对应的SELECT标签(自定义sql)

<?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.zlz.mapper.SysUserMapper">
    <select id="findUser" resultType="sysUser">
        select * from sys_user where username=#{username}
    </select>
</mapper>

2.4 在zlz包下的config包下创建域(认证、授权需要调用的dao类)

package com.zlz.config;

import com.zlz.entity.SysUser;
import com.zlz.mapper.SysUserMapper;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

//普通类,需要继承AuthorizingRealm这个抽象类
public class MysqlRealm  extends AuthorizingRealm {
    /**
     * 授权方法 当需要验证当前用户是否具有角色或者权限是 会自动调用
     *
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection p) {
        return null;
    }

    /**
     * 认证方法 当用户登录时 会自动调用
     * 登录流程
     *    用户输入用户名和密码 保存在usernamepasswordToken对象中
     *    登录时会自动调用认证方法,拿到登录的用户名去数据库查询
     *    若该用户名不存在 抛出相应异常 表示登录失败
     *    如果数据库存在该用户名 就进行密码比对,比对失败 抛出密码不对的异常
     *    如果没有任何异常抛出 说明比对成功即登录成功
     */
    @Autowired
    SysUserMapper userMapper;
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken a) throws AuthenticationException {
        //①先拿到登录时 输入的用户名
        String username = (String) a.getPrincipal();
        //去数据库查询该用户名是否存在
        SysUser user = userMapper.findUser(username);
        System.out.println(username+"123");
        //如果账户不存在 抛出相应异常
        if (user == null) {
            //账户不存在
            throw new UnknownAccountException();
        }
        //判断账户是否锁定(依据需求 可选)
        if(user.getSuo()==1){
            throw new LockedAccountException();
        }
        //密码异常java会自动抛出
        //将数据库的账户密码对象 返回出去 和登录输入的账户密码比对
        //第一个参数数据库查出来的账户,第二个是查出来的密码,第三个是域的名字
        SimpleAuthenticationInfo sai=new SimpleAuthenticationInfo(user.getUsername(),user.getPassword(),getName());

        return sai;
    }
}

2.5 在config下创建Shiro配置类ShiroConfig,其内容如下所示

package com.zlz.config;

import org.apache.shiro.realm.Realm;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ShiroConfig {
    //shiro相关的所有配置

    /**
     *
     * securityManager 安全管理器核心配置类
     */
    @Bean
    public DefaultWebSecurityManager securityManager(){
        //方法的返回值是什么,我们就会new一个这样类型的对象去返回
        DefaultWebSecurityManager dws = new DefaultWebSecurityManager();
        //设置域(认证、授权 需要调用的dao类 这个类中完成登录查询角色权限)

        dws.setRealm(MyRealm());
        //设置会话管理器,默认的会话管理器会携带jssionid导致第一次访问时无论账户密码是否输入有误,均会报错
        dws.setSessionManager(new DefaultWebSessionManager());
        return dws;
    }


    @Bean
    public Realm MyRealm(){
        MysqlRealm mysqlRealm = new MysqlRealm();
        return mysqlRealm;
    }
    @Bean("shiroFilterFactoryBean")//在spring容器中的id必须是shiroFilterFactoryBean
    public ShiroFilterFactoryBean factoryBean(){
        ShiroFilterFactoryBean sff=new ShiroFilterFactoryBean();
        //设置安全管理器
        sff.setSecurityManager(securityManager());
        //检测到需要登录但没有登录的地址(比如购物商城中的点击购物车会通过该方法跳转到登录页面)
        sff.setLoginUrl("");
        //检测到没有权限的跳转地址
        sff.setUnauthorizedUrl("");
        //成功后的地址由视图解析器去跳转,就不写这里面的东西了
        return sff;
    }
}

2.6 在controll包下的SysUserController配置登录流程的第一步和最后一步(保存对象和显示当前状态)

package com.zlz.controller;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
@RequestMapping("/users")
public class SysUserController {
    @RequestMapping("login")
    public String login(String username, String password, RedirectAttributes ra){
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username,password);
        try {
            subject.login(token);
            return "index";
        } catch (UnknownAccountException e) {
            ra.addFlashAttribute("msg", "用户名不存在");
        }catch (IncorrectCredentialsException e) {
            ra.addFlashAttribute("msg", "密码输入错误");
        }catch (LockedAccountException e) {
            ra.addFlashAttribute("msg", "账户已被锁定");
        }
        return "redirect:/";
    }

}

2.7 配置WelcomeController(默认显示页面以及页面的跳转)

package com.zlz.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;

@Controller
public class WelcomeController {
    //访问localhost:8080 默认进入b.html,默认没有配置视图解析器
    @RequestMapping("/")
    public String welcome(){
        //默认进入扫描页面
        return "login";
    }
    /*专门跳页面*/
    @RequestMapping("tohtml")
    public String tohtml(HttpServletRequest request){
        return  (String)request.getAttribute("pageName");
    }

}

2.8 测试

2.8.1 当账户有误时
a 点击登录按钮前

Spring Boot学习篇(九)

b 点击登录按钮后

Spring Boot学习篇(九)

2.8.2 当账户被锁定时
a 点击登录按钮前

Spring Boot学习篇(九)

b 点击登录按钮后

Spring Boot学习篇(九)

2.8.3 当密码输入有误时(该用户并没有被锁定)
a 点击登录按钮前

Spring Boot学习篇(九)

b 点击登录按钮后

Spring Boot学习篇(九)

2.8.4 当账户密码输入均正确时
a 点击登录按钮前

Spring Boot学习篇(九)

b 点击登录按钮后

Spring Boot学习篇(九)