Spring系列一品境之指玄境

Spring系列复习(二)


相关导航
Spring系列一品境之金刚境


Spring指玄境导航

  • Spring系列复习(二)
  • 前言
  • 一、总思维导图
  • 二、Mybatis
    • 1、起因
    • 2、框架
    • 3、开发
      • 3.1 原始DAO方法
      • 3.2 Mapper代理方法
      • 3.3 输入输出映射
      • 3.4 动态SQL
      • 3.5 SpringBoot整合Mybatis
        • 3.5.1 引入依赖
        • 3.5.2 配置连接池
        • 3.5.3 Mybatis增删改查
          • 1)注解方式
          • 2)XML方式
      • 3.6 实战经验
      • 3.7 与VO进行联系
    • 4、实战记录
    • 二、Shiro框架
      • 1、Shiro架构图
        • 1.1 Subject
        • 1.2 SecurityManager
        • 1.3 Authenticator
        • 1.4 Authorizer
        • 1.5 Realm
        • 1.6 SessionManager
        • 1.7 SessionDAO
        • 1.8 CacheManager
        • 1.9 Cryptography
      • 2、常用的Jar包
      • 2.1 SpringBoot项目整合Shiro
        • 2.1.1 概念梳理
          • 1)四种权限检验方式
          • 2)JWT
        • 2.1.2 导入依赖
        • 2.1.3 自定义Realm类
          • 1)AuthenticationToken
          • 2)AuthenticationInfo
          • 3)PincipalCollection
          • 4)AuthorizationInfo
          • 5)Subject
        • 2.1.4 编写Shiro配置类
        • 2.1.5 Controller登录逻辑
        • 2.1.6 Shiro加密与解密
          • 1)密码比对
          • 2)MD5盐值加密
        • 2.1.7 补充
          • 1)Realm判断逻辑
          • 2)Realm中注入Service
          • 3)使用Shiro内置过滤器拦截资源
          • 4)动态授权逻辑
      • 2.2 Shiro+JWT+Redis实例
  • 三、Redis
    • 1、Redis简介
    • 2、Redis的数据结构
      • 2.1 String类型
      • 2.2 哈希类型
      • 2.3 列表类型
      • 2.4 集合类型
      • 2.5 顺序集合类型
    • 3、SpringBoot整合Redis
      • 3.1 导入依赖
      • 3.2 接口中添加Redis缓存
        • 3.2.1 添加Redis配置
        • 3.2.2 启动Redis服务
      • 3.3 启动类配置
      • 3.4 Redis配置类
      • 3.5 Redis工具类
      • 3.6 业务使用
      • 3.7 补充
      • 3.8 常用注解
        • 3.8.1 @Cacheable
        • 3.8.2 @CachePut
        • 3.8.3 @CacheEvict
      • 3.9 存在问题
    • 4、技术升级
      • 4.1 数据持久化
        • 4.1.1 RDB方式
        • 4.1.2 AOF方式
      • 4.2 雪崩
        • 4.2.1 雪崩定义
        • 4.2.2 规避方案
      • 4.3 击穿
        • 4.3.1 击穿定义
        • 4.3.2 发生原因
        • 4.3.3 规避方案
  • 四、Ngnix
    • 1、负载均衡定义
    • 2、反向代理负载均衡
    • 3、Ngnix
    • 4、SpringBoot集成Ngnix
      • 4.1 Ngnix下载
        • 4.1.1 容器方式
      • 4.2 启动Ngnix
        • 4.2.1 命令
        • 4.2.2 注意点
      • 4.3 配置反向代理
        • 4.3.1 找到ngnix.conf文件
        • 4.3.2 编辑该配置文件
  • 五、Docker
    • 1、Docker定义
    • 2、Docker教程
    • 3、SpringBoot打包成Docker容器
      • 3.1 plugin
      • 3.2 DockerFile
      • 3.3 docker-maven-plugin 远程仓库
      • 3.4 项目打包
      • 3.5 创建容器并运行


前言

本博文重在夯实Spring全家桶的知识点,回归书本,夯实基础,学深学精

Java相关基础已复习完毕,现在就到了Spring全家桶系列了,欲练神功,先固内功。之前做项目对Spring全家桶学的一知半解,好多基础概念都不清楚,正好借此机会梳理一下相关知识点。
参考书籍:《Spring In Action 5th EDITION》与《多线程与高并发 马士兵丛书》


 本博文主要归纳整理Spring全家桶中SpringBoot整合MybatisMybatis-plusDockerRedisShiroNgnix的一些方法。

一、总思维导图

SpringBoot项目实战
Mybatis-plus
Shiro
Redis
Ngnix
Docker

二、Mybatis

1、起因

其实简单来说,可以用发展的眼光去看待Mybatis。
其是对JDBC改进和完善
Spring系列一品境之指玄境_第1张图片
主要元素

  • xml文件
  • DAO层或者Mapper层(其实两个是相同概念)

2、框架

Spring系列一品境之指玄境_第2张图片

Spring系列一品境之指玄境_第3张图片

3、开发

3.1 原始DAO方法

3.2 Mapper代理方法

Mapper代理开发方法,只需要编写Mapper接口,但需要遵循开发规范
Spring系列一品境之指玄境_第4张图片
Spring系列一品境之指玄境_第5张图片
Spring系列一品境之指玄境_第6张图片

3.3 输入输出映射

Spring系列一品境之指玄境_第7张图片

3.4 动态SQL

Spring系列一品境之指玄境_第8张图片

3.5 SpringBoot整合Mybatis

Spring系列一品境之指玄境_第9张图片

3.5.1 引入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.2</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.20</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>

依赖关系
Spring系列一品境之指玄境_第10张图片

3.5.2 配置连接池

spring:
  datasource:
    username: root
    password: 1111
    url: jdbc:mysql://localhost:3306/springboot_mybatis
    driver-class-name: com.mysql.jdbc.Driver
    initialization-mode: always
    # 数据源更改为druid
    type: com.alibaba.druid.pool.DruidDataSource

    druid:
      # 连接池配置
      # 配置初始化大小、最小、最大
      initial-size: 1
      min-idle: 1
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 3000
      validation-query: SELECT 1 FROM DUAL
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      pool-prepared-statements: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      filters: stat,wall,slf4j
      # 配置web监控,默认配置也和下面相同(除用户名密码,enabled默认false),其他可以不配
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: admin
        login-password: root
        allow: 127.0.0.1

    schema:
      - classpath:sql/department.sql
      - classpath:sql/employee.sql
//开启驼峰映射
mybatis:
  configuration:
    map-underscore-to-camel-case: true

3.5.3 Mybatis增删改查

1)注解方式
  • 创建Mapper接口

// 指定这是一个操作数据库的mapper
@Mapper // 这里必须要添加这个Mapper注解; 也可以在主启动类上统一通过@MapperScan(value="con.zy.mapper")来扫描
public interface DepartmentMapper {

    @Select("SELECT * FROM department WHERE id = #{id}")
    public Department getDeptById(@Param("id") Integer id);

    @Delete("DELETE FROM department WHERE id = #{id}")
    public int deleteDeptById(@Param("id") Integer id);

    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("INSERT INTO department(department_name) VALUES(#{departmentName})")
    public int insertDept(Department department);

    @Update("UPDATE department SET department_name = #{departmentName} WHERE id = #{id}")
    public int updateDept(Department department);
}

  • 调用
@RestController
public class DeptController {

    @Resource
    private DepartmentMapper departmentMapper;  //重点

    @GetMapping("/dept/{id}")
    public Department getDepartment(@PathVariable("id") Integer id) {
        return departmentMapper.getDeptById(id);
    }

    @GetMapping("/dept")
    public Department insertDept(Department department) {
        int count = departmentMapper.insertDept(department);
        if (count > 0) {
            System.out.println("插入数据成功");
        }
        return department;
    }

  • Mapper扫描
  • 使用@mapper注解的类可以被扫描到容器中,但是每个Mapper都要加上这个注解就是一个繁琐的工作
  • 可以在springboot启动类上加上@MapperScan
@MapperScan(“cn.clboy.springbootmybatis.mapper”)//扫描某个包下的所有Mapper接口
2)XML方式
  • 创建Mybatis全局配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

    <!-- 开启数据库中列名和pojp的驼峰命名映射 -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
</configuration>

  • 创建Mapper接口
@Mapper 或者 @MapperScan将接口扫描装配到容器中
public interface EmployeeMapper {

