天天看點

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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整合

Mybatis

Mybatis-plus

Docker

Redis

Shiro

Ngnix

的一些方法。

一、總思維導圖

二、Mybatis

1、起因

其實簡單來說,可以用發展的眼光去看待Mybatis。

其是對JDBC改進和完善

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

主要元素

  • xml檔案
  • DAO層或者Mapper層(其實兩個是相同概念)

2、架構

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

3、開發

3.1 原始DAO方法

3.2 Mapper代理方法

Mapper代理開發方法,隻需要編寫Mapper接口,但需要遵循開發規範

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

3.3 輸入輸出映射

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

3.4 動态SQL

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

3.5 SpringBoot整合Mybatis

Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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

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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
<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、實戰記錄

<!-- 引入druid資料源 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>     <!-- druid.version在父子產品上有初始化指派-->
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.1.9</version>
        </dependency>

		 <!-- 引入lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
        </dependency>


		<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.59</version>
        </dependency>


		<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>6.0.6</version>
            <scope>runtime</scope>
        </dependency>

		 <!-- mp依賴
            mybatisPlus 會自動的維護Mybatis 以及MyBatis-spring相關的依賴
        -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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應用)
  • 注解方式(攔截方法)(Web應用)
  • shiro提供的标簽((攔截頁面元素:按鈕,表格等))(Web應用)
<shiro:hasPermission name="使用者管理">
    <a href="#">使用者管理</a>
</shiro:hasPermission>


           

嘗試用第四種方式

2)JWT

Json Web token(JWT)

是為了在

網絡應用環境

間傳遞聲明而執行的一種

基于JSON的開放标準

(RFC 7519)。

它是用戶端和服務端

安全傳遞

以及

身份認證

的一種解決方案,可以用在登入上。該token可以被

加密

,可以在上面添加一些業務資訊供識别

組成主要有三個部分,頭部,載荷和簽證

  • 頭部:聲明類型和加密算法
  • 載荷:存放一些有效資訊,比如一些業務相關的資訊,例如使用者資訊
  • 簽證:簽證資訊,說白了就是拿頭部和載荷然後做加密操作而構成
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
  • 浏覽器通過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.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
            <dependency>
                <groupId>javax.xml.bind</groupId>
                <artifactId>jaxb-api</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-impl</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>com.sun.xml.bind</groupId>
                <artifactId>jaxb-core</artifactId>
                <version>2.3.0</version>
            </dependency>
            <dependency>
                <groupId>javax.activation</groupId>
                <artifactId>activation</artifactId>
                <version>1.1.1</version>
            </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執行個體的建立,代碼如下
  • 一般用法
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)密碼比對
通過

AuthenticatingRealm

credentialsMatcher

屬性來進行密碼的比對!
  • 擷取目前的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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

三、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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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子產品下面

<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- spring2.X內建redis所需common-pool2-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.0</version>
</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,設定

    合理的TT

    L或

    永不過期

  • 加互斥鎖

互斥鎖案例

  • 常量類
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系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker
Spring系列一品境之指玄境Spring系列複習(二)前言一、總思維導圖二、Mybatis三、Redis四、Ngnix五、Docker

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.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.13</version>
  <executions>
    <execution>
      <id>default</id>
      <goals>
        <goal>build</goal>
        <goal>push</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <repository>${docker.image.prefix}/${project.artifactId}</repository>
    <tag>${project.version}</tag>
    <buildArgs>
      <JAR_FILE>${project.build.finalName}.jar</JAR_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.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>1.0.0</version>
    <configuration>
        <!--鏡像名稱-->
        <imageName>10.211.55.4:5000/${project.artifactId}</imageName>
        <!--指定dockerfile路徑-->
        <!--<dockerDirectory>${project.basedir}/src/main/resources</dockerDirectory>-->
        <!--指定标簽-->
        <imageTags>
            <imageTag>latest</imageTag>
        </imageTags>
        <!--遠端倉庫位址-->
        <registryUrl>10.211.55.4:5000</registryUrl>
        <pushImage>true</pushImage>
        <!--基礎鏡像jdk1.8-->
        <baseImage>java</baseImage>
        <!--制作者提供本人資訊-->
        <maintainer>niceyoo [email protected]</maintainer>
        <!--切換到ROOT目錄-->
        <workdir>/ROOT</workdir>
        <cmd>["java","-version"]</cmd>
        <entryPoint>["java","-jar","${project.build.finalName}.jar"]</entryPoint>
        <!--指定遠端docker位址-->
        <dockerHost>http://10.211.55.4:2375</dockerHost>
        <!--這裡是複制jar包到docker容器指定目錄配置-->
        <resources>
            <resource>
                <targetPath>/ROOT</targetPath>
                <!--指定需要複制的根目錄,${project.build.directory}表示target目錄-->
                <directory>${project.build.directory}</directory>
                <!--用于指定需要複制的檔案,${project.build.finalName}.jar表示打包後的jar封包件-->
                <include>${project.build.finalName}.jar</include>
            </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