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改進和完善
主要元素
- xml檔案
- DAO層或者Mapper層(其實兩個是相同概念)
2、架構
3、開發
3.1 原始DAO方法
3.2 Mapper代理方法
Mapper代理開發方法,隻需要編寫Mapper接口,但需要遵循開發規範
3.3 輸入輸出映射
3.4 動态SQL
3.5 SpringBoot整合Mybatis
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>
依賴關系
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 實戰經驗
<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架構圖
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
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可以被
身份認證
,可以在上面添加一些業務資訊供識别
加密
組成主要有三個部分,頭部,載荷和簽證
- 頭部:聲明類型和加密算法
- 載荷:存放一些有效資訊,比如一些業務相關的資訊,例如使用者資訊
- 簽證:簽證資訊,說白了就是拿頭部和載荷然後做加密操作而構成
- 浏覽器通過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鹽值加密
主要為了防止
鹽值加密
出現
相同密碼
的情況,通過
相同密文
放入
随機鹽産生不同的密文
。
資料庫
:通過這個類的Util.bytes(“”)方法産生不同的鹽值。
ByteSource
- 在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執行個體
三、Redis
1、Redis簡介
Redis是現在最受歡迎的之一,Redis是一個使用ANSI C編寫的開源、包含多種資料結構、支援網絡、
NoSQL資料庫
、可選持久性的
基于記憶體
資料庫。
鍵值對存儲
-
編寫語言
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一個屬性一樣隻修改某一項屬性值。
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提供了判斷自動排重
的重要接口,這個也是list所不能提供的。是某個成員是否在一個set集合内
。不會自動有序的
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,設定
L或合理的TT
永不過期
- 加互斥鎖
互斥鎖案例
- 常量類
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 節點分發請求,達到負載均衡的效果
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