    public Employee getEmpById(@Param("id") Integer id);

    public void insertEmp(Employee employee);
}

  • 创建映射文件mapper.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.zy.mapper.EmployeeMapper">

    <select id="getEmpById" resultType="com.zy.pojo.Employee">
        SELECT * FROM employee WHERE id = #{id};
    </select>

    <insert id="insertEmp">
        INSERT INTO employee (lastName, email, gender, d_id) VALUSE (#{lastName}, #{email}, #{gender}, #{dId})
    </insert>
</mapper>

  • 配置文件application.yaml
# 加载mybati的全局配置文件
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml
  mapper-locations: classpath:mybatis/mapper/*.xml

  • 调用
@RestController
public class EmpController {
	@Resource
    private EmployeeMapper employeeMapper;//通过这个方式注入mapper

    @GetMapping("/emp/{id}")
    public Employee getEmp(@PathVariable("id") Integer id) {
        return employeeMapper.getEmpById(id);
    }
}

3.6 实战经验

Spring系列一品境之指玄境_第11张图片

<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">//nameplace对应Mapper.java全路径
<!-- 根据id获取用户信息 -->//根据id映射到Mapper接口
	<select id="findByUserId" parameterType="int" resultType="cn.itcast.mybatis.po.User">
		select * from user where id = #{id}
	</select>
</mapper>



---
SqlMapConfig。xml配置文件
	<mappers>
		<mapper resource="Sqlmap/User.xml" />
	</mappers>


---
测试代码
public class MapperTest {

	private SqlSessionFactory sqlSessionFactory;
	@Before
	public void setUp() throws IOException{
		String resource="SqlMapConfig.xml";
		InputStream inputStream= Resources.getResourceAsStream(resource);
		sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream);	//创建配置工厂
	}
	
	@Test
	public void test() {
		SqlSession sqlSession =sqlSessionFactory.openSession();
		UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
		User user=userMapper.findByUserId(1);
		System.out.println(user);
		sqlSession.close();
	}

}

3.7 与VO进行联系

在Service层中实现类的方法的形参列表VO对象即可

@Service
public class AdminServiceImpl extends SuperServiceImpl<AdminMapper, Admin> implements AdminService {

    @Autowired
    AdminService adminService;
    @Autowired
    RedisUtil redisUtil;
    @Autowired
    SysParamsService sysParamsService;
    @Resource
    private AdminMapper adminMapper;
    @Autowired
    private WebUtil webUtil;
    @Resource
    private PictureFeignClient pictureFeignClient;
    @Autowired
    private RoleService roleService;

    @Override
    public Admin getAdminByUid(String uid) {
        return adminMapper.getAdminByUid(uid);
    }

    @Override
    public String getOnlineAdminList(AdminVO adminVO) {
        // 获取Redis中匹配的所有key
        Set<String> keys = redisUtil.keys(RedisConf.LOGIN_TOKEN_KEY + "*");
        List<String> onlineAdminJsonList = redisUtil.multiGet(keys);
        // 拼装分页信息
        int pageSize = adminVO.getPageSize().intValue();
        int currentPage = adminVO.getCurrentPage().intValue();
        int total = onlineAdminJsonList.size();
        int startIndex = Math.max((currentPage - 1) * pageSize, 0);
        int endIndex = Math.min(currentPage * pageSize, total);
        //TODO 截取出当前分页下的内容,后面考虑用Redis List做分页
        List<String> onlineAdminSubList = onlineAdminJsonList.subList(startIndex, endIndex);
        List<OnlineAdmin> onlineAdminList = new ArrayList<>();
        for (String item : onlineAdminSubList) {
            OnlineAdmin onlineAdmin = JsonUtils.jsonToPojo(item, OnlineAdmin.class);
            // 数据脱敏【移除用户的token令牌】
            onlineAdmin.setToken("");
            onlineAdminList.add(onlineAdmin);
        }
        Page<OnlineAdmin> page = new Page<>();
        page.setCurrent(currentPage);
        page.setTotal(total);
        page.setSize(pageSize);
        page.setRecords(onlineAdminList);
        return ResultUtil.successWithData(page);
    }
}

简单来说,Service层是将Mapper和VO联系起来的媒介

4、实战记录

		
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>${druid.version}version>     
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druid-spring-boot-starterartifactId>
            <version>1.1.9version>
        dependency>

		 
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>${lombok.version}version>
        dependency>


		<dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.59version>
        dependency>


		<dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>6.0.6version>
            <scope>runtimescope>
        dependency>

		 
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.1version>
        dependency>
  • 使用注解模式时,在mapper接口,写了@Mapper不能@Component注解了,会产生注解冲突
  • @Component 泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注,它的作用就是实现Bean的注入。
  • @Component 和 @Bean 是两种使用注解来定义bean的方式。 @Component 注解作用于,而@Bean注解作用于方法
    @Component(和@Service、@Repository等)用于自动检测和使用类路径扫描自动配置Bean。注释类和Bean之间存在隐式的一对一映射(即每个类一个bean)。
    这种方法对需要进行逻辑处理的控制非常有限,因为它纯粹是声明性的。
  • @Bean用于显式声明单个Bean,而不是让Spring像上面那样自动执行它。它将Bean的声明类定义分离,并允许您精确地创建和配置Bean
@Component
public class Student {
 
    private String name = "lkm";
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
}
-------
@Configuration
public class WebSocketConfig {
    @Bean
    public Student student(){
        return new Student();
    }
 
}

二、Shiro框架

Shiro是一个功能强大且易于使用的Java安全框架。

  • 身份验证
  • 会话管理
  • 授权
  • 加密
  • 缓存授权

使用Shiro易于理解的API,您可以快速轻松地保护任何应用程序—从最小的移动应用程序到最大的web和企业应用程序

1、Shiro架构图

Spring系列一品境之指玄境_第12张图片

1.1 Subject

  • 即主体,Subject记录了当前操作用户,可以将用户的概念理解为当前操作的主体,可能是用户,也可能是程序
  • 外部程序通过Subject进行认证授,而Subject是通过SecurityManager安全管理器进行认证授权

1.2 SecurityManager

  • SecurityManager即安全管理器,对全部的Subject进行安全管理,它是Shiro的核心,负责对所有的Subject进行安全管理。
  • 通过SecurityManager可以完成Subject的认证、授权等
  • 实质上SecurityManager是通过Authenticator进行认证、通过Authorizer进行授权、通过SessionManager进行会话管理等。

1.3 Authenticator

  • Authenticator即认证器,对用户身份进行认证

1.4 Authorizer

  • Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限

1.5 Realm

  • Realm即领域,相当于DataSource数据源
  • SecurityManager进行安全认证需要通过Realm获取用户权限数据
  • 在Realm中还有认证授权校验的相关的代码

1.6 SessionManager

  • SessionManager即会话管理,shiro框架定义了一套会话管理
  • 不依赖web容器的session,所以shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理
  • 可实现实现单点登录

1.7 SessionDAO

  • SessionDAO即会话Dao,是对Session会话操作的一套接口

1.8 CacheManager

  • CacheManager即缓存管理,将用户权限数据存储在缓存
  • 只是存在本地缓存,可以整合Redis存在远程缓存里

1.9 Cryptography

  • Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发。
  • 比如提供常用的散列、加/解密等功能。

2、常用的Jar包

http://shiro.apache.org/download.html
shiro-all 是shiro的所有功能jar包
shiro-core 是shiro的基本功能包
shiro-web 和web集成的包
shiro-spring shrio和spring集成的包

2.1 SpringBoot项目整合Shiro

Spring系列一品境之指玄境_第13张图片

2.1.1 概念梳理

  • Subject 当前操作用户
  • SecurityManager 典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例
  • Realm Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”;它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。重写两个方法,一个是授权,一个是认证
Subject:登陆的这个用户(用户、程序) 、谁认证那么这个主体就是谁
Principal:用户名(还可以是用户信息的封装)
Credential:密码
Token:令牌(用户名+密码的封装)----进行进行认证的封装对象
​ 这个的对象并不是前后分离的这个token

Security Manager:安全管理器(只要使用了shiro框架那么这个对象都是必不可少的)
Authenticator:认证器(主要做用户身份认证、简单跟你说就是用来登陆的时候做身份校验的)
Authrizer:授权器(简单的说就是用来做用户的授权的)
Realm:用户认证和授权的时候 和数据库交互的对象(这里面干的事情就是从数据库查询数据 封装成token然后取进行认证和授权)
  • 认证 主要是进行身份的认证(可以说局限在登入认证这一块)
  • 授权 认证成功后获取用户的权限 (给该用户分配对应的权限);访问资源时候,进行授权校验:用访问资源需要的权限去用户权限列表查找,如果存在,则有权限访问资源。(权限拦截
1)四种权限检验方式
  • 硬编码方式(拦截方法)(非Web应用,Web应用)
Subject subject = SecurityUtils.getSubject();
subject.checkPermission("部门管理");

  • 过滤器配置方式(拦截url)(Web应用)
/system/user/list.do = perms["用户管理"]

  • 注解方式(拦截方法)(Web应用)
@RequiresPermissions(“”)

  • shiro提供的标签((拦截页面元素:按钮,表格等))(Web应用)
<shiro:hasPermission name="用户管理">
    <a href="#">用户管理</a>
</shiro:hasPermission>


尝试用第四种方式

2)JWT

Json Web token(JWT)
是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。
它是客户端和服务端安全传递以及身份认证的一种解决方案,可以用在登录上。该token可以被加密,可以在上面添加一些业务信息供识别

组成主要有三个部分,头部,载荷和签证

  • 头部:声明类型和加密算法
  • 载荷:存放一些有效信息,比如一些业务相关的信息,例如用户信息
  • 签证:签证信息,说白了就是拿头部和载荷然后做加密操作而构成

Spring系列一品境之指玄境_第14张图片

  • 浏览器通过http请求发送用户名和密码到服务器

  • 服务器进行验证,验证通过后创建一个jwt token(携带用户信息)

  • 将该token返回给浏览器,由浏览器保存

  • 下次请求时,浏览器会带上当前token

  • 服务器对该token进行验签,通过后从token中获取用户信息

  • 根据当前获取的用户信息,做出响应,返回对应的数据


和Cookie的区别(开发中尽量尝试token

  • cookie数据需要客户端和服务器同时存储,是有状态的 ;这个token只需要存在客户端服务器在收到数据后,进行解析,token是无状态的
  • token相对cookie的优势
1、  支持跨域访问 ,将token置于请求头中,而cookie是不支持跨域访问的;

2、  无状态化, 服务端无需存储token ,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session;

3、  无需绑定到一个特殊的身份验证 方案(传统的用户名密码登陆),只需要生成的token是符合我们预期设定的即可;

4、  更适用于移动端 (Android,iOS,小程序等等),像这种原生平台不支持cookie,比如说微信小程序,每一次请求都是一次会话,当然我们可以每次去手动为他添加cookie,详情请查看博主另一篇博客;

5、  避免CSRF跨站伪造攻击 ,还是因为不依赖cookie;

6、  非常适用于RESTful API ,这样可以轻易与各种后端(java,.net,python…)相结合,去耦合

生成token

  • 导入依赖
            <dependency>
                <groupId>io.jsonwebtokengroupId>
                <artifactId>jjwtartifactId>
                <version>0.9.1version>
            dependency>
            <dependency>
                <groupId>javax.xml.bindgroupId>
                <artifactId>jaxb-apiartifactId>
                <version>2.3.0version>
            dependency>
            <dependency>
                <groupId>com.sun.xml.bindgroupId>
                <artifactId>jaxb-implartifactId>
                <version>2.3.0version>
            dependency>
            <dependency>
                <groupId>com.sun.xml.bindgroupId>
                <artifactId>jaxb-coreartifactId>
                <version>2.3.0version>
            dependency>
            <dependency>
                <groupId>javax.activationgroupId>
                <artifactId>activationartifactId>
                <version>1.1.1version>
            dependency>

  • JWTToken
import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;

/**
 * jwt token
 * @author zz
 **/
@Data
public class JWTToken implements AuthenticationToken {

    private static final long serialVersionUID = 1282057025599826155L;

    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

  • JWTFilter
import com.demo.ops.mgt.util.EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

/**
 * jwt过滤器,核心实现类
 * @author zz
 **/
@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {

    public static final String TOKEN = "X-Token";

    private static String whiteList;

    private static Set<String> whiteSet = new HashSet<>();

    private static List<String> prefixSet = new ArrayList<>();

    public synchronized void init() {
        whiteList = "/sys/login,/sys/logout,/v2/*";
        initWhiteSet(whiteList);
    }

    private static void initWhiteSet(String whiteList) {
        if (whiteList != null) {
            log.info("reset whiteList: {}", whiteList);
            Set<String> set = new HashSet<>();
            List<String> prefixs = new ArrayList<>();
            Arrays.stream(whiteList.split("\\s*,\\s*"))
                    .forEach((s) -> {
                        if (s.endsWith("*")) {
                            prefixs.add(s.substring(0, s.length() - 1));
                        } else {
                            set.add(s);
                        }
                    });
            prefixSet = prefixs;
            whiteSet = set;
        }
    }

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
        if (whiteList == null) {
            init();
        }

        HttpServletRequest httpServletRequest = (HttpServletRequest) request;

        String path = httpServletRequest.getServletPath();
        if (whiteSet.contains(path)) {
            return true;
        }
        for(String whitePrefix : prefixSet){
            if(path.startsWith(whitePrefix)){
                return true;
            }
        }

        if (isLoginAttempt(request, response)) {
            return executeLogin(request, response);
        }
        return false;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String token = req.getHeader(TOKEN);
        return token != null;
    }

    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader(TOKEN);
        JWTToken jwtToken = new JWTToken(decryptToken(token));
        try {
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (Exception e) {
            log.debug("登录检查异常!异常信息:{}", e.getMessage(), e);
            return false;
        }
    }

    /**
     * 对跨域提供支持
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    @Override
    protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
        log.debug("认证401!");
        HttpServletResponse httpResponse = WebUtils.toHttp(response);
        httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
        httpResponse.setCharacterEncoding("utf-8");
        httpResponse.setContentType("application/json; charset=utf-8");
        final String message = "请先登录";
        try (PrintWriter out = httpResponse.getWriter()) {
            String responseJson = "{\"msg\":\"" + message + "\",\"symbol\":false}";
            out.print(responseJson);
        } catch (IOException e) {
            log.error("登录检查输出信息异常!异常信息:", e);
        }
        return false;
    }

    /**
     * token 加密
     * @param token token
     * @return 加密后的 token
     */
    public static String encryptToken(String token) {
        try {
            EncryptUtil encryptUtil = new EncryptUtil(AnthenticationConstants.TOKEN_CACHE_PREFIX);
            return encryptUtil.encrypt(token);
        } catch (Exception e) {
            log.error("token加密异常!异常信息:", e);
            return null;
        }
    }

    /**
     * token 解密
     * @param encryptToken 加密后的 token
     * @return 解密后的 token
     */
    public static String decryptToken(String encryptToken) {
        try {
            EncryptUtil encryptUtil = new EncryptUtil(AnthenticationConstants.TOKEN_CACHE_PREFIX);
            return encryptUtil.decrypt(encryptToken);
        } catch (Exception e) {
            log.error("token解密异常!异常信息:", e);
            return null;
        }
    }
}

  • JWTUtil
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.boot.util.SpringContextUtil;
import com.demo.ops.mgt.entity.SysUser;
import com.demo.ops.mgt.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * jwt工具类
 * @author zz
 **/
@Slf4j
public class JWTUtil {

    private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;

    /**
     * 校验 token是否正确
     * @param token  密钥
     * @param secret 用户的密码
     * @return 是否正确
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            verifier.verify(token);
            return true;
        } catch (Exception e) {
            log.debug("token过期!过期信息:{}", e.getMessage());
            return false;
        }
    }

    /**
     * 从token中获取用户名
     * @return token中包含的用户名
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            log.debug("从token中获取用户名异常!异常信息:{}", e.getMessage());
            return null;
        }
    }

    /**
     * 生成token
     * @param username 用户名
     * @param secret   用户的密码
     * @return token
     */
    public static String sign(String username, String secret) {
        try {
            username = StringUtils.lowerCase(username);
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (Exception e) {
            log.error("生成token异常!异常信息:{}", e.getMessage());
            return null;
        }
    }

    /**
     * 获取当前系统用户
     * @param httpServletRequest
     * @return
     */
    public static SysUser getCurrentSysUser(HttpServletRequest httpServletRequest) {
        String token = httpServletRequest.getHeader(JWTFilter.TOKEN);
        if (StringUtils.isBlank(token)) {
            return null;
        }
        String decryptToken = JWTFilter.decryptToken(token);
        String userName = JWTUtil.getUsername(decryptToken);
        ISysUserService sysUserService = (ISysUserService) SpringContextUtil.getBean("sysUserService");
        return sysUserService.selectUserByUsername(userName);
    }
}


2.1.2 导入依赖

修改pom.xml,导入依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.0</version>
</dependency>

2.1.3 自定义Realm类

继承自AuthorizingRealm,并结合Service层,可以写多个Realm,分别对应不同功能

1)AuthenticationToken
  • 收集用户提交的身份信息(如用户名和凭据(如密码))的接口。
  • 扩展接口RememberMeAuthenticationToken:提供boolean isRememberMe()实现记住我功能。
  • 扩展接口HostAuthenticationToken:提供String getHost()获取用户主机。
  • 内置实现类UsernamePasswordToken:仅保存用户名、密码,并实现了以上两个接口,可以实现记住我主机验证的支持。
2)AuthenticationInfo
  • 封装验证通过的身份信息,主要包括Object属性principal(一般存储用户名)和credentials(密码)。
  • MergableAuthenticationInfo子接口:在多Realm时合并AuthenticationInfo,主要合并Principal,如果是其他信息如credentialsSalt,则会后合并进来的AuthenticationInfo覆盖。
  • SaltedAuthenticationInfo子接口:比如HashedCredentialsMatcher,在验证时会判断AuthenticationInfo是否是SaltedAuthenticationInfo的子类,是则获取其盐。
  • Account子接口:相当于我们之前的[users],SimpleAccount是其实现。在IniRealm、PropertiesRealm这种静态创建账号的场景中使用,它们继承了SimpleAccountRealm,其中就有API用于增删查改SimpleAccount。适用于账号不是特别多的情况。
  • SimpleAuthenticationInfo:一般都是返回这个类型。
3)PincipalCollection
  • Principal前缀:应该是上面AuthenticationInfo的属性principal。

  • PincipalCollection:是一个身份集合,保存登录成功用户身份信息。因为我们可以在Shiro中同时配置多个Realm,所以身份信息就有多个。可以传给doGetAuthorizationInfo()方法为登录成功的用户授权。

  • 示例

准备三个Realm,命名分别为a,b,c,身份凭证只有细微差别。
public class MyRealm1 implements Realm {
    @Override
    public String getName() {
        return "a"; //realm name 为 “a”
    }
 
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        return new SimpleAuthenticationInfo(
                "zhang", //身份 字符串类型
                "123",   //凭据
                getName() //Realm Name
        );
    }
}
 
//和1完全一样,只是命名为b
public class MyRealm2 implements Realm {
    @Override
    public String getName() {
        return "b"; //realm name 为 “b”
    }
 
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        return new SimpleAuthenticationInfo(
                "zhang", //身份 字符串类型
                "123",   //凭据
                getName() //Realm Name
        );
    }
}
 
//除了命名不同,只是Principal类型为User,而不是简单的String
public class MyRealm3 implements Realm {
    @Override
    public String getName() {
        return "c"; //realm name 为 “c”
    }
 
    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
            throws AuthenticationException {
        User user=new User("zhang","123");
        return new SimpleAuthenticationInfo(
                user, //身份 User类型
                "123",   //凭据
                getName() //Realm Name
        );
    }
}
public class PrincipalCollectionTest extends BaseTest {
    @Test
    public void testPrincipalCollection(){
        login("classpath:config/shiro-multirealm.ini",
                "zhang","123");
 
        Subject subject=subject();
 
        //获取Map中第一个Principal,即PrimaryPrincipal
        Object primaryPrincipal1=subject.getPrincipal();
        //获取PrincipalCollection
        PrincipalCollection principalCollection=subject.getPrincipals();
        //也是获取PrimaryPrincipal
        Object primaryPrincipal2=principalCollection.getPrimaryPrincipal();
 
        //获取所有身份验证成功的Realm名字
        Set<String> realmNames=principalCollection.getRealmNames();
        for(String realmName:realmNames)
            System.out.println(realmName);
 
        //将身份信息转换为Set/List(实际转换为List也是先转为Set)
        List<Object> principals=principalCollection.asList();
        /*返回集合包含两个String类、一个User类,但由于两个String类都是"zhang",
        所以只只剩下一个,转为List结果也是一样*/
        for(Object principal:principals)
            System.out.println("set:"+principal);
 
        //根据realm名字获取身份,因为realm名字可以重复,
        //所以可能有多个身份,建议尽量不要重复
        Collection<User> users=principalCollection.fromRealm("c");
        for(User user:users)
            System.out.println("c:user="+user.getUsername()+user.getPassword());
        Collection<String> usernames=principalCollection.fromRealm("b");
        for(String username:usernames)
            System.out.println("b:username="+username);
    }
}
4)AuthorizationInfo
  • 封装权限信息,主要是doGetAuthorizationInfo()时封装授权信息然后返回的。
  • SimpleAuthorizationInfo:实现类,大多数时候使用这个。主要增加了以下方法
authorizationInfo.addRole("role1"); //添加角色到内部维护的role集合;
    添加角色后调用MyRolePermissionResolver解析出权限
authorizationInfo.setRoles(Set<String> roles); //将内部维护的role集合设置为入参
 
authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); //添加对象型权限
authorizationInfo.addObjectPermission(new WildcardPermission("user1:*"));
authorizationInfo.addStringPermission("+user2+10"); //字符串型权限
authorizationInfo.addStringPermission("user2:*");
authorizationInfo.setStringPermissions(Set<String> permissions);
5)Subject
  • Shiro核心对象,基本所有身份验证、授权都是通过Subject完成的。
//获取身份信息
Object getPrincipal(); //Primary Principal
PrincipalCollection getPrincipals(); // PrincipalCollection
 
//身份验证
void login(AuthenticationToken token) throws AuthenticationException; //调用各种方法;
    登录失败抛AuthenticationException,成功则调用isAuthenticated()返回true
boolean isAuthenticated(); //与isRemembered()一个为true一个为false
boolean isRemembered(); //返回true表示是通过记住我登录到额而不是调用login方法
 
//角色验证
boolean hasRole(String roleIdentifier); //返回true或false表示成功与否
boolean[] hasRoles(List<String> roleIdentifiers);
boolean hasAllRoles(Collection<String> roleIdentifiers);
void checkRole(String roleIdentifier) throws AuthorizationException; //失败抛异常
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(String... roleIdentifiers) throws AuthorizationException;
 
//权限验证
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
 
//会话(登录成功相当于建立了会话,然后调用getSession获取
Session getSession(); //相当于getSession(true)
Session getSession(boolean create); //当create=false,如果没有会话将返回null,
    当create=true,没有也会强制创建一个
 
//退出
void logout();
 
//RunAs
void runAs(PrincipalCollection principals) 
    throws NullPointerException, IllegalStateException; //实现允许A作为B进行访问,
    调用runAs(b)即可
boolean isRunAs(); //此时此方法返回true
PrincipalCollection getPreviousPrincipals(); //得到a的身份信息,getPrincipals()得到b的身份信息
PrincipalCollection releaseRunAs(); //不需要了RunAs则调用这个
 
//多线程
<V> V execute(Callable<V> callable) throws ExecutionException;
void execute(Runnable runnable);
<V> Callable<V> associateWith(Callable<V> callable);
Runnable associateWith(Runnable runnable);
  • Subject的获取 一般不需要我们创建,直接通过SecurityUtils获取即可
public static Subject getSubject() {
    Subject subject = ThreadContext.getSubject();
    if (subject == null) {
        subject = (new Subject.Builder()).buildSubject();
        ThreadContext.bind(subject);
    }
    return subject;
}
  • 首先查看当前线程是否绑定了Subject,没有则通过Subject.BUilder构建一个并绑定到线程返回。如果想自定义Subject实例的创建,代码如下
new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
  • 一般用法
1、身份验证login()
2、授权hasRole*()/isPermitted*/checkRole*()/checkPermission*()
3、将相应的数据存储到会话Session
4、切换身份RunAs/多线程身份传播
5、退出

import com.cxh.mall.entity.SysUser;
import com.cxh.mall.service.SysMenuService;
import com.cxh.mall.service.SysRoleService;
import com.cxh.mall.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.StringUtils;
 
import java.util.HashSet;
import java.util.Set;
 
 
public class LoginRealm extends AuthorizingRealm {
 
    @Autowired
    @Lazy
    private SysUserService sysUserService;
 
    @Autowired
    @Lazy
    private SysRoleService sysRoleService;
 
    @Autowired
    @Lazy
    private SysMenuService sysMenuService;
 
 
    /**
     * 授权
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
        String username = (String) arg0.getPrimaryPrincipal();
        SysUser sysUser = sysUserService.getUserByName(username);
        // 角色列表
        Set<String> roles = new HashSet<String>();
        // 功能列表
        Set<String> menus = new HashSet<String>();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        roles = sysRoleService.listByUser(sysUser.getId());
        menus = sysMenuService.listByUser(sysUser.getId());
        // 角色加入AuthorizationInfo认证对象
        info.setRoles(roles);
        // 权限加入AuthorizationInfo认证对象
        info.setStringPermissions(menus);
        return info;
    }
 
    /**
     * 登录认证
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
            return null;
        }
        //获取用户信息
        String username = authenticationToken.getPrincipal().toString();
        if (username == null || username.length() == 0)
        {
            return null;
        }
        //获取用户信息
        SysUser user = sysUserService.getUserByName(username);
        if (user == null)
        {
            throw new UnknownAccountException(); //未知账号
        }
        //判断账号是否被锁定,状态(0:禁用;1:锁定;2:启用)
        if(user.getStatus() == 0)
        {
            throw new DisabledAccountException(); //帐号禁用
        }
 
        if (user.getStatus() == 1)
        {
            throw new LockedAccountException(); //帐号锁定
        }
        //盐
        String salt = "123456";
        //验证
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                username, //用户名
                user.getPassword(), //密码
                ByteSource.Util.bytes(salt), //盐
                getName() //realm name
        );
        return authenticationInfo;
 
    }
 
    public static void main(String[] args) {
        String originalPassword = "123456"; //原始密码
        String hashAlgorithmName = "MD5"; //加密方式
        int hashIterations = 2; //加密的次数
        //盐
        String salt = "123456";
        //加密
        SimpleHash simpleHash = new SimpleHash(hashAlgorithmName, originalPassword, salt, hashIterations);
        String encryptionPassword = simpleHash.toString();
        //输出加密密码
        System.out.println(encryptionPassword);
    }
 
 
 
}

2.1.4 编写Shiro配置类

使用@Configuration注解注入

import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
import java.util.HashMap;
import java.util.Map;
 
@Configuration
public class ShiroConfig {
 
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
        defaultAAP.setProxyTargetClass(true);
        return defaultAAP;
    }
 
    //凭证匹配器, 密码校验交给Shiro的SimpleAuthenticationInfo进行处理
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;
        hashedCredentialsMatcher.setHashIterations(2);//散列的次数;
        return hashedCredentialsMatcher;
    }
 
 
    //将自己的验证方式加入容器
    @Bean
    public LoginRealm myShiroRealm() {
        LoginRealm loginRealm = new LoginRealm();
        //加入密码管理
        loginRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return loginRealm;
    }
 
    //权限管理,配置主要是Realm的管理认证
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myShiroRealm());
        return securityManager;
    }
 
    //Filter工厂,设置对应的过滤条件和跳转条件
    // 添加shiro的内置过滤器
        /**
         * anon:无需认证就可以访问
         * authc:必须认证了才能访问
         * user:必须拥有记住我功能才能用
         * perms:拥有对某个资源的权限才能访问
         * role:拥有某个角色的权限才能访问
         */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> map = new HashMap<>();
        //登出
        map.put("/logout", "logout");
        //登录
        map.put("/loginSubmit", "anon");
        //静态文件包
        map.put("/res/**", "anon");
        //对所有用户认证
        map.put("/**", "authc");
        //登录
        shiroFilterFactoryBean.setLoginUrl("/login");
        //首页
        shiroFilterFactoryBean.setSuccessUrl("/index");
        //错误页面,认证不通过跳转
        shiroFilterFactoryBean.setUnauthorizedUrl("/error");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
 
 
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

2.1.5 Controller登录逻辑

   import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
 
 
@Controller
@Slf4j
public class LoginController {
 
    /**
     * 登录页面
     */
    @GetMapping(value={"/", "/login"})
    public String login(){
        return "admin/loginPage";
    }
 
    /**
     * 登录操作
     */
    @RequestMapping("/loginSubmit")
    public String login(String username, String password, ModelMap modelMap)
    {
        //参数验证
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
        {
            modelMap.addAttribute("message", "账号密码必填!");
            return "admin/loginPage";
        }
        //账号密码令牌
        AuthenticationToken token = new UsernamePasswordToken(username, password);
        //获得当前用户到登录对象,现在状态为未认证
        Subject subject = SecurityUtils.getSubject();
        try
        {
            //将令牌传到shiro提供的login方法验证,需要自定义realm
            subject.login(token);
            //没有异常表示验证成功,进入首页
            return "admin/homePage";
        }
        catch (IncorrectCredentialsException ice)
        {
            modelMap.addAttribute("message", "用户名或密码不正确!");
        }
        catch (UnknownAccountException uae)
        {
            modelMap.addAttribute("message", "未知账户!");
        }
        catch (LockedAccountException lae)
        {
            modelMap.addAttribute("message", "账户被锁定!");
        }
        catch (DisabledAccountException dae)
        {
            modelMap.addAttribute("message", "账户被禁用!");
        }
        catch (ExcessiveAttemptsException eae)
        {
            modelMap.addAttribute("message", "用户名或密码错误次数太多!");
        }
        catch (AuthenticationException ae)
        {
            modelMap.addAttribute("message", "验证未通过!");
        }
        catch (Exception e)
        {
            modelMap.addAttribute("message", "验证未通过!");
        }
        //返回登录页
        return "admin/loginPage";
    }
 
    /**
     * 登出操作
     */
    @RequestMapping("/logout")
    public String logout()
    {
        //登出清除缓存
        Subject subject = SecurityUtils.getSubject();
        subject.logout();
        return "redirect:/login";
    }
 
}
 

-------
前端请求
<div id="div_main">
        <div id="div_head"><p>cxh电商平台管理后台</p></div>
        <div id="div_content">
            <form id="form_login" name="loginForm" method="post" action="/cxh/loginSubmit" onsubmit="return SubmitLogin()" autocomplete="off">
                <input type="text" class="form-control form_control" name="username" placeholder="用户名" id="input_username" title="请输入用户名"/>
                <input type="password" class="form-control form_control" name="password" placeholder="密码" id="input_password" title="请输入密码" autocomplete="on">
                <span id="error_msg" style="color: red;">${message}</span>
                <input type="submit" class="btn btn-danger" id="btn_login" value="登录"/>
            </form>
        </div>
    </div>

//提交登录
function SubmitLogin() {
    //判断用户名是否为空
    if (!loginForm.username.value) {
        alert("请输入用户姓名!");
        loginForm.username.focus();
        return false;
    }
 
    //判断密码是否为空
    if (!loginForm.password.value) {
        alert("请输入登录密码!");
        loginForm.password.focus();
        return false;
    }
    return true;
}

2.1.6 Shiro加密与解密

1)密码比对

通过AuthenticatingRealmcredentialsMatcher属性来进行密码的比对!

  • 获取当前的Subject,调用SecurityUtils.getSubject();
  • 测试当前的用户是否已经被认证,即是否已经登录,调用Subject的isAuthenticated();
  • 若没有被认证,则把用户名和密码封装为UsernamePasswirdToken对象(1)创建一个表单页面(2)把请求提交到Controller(3)获取用户名和密码
  • 执行登录:调用Subject的login(AuthenticationToken)方法。
  • 自定义Realm的方法,从数据库中获取对应的记录,返回给Shiro。(1)实际上需要继承AuthenticatingRealm类。(2)实现doGetAuthenticationInfo(AuthenticationToken)方法;
  • 由Shiro完成对密码的比对。
2)MD5盐值加密

盐值加密主要为了防止相同密码出现相同密文的情况,通过随机盐产生不同的密文放入数据库
ByteSource:通过这个类的Util.bytes(“”)方法产生不同的盐值。

  • 在doGetAuthenticationInfo方法返回值创建SimpleAutenticationInfo对象的时候,需要使用SimpleAuthenticationInfo(pirncipla,credentials,credentialsSalt,realmName)构造器;

  • 使用ByteSource.Util.bytes()来产生盐值;

  • 盐值需要唯一:一般使用随机字符串或者user对于的id进行生成;

  • 使用new SimpleHash(hashAlgorithmName,credentials,salt,hashIterations);来计算盐值加密后的密码的值。//hashAlgorithmName加密方式,这边选用MD5,crdentials密码原值, salt盐值,hashIterations加密次数

  • 项目开发可以使用JWT基于Json的开发标准

2.1.7 补充

1)Realm判断逻辑
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行认证逻辑!");
    //模拟数据库中的用户名和密码
    String username = "aaa";
    String password = "123456";
    //编写Shiro的判断逻辑,判断用户名和密码
    UsernamePasswordToken token1 = (UsernamePasswordToken) token;
    //判断用户名
    if(!token1.getUsername().equals(username)){
        //用户名不存在!
        return null; //Shiro底层会抛出UnKnowAccountException
    }
    //判断密码
    return new SimpleAuthenticationInfo("",password,"");
}



2)Realm中注入Service
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    System.out.println("执行认证逻辑!");
    //模拟数据库中的用户名和密码
    String username = "aaa";
    String password = "123456";
    //编写Shiro的判断逻辑,判断用户名和密码
    UsernamePasswordToken token1 = (UsernamePasswordToken) token;
    //判断用户名
    if(!token1.getUsername().equals(username)){
        //用户名不存在!
        return null; //Shiro底层会抛出UnKnowAccountException
    }
    //判断密码
    return new SimpleAuthenticationInfo("",password,"");
}

3)使用Shiro内置过滤器拦截资源
1).在shiroConfig中对接口添加需要授权
    /**
     *  为add接口添加授权过滤器
     *  注意: 当授权拦截后,shiro会自动跳转到未授权页面
     */
    map.put("/add","perms[user:add]");  
2). 设置未授权提示页面
    //设置未授权提示页面
    shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth"); //跳转到的controller接口  
3). 编写跳转接口以及接口中定义跳转的页面
    @RequestMapping("unAuth")
    public String unAuth(){
        return "user/unAuth";
    }  

4)动态授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    System.out.println("执行授权逻辑!");
    //给资源进行授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    //添加授权字符串,就是在shiroConfig中授权时定义的字符串
    //到数据库中查询当前登录用户的授权字符串
    //获取当前用户
    Subject subject = SecurityUtils.getSubject();
    //要想获取到当前用户,需要在下面的认证逻辑完成传过来

    User user = (User) subject.getPrincipal();
      User dbUser = userService.selectUserById(user.getId());

      //然后添加授权字符串
      info.addStringPermission(dbUser.getPerms());
  //  info.addStringPermission("user:add");
 //   info.addStringPermissions();  添加一个集合
      return info;
  }

2.2 Shiro+JWT+Redis实例

Spring系列一品境之指玄境_第15张图片
Spring系列一品境之指玄境_第16张图片

三、Redis

1、Redis简介

Redis是现在最受欢迎的NoSQL数据库之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存、可选持久性的键值对存储数据库。

  • 编写语言
    Redis 是采用C语言编写的,好处就是底层代码执行效率高依赖性低,没有太多运行时的依赖,而且系统的兼容性好稳定性高
  • 存储
    Redis是基于内存的数据里,可避免磁盘IO,因此也被称作缓存工具
  • 数据结构
    Redis采用key-value的方式进行存储,也就是使用hash结构进行操作,数据的操作时间复杂度是O(1)
  • 设计模型
    Redis采用的是单进程单线程的模型,可以避免上下文切换和线程之间引起的资源竞争。而且Redis还采用了IO多路复用技术,这里的多路复用是指多个socket网络连接,复用是指一个线程中处理多个IO请求,这样可以减少网络IO的消耗,大幅度提升效率

应用场景浓缩为 高性能、高并发

2、Redis的数据结构

Redis提供的数据类型主要分为5种自有类型和一种自定义类型,这5种自有类型包括:String类型、哈希类型、列表类型、集合类型和顺序集合类型。

2.1 String类型

  • String数据结构是简单的key-value类型,value其实不仅是String,也可以是数字。
  • 常规操作 set,get,decr,incr,mget等。
  • 补充操作
• 获取字符串长度
• 设置和获取字符串的某一段内容
• 设置及获取字符串的某一位(bit)
• 设置及获取字符串的某一位
• 批量设置一系列字符串的内容

2.2 哈希类型

  • 该类型是由field和关联的value组成的map。其中,field和value都是字符串类型的。
  • 常用命令:hget,hset,hgetall等。
  • Redis的Hash结构可以使你像在数据库中Update一个属性一样只修改某一项属性值。

Spring系列一品境之指玄境_第17张图片

2.3 列表类型

  • 该类型是一个插入顺序排序的字符串元素集合, 基于双链表实现。
  • 常用命令:lpush,rpush,lpop,rpop,lrange等。
  • 使用Lists结构,我们可以轻松的实现最新消息排行等功能。Lists的另一个应用就是消息队列。可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。Redis还提供了操作Lists中某一段的api,你可以直接查询,删除Lists中某一段的元素。

2.4 集合类型

  • Set类型是一种无顺序集合, 它和List类型最大的区别是:集合中的元素没有顺序, 且元素是唯一的。
  • 常用命令:sadd,spop,smembers,sunion 等。
  • 应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。是不会自动有序的

2.5 顺序集合类型

  • ZSet是一种有序集合类型,每个元素都会关联一个double类型的分数权值,通过这个权值来为集合中的成员进行从小到大的排序。与Set类型一样,其底层也是通过哈希表实现的。
  • 常用命令:zadd,zpop, zmove, zrange,zrem,zcard,zcount等。
  • 使用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。

3、SpringBoot整合Redis

3.1 导入依赖

Redis缓存是公共应用,可以把依赖与配置添加到了common模块下面


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>


<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-pool2artifactId>
    <version>2.6.0version>
dependency>

3.2 接口中添加Redis缓存

3.2.1 添加Redis配置

在application.properties(或.yml或.yaml,其后缀表示同一种文件类型即.yaml类型)文件中添加

spring.redis.host=192.168.44.132
spring.redis.port=6379
spring.redis.database= 0
spring.redis.timeout=1800000

spring.redis.lettuce.pool.max-active=20
spring.redis.lettuce.pool.max-wait=-1
#最大阻塞等待时间(负数表示没限制)
spring.redis.lettuce.pool.max-idle=5
spring.redis.lettuce.pool.min-idle=0

 	spring:
      #redis 配置
      redis:
        host: 127.0.0.1
        port: 6379
        password:
        #连接超时时间(毫秒)
        timeout: 36000ms
        # Redis默认情况下有16个分片,默认0
        database: 0
        lettuce:
          pool:
            # 连接池最大连接数(使用负值表示没有限制) 默认 8
            max-active: 8
            # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
            max-wait: -1ms
            # 连接池中的最大空闲连接 默认 8
            max-idle: 8
            # 连接池中的最小空闲连接 默认 0
            min-idle: 0


3.2.2 启动Redis服务

Redis的安装与使用

3.3 启动类配置

@SpringBootApplication
@MapperScan(basePackages = "com.arbor.mall.model.dao")
@EnableCaching	// 加上此注解
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}

3.4 Redis配置类

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setConnectionFactory(factory);
        //key序列化方式
        template.setKeySerializer(redisSerializer);
        //value序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //value hashmap序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        return template;
    }

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        RedisSerializer<String> redisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        //解决查询缓存转换异常的问题
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // 配置序列化(解决乱码的问题),过期时间600秒
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(600))
              .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                .disableCachingNullValues();
        RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
                .cacheDefaults(config)
                .build();
        return cacheManager;
    }
}

3.5 Redis工具类

@Component
public class RedisUtils {

	@Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void RedisUtils(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间
     *
     * @param key 键 不能为null
     * @return 时间(秒) 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除缓存
     *
     * @param key 可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public boolean del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                return redisTemplate.delete(key[0]);
            }
            return redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)) > 0 ? true : false;
        }
        return false;
    }

    /**
     * 匹配所有的key
     *
     * @param pettern
     * @return
     */
    public Set<String> keys(String pettern) {
        if (pettern.trim() != "" && pettern != null) {
            return redisTemplate.keys(pettern);
        }
        return null;
    }
// ============================String=============================

    /**
     * 普通缓存获取
     *
     * @param key 键
     * @return 值
     */
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     *
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */
    public boolean set(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key      键
     * @param value    值
     * @param time     时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @param timeUnit 过期时间单位
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, timeUnit);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     *
     * @param key   键
     * @param delta 要增加几(大于0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 递减
     *
     * @param key   键
     * @param delta 要减少几(小于0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }
}

3.6 业务使用

public Result login(UserDto userDto) {
        //判断空
        String uname = userDto.getUname();
        String upassword = userDto.getUpassword();
        if(StringUtils.isBlank(uname) || StringUtils.isBlank(upassword)){
            return Result.error("300","参数错误");
        }
        //获取用户信息
        User one = getControllerInfo(userDto);
        if(!Objects.equals(one,null)){
            BeanUtil.copyProperties(one,userDto,true);
            String token = TokenUtils.genToken(one.getUid().toString(), one.getUpassword());
            //存入到redis中  直接存token值到Redis中
          //redisUtils.set(GlobalConstant.REDIS_KEY_TOKEN+one.getUid(),token);
            //设置过期时间对应时间单位     
            redisUtils.set(GlobalConstant.REDIS_KEY_TOKEN+one.getUid(),token,GlobalConstant.REDIS_KEY_TOKEN_TIME, TimeUnit.HOURS);
            userDto.setToken(token);
            //设置动态菜单
            String role = one.getRole();
            List<Menu> menuList=getMenuListByRole(role);

            userDto.setMenuList(menuList);
            return Result.success(userDto);
        }
        return Result.error("300","用户名或者密码错误");

    }

------------
@Service
public class CategoryServiceImpl implements CategoryService {
	@Override
	// 方法加上此注解,value是在Redis存储时key的值
    @Cacheable(value = "listCategoryForCustomer")
    public List<CategoryVO> listCategoryForCustomer() {
        ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
        recursivelyFindCategories(categoryVOList, 0);
        return categoryVOList;
    }
}


3.7 补充

修改密码,删除用户,退出登录进行删除对应的token
//删除Redis中token值
redisUtils.del(GlobalConstant.REDIS_KEY_TOKEN+cid);

3.8 常用注解

3.8.1 @Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。

属性值如下

属性/方法 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

3.8.2 @CachePut

使用该注解标志的方法每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。

属性值如下

属性/方法 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key

3.8.3 @CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上

属性值如下

属性/方法 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
allEntries 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存
beforeInvocation 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存

3.9 存在问题

  • 关闭防火墙
  • 找到redis配置文件, 注释一行配置 注释掉:#bind 127.0.0.1

4、技术升级

4.1 数据持久化

由于Redis的强大性能很大程度上是因为所有数据都是存储在内存中,然而当出现服务器宕机、redis重启等特殊场景,所有存储在内存中的数据将会丢失,这是无法容忍的事情,所以必须将内存数据持久化。例如:将redis作为数据库使用的;将redis作为缓存服务器使用等场景。
目前持久化存在两种方式:RDB方式和AOF方式。

4.1.1 RDB方式

RDB持久化是把当前进程数据生成快照保存到硬盘的过程, 触发RDB持久化过程分为手动触发自动触发

一般存在以下情况会对数据进行快照。

  • 根据配置规则进行自动快照;
  • 用户执行SAVE, BGSAVE命令;
  • 执行FLUSHALL命令;
  • 执行复制(replication)时。

优缺点:恢复数据较AOF更快;

4.1.2 AOF方式

独立日志的方式记录每次写命令(写入的内容直接是文本协议格式 ),重启时重新执行AOF文件中的命令达到恢复数据的目的。

  • AOF的工作流程操作: 命令写入(append) 、 文件同步(sync) 、 文件重写(rewrite) 、 重启加载(load)
  • 优点:实时性较好

4.2 雪崩

4.2.1 雪崩定义

缓存雪崩是指Redis缓存层由于某种原因宕机后(有一种情况就是,缓存中大批量数据到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机),所有的请求会涌向存储层,短时间内的高并发请求可能会导致存储层挂机,称之为“Redis雪崩”。

4.2.2 规避方案

  • 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  • 使用Redis集群,如果缓存数据库是分布式部署,将热点数据均匀分布不同的缓存数据库中。
  • 设置热点数据永远不过期
  • 限流
  • 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
  • 事后:redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

4.3 击穿

4.3.1 击穿定义

缓存击穿是指,在Redis获取某一key时, 由于key不存在在缓存中但数据库中有, 而必须向DB发起一次请求的行为, 这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力,称为“Redis击穿”。

4.3.2 发生原因

  • 第一次访问
  • 恶意访问不存在的Key
  • Key过期

4.3.3 规避方案

  • 服务器启动时, 提前写入对应的key
  • 规范key的命名, 通过中间件拦截
  • 对某些高频访问的Key,设置合理的TTL或永不过期
  • 加互斥锁

互斥锁案例

  • 常量类
package com.wl.standard.common.result.constants;

/**
 * redis常量
 * @author wl
 * @date 2022/3/17 16:09
 */
public interface RedisConstants {
    /**
     * 空值缓存过期时间(分钟)
     */
    Long CACHE_NULL_TTL = 2L;

    /**
     * 城市redis缓存key
     */
    String CACHE_CITY_KEY = "cache:city:";
    /**
     * 城市redis缓存过期时间(分钟)
     */
    Long CACHE_CITY_TTL = 30L;

    /**
     * 城市redis互斥锁key
     */
    String LOCK_CITY_KEY = "lock:city:";
}

  • Service实现层
package com.wl.standard.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.standard.common.result.constants.RedisConstants;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import com.wl.standard.mapper.CityMapper;
import com.wl.standard.entity.City;
import com.wl.standard.service.CityService;

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.TimeUnit;

/**
 * @author wl
 * @date 2021/11/18
 */
@Service
@Slf4j
public class CityServiceImpl extends ServiceImpl<CityMapper, City> implements CityService{

    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    public CityServiceImpl(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public City getByCode(String cityCode) {
        String key = RedisConstants.CACHE_CITY_KEY+cityCode;
        return queryCityWithMutex(key, cityCode);
    }

    /**
     * 通过互斥锁机制查询城市信息
     * @param key
     */
    private City queryCityWithMutex(String key, String cityCode) {
        City city = null;
        // 1.查询缓存
        String cityJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断缓存是否有数据
        if (StringUtils.isNotBlank(cityJson)) {
            // 3.有,则返回
            city = JSONObject.parseObject(cityJson, City.class);
            return city;
        }
        // 4.无,则获取互斥锁
        String lockKey = RedisConstants.LOCK_CITY_KEY + cityCode;
        Boolean isLock = tryLock(lockKey);
        // 5.判断获取锁是否成功
        try {
            if (!isLock) {
                // 6.获取失败, 休眠并重试
                Thread.sleep(100);
                return queryCityWithMutex(key, cityCode);
            }
            // 7.获取成功, 查询数据库
            city = baseMapper.getByCode(cityCode);
            // 8.判断数据库是否有数据
            if (city == null) {
                // 9.无,则将空数据写入redis
                stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            // 10.有,则将数据写入redis
            stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(city), RedisConstants.CACHE_CITY_TTL, TimeUnit.MINUTES);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 11.释放锁
            unLock(lockKey);
        }
        // 12.返回数据
        return city;
    }

    /**
     * 获取互斥锁
     * @return
     */
    private Boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtils.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    private void unLock(String key) {
        stringRedisTemplate.delete(key);
    }
}

四、Ngnix

1、负载均衡定义

当一台服务器的性能达到极限时,我们可以使用服务器集群来提高网站的整体性能。
那么,在服务器集群中,需要有一台服务器充当调度者的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况将请求分配给某一台后端服务器去处理。

2、反向代理负载均衡

反向代理服务器是一个位于实际服务器之前的服务器,所有向我们网站发来的请求都首先要经过反向代理服务器。
服务器根据用户的请求要么直接将结果返回给用户,要么将请求交给后端服务器处理,再返回给用户

3、Ngnix

俄罗斯人开发的一个高性能的 HTTP和反向代理服务器。
由于Nginx 超越 Apache 的高性能和稳定性,使得国内使用 Nginx 作为 Web 服务器的网站也越来越多,其中包括新浪博客、新浪播客、网易新闻、腾讯网、搜狐博客等门户网站频道等,在3w以上的高并发环境下,ngnix处理能力相当于apache的10倍。

4、SpringBoot集成Ngnix

Nginx代理服务器搭配多台Tomcat服务器即多个SpringBoot容器,利用负载均衡策略实现tomcat集群的部署。
SpringBoot容器可以相同,也可以不同
Ngnix通过配置 upstream 节点分发请求,达到负载均衡的效果
Spring系列一品境之指玄境_第18张图片
Spring系列一品境之指玄境_第19张图片

4.1 Ngnix下载

官方网址

4.1.1 容器方式

启动Docker服务 systemctl start docker.service
拉取 nginx 最新镜像 docker pull nginx
运行ngnix docker run -d --name mynginx01 -p 80:80 nginx

4.2 启动Ngnix

4.2.1 命令

启动 start nginx
关闭 nginx -s stop
重启 nginx -s reload;(先启动才能重启)

4.2.2 注意点

  • 解压/安装目录不要放在c盘,不要有中文路径
  • Nginx启动会占用80端口,注意端口冲突
  • Nginx只能启动一次,如果多次启动,会破坏第一次正常启动的Nginx,任务管理器中查看Nginx启用情况
  • 第一次使用右键->超级管理员身份运行,目的获取权限
  • 每次启动Nginx都会启动两个线程,守护线程:防止主进程意外关闭(占用内存比较小);主线程:nginx主要服务项(占用内存比较大)。所以如果手动关闭,需要先关守护线程再关主线程。

4.3 配置反向代理

以linux为例,启动多个相同的容器或者Jar包

容器方式
docker修改容器内ngnix配置文件
简单来说,cp复制一个conf文件更简单
docker cp /etc/nginx/conf.d/***.conf 96f7f14e99ab:/nginx/conf.d/***.conf
docker stop mynginx
docker start mynginx

4.3.1 找到ngnix.conf文件

locate nginx.conf

4.3.2 编辑该配置文件

#user  nobody;
worker_processes  1;
 
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;
 
#pid        logs/nginx.pid;
 
 
events {
    worker_connections  1024;
}
 
 
http {
    include       mime.types;
    default_type  application/octet-stream;
 
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';
 
    #access_log  logs/access.log  main;
 
    sendfile        on;
    #tcp_nopush     on;
 
    #keepalive_timeout  0;
    keepalive_timeout  65;
 
    #gzip  on;
 
    upstream dispense {
        server springboot-8090:8090 weight=1;
        server springboot-8091:8091 weight=2;
    }
 
    server {
        listen       8080;
        server_name  localhost;
 
        #charset koi8-r;
 
        #access_log  logs/host.access.log  main;
 
        location / {
            proxy_pass   http://dispense;
            index  index.html index.htm;
        }
 
        #error_page  404              /404.html;
 
        # redirect server error pages to the static page /50x.html
        #
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
 
        # proxy the PHP scripts to Apache listening on 127.0.0.1:80
        #
        #location ~ \.php$ {
        #    proxy_pass   http://127.0.0.1;
        #}
 
        # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
        #
        #location ~ \.php$ {
        #    root           html;
        #    fastcgi_pass   127.0.0.1:9000;
        #    fastcgi_index  index.php;
        #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
        #    include        fastcgi_params;
        #}
 
        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #    deny  all;
        #}
    }
 
 
    # another virtual host using mix of IP-, name-, and port-based configuration
    #
    #server {
    #    listen       8000;
    #    listen       somename:8080;
    #    server_name  somename  alias  another.alias;
 
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
 
 
    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;
 
    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;
 
    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;
 
    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;
 
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
 
}

通过配置 upstream 节点分发请求,达到负载均衡的效果
注意 springboot-8090 和 springboot-8091 都是等会启动的 SpringBoot 容器的名称
使用 weight 设置权重
location 节点中配置代理 proxy_pass 为 http://dispense; 即为上面配置upstream的名称
注意我把默认端口 80 更改为 8080 了,因为我的服务器上 80 端口有别的应用在使用

五、Docker

1、Docker定义

  • Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。
  • Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级可移植容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化
  • 容器是完全使用沙箱机制相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低

2、Docker教程

apt install docker.io    #安装docker
docker -v  #查看版本  

Docker基本命令

docker镜像: ----类似java中 class

docker容器 : ----类似java中 class new 出来的实例对象

3、SpringBoot打包成Docker容器

将 SpringBoot 项目打包成 Docker 镜像,其主要通过 Maven plugin 插件来进行构建
现在已经升级出现了新的插件
dockerfile-maven-plugin

3.1 plugin

<plugin>
  <groupId>com.spotifygroupId>
  <artifactId>dockerfile-maven-pluginartifactId>
  <version>1.4.13version>
  <executions>
    <execution>
      <id>defaultid>
      <goals>
        <goal>buildgoal>
        <goal>pushgoal>
      goals>
    execution>
  executions>
  <configuration>
    <repository>${docker.image.prefix}/${project.artifactId}repository>
    <tag>${project.version}tag>
    <buildArgs>
      <JAR_FILE>${project.build.finalName}.jarJAR_FILE>
    buildArgs>
  configuration>
plugin>

  • repository:指定Docker镜像的repo名字,要展示在docker images 中的。
  • tag:指定Docker镜像的tag,不指定tag默认为latest
  • buildArgs:指定一个或多个变量,传递给Dockerfile,在Dockerfile中通过ARG指令进行引用。JAR_FILE 指定 jar 文件名。

另外,可以在execution中同时指定build和push目标。当运行mvn package时,会自动执行build目标,构建Docker镜像。

3.2 DockerFile

DockerFile 文件需要放置在项目 pom.xml同级目录下
内容如下

FROM java:8
EXPOSE 8080
ARG JAR_FILE
ADD target/${JAR_FILE} /niceyoo.jar
ENTRYPOINT ["java", "-jar","/niceyoo.jar"]

  • FROM:基于java:8镜像构建
  • EXPOSE:监听8080端口
  • ARG:引用plugin中配置的 JAR_FILE 文件
  • ADD:将当前 target 目录下的 jar 放置在根目录下,命名为 niceyoo.jar,推荐使用绝对路径。
  • ENTRYPOINT:执行命令 java -jar /niceyoo.jar

3.3 docker-maven-plugin 远程仓库

SpringBoot项目构建 docker 镜像并推送到远程仓库

<plugin>
    <groupId>com.spotifygroupId>
    <artifactId>docker-maven-pluginartifactId>
    <version>1.0.0version>
    <configuration>
        
        <imageName>10.211.55.4:5000/${project.artifactId}imageName>
        
        
        
        <imageTags>
            <imageTag>latestimageTag>
        imageTags>
        
        <registryUrl>10.211.55.4:5000registryUrl>
        <pushImage>truepushImage>
        
        <baseImage>javabaseImage>
        
        <maintainer>niceyoo [email protected]maintainer>
        
        <workdir>/ROOTworkdir>
        <cmd>["java","-version"]cmd>
        <entryPoint>["java","-jar","${project.build.finalName}.jar"]entryPoint>
        
        <dockerHost>http://10.211.55.4:2375dockerHost>
        
        <resources>
            <resource>
                <targetPath>/ROOTtargetPath>
                
                <directory>${project.build.directory}directory>
                
                <include>${project.build.finalName}.jarinclude>
            resource>
        resources>
    configuration>
plugin>

执行 mvn package docker:build,即可完成打包至 docker 镜像中。【使用docker-maven-plugin

使用dockerfile-maven-plugin
Dockerfile 就不一样了,从我们开始编写 Dockerfile 文件 FROM 命令开始,我们就发现,这个必须依赖于Docker,但问题就是,假设我本地跟 Docker 并不在一台机器上,那么我是没法执行 dockerfile 的,如果在本地不安装 docker 环境下,是没法执行打包操作的,那么就可以将代码拉取到 Docker 所在服务器,执行打包操作。

3.4 项目打包

mvn clean package dockerfile:build -Dmaven.test.skip=true

执行 docker images 查看

3.5 创建容器并运行

docker run -d -p 8080:8080 10.211.55.4:5000/springboot-demo:0.0.1-SNAPSHOT
-d:表示在后台运行

-p:指定端口号,第一个8080为容器内部的端口号,第二个8080为外界访问的端口号,将容器内的8080端口号映射到外部的8080端口号

10.211.55.4:5000/springboot-demo:0.0.1-SNAPSHOT:镜像名+版本号。


----------
重命名容器名称
docker tag 镜像IMAGEID 新的名称:版本号
如果版本号不加的话,默认为 latest
例子
docker tag 1815d40a66ae demo:latest


你可能感兴趣的:(spring,java,spring,boot,容器)