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: rootpassword: 1111url: jdbc:mysql://localhost:3306/springboot_mybatisdriver-class-name: com.mysql.jdbc.Driverinitialization-mode: always# 数据源更改为druidtype: com.alibaba.druid.pool.DruidDataSourcedruid:# 连接池配置# 配置初始化大小、最小、最大initial-size: 1min-idle: 1max-active: 20# 配置获取连接等待超时的时间max-wait: 3000validation-query: SELECT 1 FROM DUALtest-on-borrow: falsetest-on-return: falsetest-while-idle: truepool-prepared-statements: truetime-between-eviction-runs-millis: 60000min-evictable-idle-time-millis: 300000filters: stat,wall,slf4j# 配置web监控,默认配置也和下面相同(除用户名密码,enabled默认false外),其他可以不配web-stat-filter:enabled: trueurl-pattern: /*exclusions: "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"stat-view-servlet:enabled: trueurl-pattern: /druid/*login-username: adminlogin-password: rootallow: 127.0.0.1schema:- 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 {@Resourceprivate DepartmentMapper departmentMapper; //重点@GetMapping("/dept/{id}")public Department getDepartment(@PathVariable("id") Integer id) {return departmentMapper.getDeptById(id);}@GetMapping("/dept")public Department insertDept(Department department) {int count = departmentMapper.insertDept(department);if (count > 0) {System.out.println("插入数据成功");}return department;}
- Mapper扫描
- 使用@mapper注解的类可以被扫描到容器中,但是每个Mapper都要加上这个注解就是一个繁琐的工作
- 可以在springboot启动类上加上
@MapperScan
@MapperScan(“cn.clboy.springbootmybatis.mapper”)//扫描某个包下的所有Mapper接口
2)XML方式
- 创建Mybatis全局配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configurationPUBLIC "-//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 mapperPUBLIC "-//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.xmlmapper-locations: classpath:mybatis/mapper/*.xml
- 调用
@RestController
public class EmpController {@Resourceprivate 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;@Beforepublic void setUp() throws IOException{String resource="SqlMapConfig.xml";InputStream inputStream= Resources.getResourceAsStream(resource);sqlSessionFactory=new SqlSessionFactoryBuilder().build(inputStream); //创建配置工厂}@Testpublic 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 {@AutowiredAdminService adminService;@AutowiredRedisUtil redisUtil;@AutowiredSysParamsService sysParamsService;@Resourceprivate AdminMapper adminMapper;@Autowiredprivate WebUtil webUtil;@Resourceprivate PictureFeignClient pictureFeignClient;@Autowiredprivate RoleService roleService;@Overridepublic Admin getAdminByUid(String uid) {return adminMapper.getAdminByUid(uid);}@Overridepublic String getOnlineAdminList(AdminVO adminVO) {// 获取Redis中匹配的所有keySet<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 {@Beanpublic 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:令牌(用户名+密码的封装)----进行进行认证的封装对象
这个的对象并不是前后分离的这个tokenSecurity Manager:安全管理器(只要使用了shiro框架那么这个对象都是必不可少的)
Authenticator:认证器(主要做用户身份认证、简单跟你说就是用来登陆的时候做身份校验的)
Authrizer:授权器(简单的说就是用来做用户的授权的)
Realm:用户认证和授权的时候 和数据库交互的对象(这里面干的事情就是从数据库查询数据 封装成token然后取进行认证和授权)
认证
主要是进行身份的认证
(可以说局限在登入认证
这一块)- 授权
认证成功后
,获取
用户的权限
(给该用户分配对应的权限);访问资源时候,进行授权校验:用访问资源需要的权限去用户权限列表查找,如果存在,则有权限访问资源。(权限拦截
)
1)四种权限检验方式
- 硬编码方式(拦截方法)(非Web应用,Web应用)
Subject subject = SecurityUtils.getSubject();
subject.checkPermission("部门管理");
- 过滤器配置方式(拦截url)(Web应用)
/system/user/list.do = perms["用户管理"]
- 注解方式(拦截方法)(Web应用)
@RequiresPermissions(“”)
- shiro提供的标签((拦截页面元素:按钮,表格等))(Web应用)
<shiro:hasPermission name="用户管理"><a href="#">用户管理</a>
</shiro:hasPermission>
尝试用第四种方式
2)JWT
Json Web token(JWT)
是为了在网络应用环境
间传递声明而执行的一种基于JSON的开放标准
(RFC 7519)。
它是客户端和服务端安全传递
以及身份认证
的一种解决方案,可以用在登录上。该token可以被加密
,可以在上面添加一些业务信息供识别
组成主要有三个部分,头部,载荷和签证
- 头部:声明类型和加密算法
- 载荷:存放一些有效信息,比如一些业务相关的信息,例如用户信息
- 签证:签证信息,说白了就是拿头部和载荷然后做加密操作而构成
-
浏览器通过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;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic 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;}}@Overrideprotected 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;}@Overrideprotected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {HttpServletRequest req = (HttpServletRequest) request;String token = req.getHeader(TOKEN);return token != null;}@Overrideprotected 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;}}/*** 对跨域提供支持*/@Overrideprotected 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);}@Overrideprotected 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 {@Overridepublic String getName() {return "a"; //realm name 为 “a”}@Overridepublic AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {return new SimpleAuthenticationInfo("zhang", //身份 字符串类型"123", //凭据getName() //Realm Name);}
}//和1完全一样,只是命名为b
public class MyRealm2 implements Realm {@Overridepublic String getName() {return "b"; //realm name 为 “b”}@Overridepublic AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)throws AuthenticationException {return new SimpleAuthenticationInfo("zhang", //身份 字符串类型"123", //凭据getName() //Realm Name);}
}//除了命名不同,只是Principal类型为User,而不是简单的String
public class MyRealm3 implements Realm {@Overridepublic String getName() {return "c"; //realm name 为 “c”}@Overridepublic 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 {@Testpublic void testPrincipalCollection(){login("classpath:config/shiro-multirealm.ini","zhang","123");Subject subject=subject();//获取Map中第一个Principal,即PrimaryPrincipalObject primaryPrincipal1=subject.getPrincipal();//获取PrincipalCollectionPrincipalCollection principalCollection=subject.getPrincipals();//也是获取PrimaryPrincipalObject primaryPrincipal2=principalCollection.getPrimaryPrincipal();//获取所有身份验证成功的Realm名字Set<String> realmNames=principalCollection.getRealmNames();for(String realmName:realmNames)System.out.println(realmName);//将身份信息转换为Set/List(实际转换为List也是先转为Set)List<Object> principals=principalCollection.asList();/*返回集合包含两个String类、一个User类,但由于两个String类都是"zhang",所以只只剩下一个,转为List结果也是一样*/for(Object principal:principals)System.out.println("set:"+principal);//根据realm名字获取身份,因为realm名字可以重复,//所以可能有多个身份,建议尽量不要重复Collection<User> users=principalCollection.fromRealm("c");for(User user:users)System.out.println("c:user="+user.getUsername()+user.getPassword());Collection<String> usernames=principalCollection.fromRealm("b");for(String username:usernames)System.out.println("b:username="+username);}
}
4)AuthorizationInfo
- 封装权限信息,主要是doGetAuthorizationInfo()时封装授权信息然后返回的。
- SimpleAuthorizationInfo:实现类,大多数时候使用这个。主要增加了以下方法
authorizationInfo.addRole("role1"); //添加角色到内部维护的role集合;添加角色后调用MyRolePermissionResolver解析出权限
authorizationInfo.setRoles(Set<String> roles); //将内部维护的role集合设置为入参authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); //添加对象型权限
authorizationInfo.addObjectPermission(new WildcardPermission("user1:*"));
authorizationInfo.addStringPermission("+user2+10"); //字符串型权限
authorizationInfo.addStringPermission("user2:*");
authorizationInfo.setStringPermissions(Set<String> permissions);
5)Subject
- Shiro核心对象,基本所有身份验证、授权都是通过Subject完成的。
//获取身份信息
Object getPrincipal(); //Primary Principal
PrincipalCollection getPrincipals(); // PrincipalCollection//身份验证
void login(AuthenticationToken token) throws AuthenticationException; //调用各种方法;登录失败抛AuthenticationException,成功则调用isAuthenticated()返回true
boolean isAuthenticated(); //与isRemembered()一个为true一个为false
boolean isRemembered(); //返回true表示是通过记住我登录到额而不是调用login方法//角色验证
boolean hasRole(String roleIdentifier); //返回true或false表示成功与否
boolean[] hasRoles(List<String> roleIdentifiers);
boolean hasAllRoles(Collection<String> roleIdentifiers);
void checkRole(String roleIdentifier) throws AuthorizationException; //失败抛异常
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(String... roleIdentifiers) throws AuthorizationException;//权限验证
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;//会话(登录成功相当于建立了会话,然后调用getSession获取
Session getSession(); //相当于getSession(true)
Session getSession(boolean create); //当create=false,如果没有会话将返回null,当create=true,没有也会强制创建一个//退出
void logout();//RunAs
void runAs(PrincipalCollection principals) throws NullPointerException, IllegalStateException; //实现允许A作为B进行访问,调用runAs(b)即可
boolean isRunAs(); //此时此方法返回true
PrincipalCollection getPreviousPrincipals(); //得到a的身份信息,而getPrincipals()得到b的身份信息
PrincipalCollection releaseRunAs(); //不需要了RunAs则调用这个//多线程
<V> V execute(Callable<V> callable) throws ExecutionException;
void execute(Runnable runnable);
<V> Callable<V> associateWith(Callable<V> callable);
Runnable associateWith(Runnable runnable);
- Subject的获取 一般不需要我们创建,直接通过SecurityUtils获取即可
public static Subject getSubject() {Subject subject = ThreadContext.getSubject();if (subject == null) {subject = (new Subject.Builder()).buildSubject();ThreadContext.bind(subject);}return subject;
}
- 首先查看当前线程是否绑定了Subject,没有则通过Subject.BUilder构建一个并绑定到线程返回。如果想自定义Subject实例的创建,代码如下
new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
- 一般用法
1、身份验证login()
2、授权hasRole*()/isPermitted*/checkRole*()/checkPermission*()
3、将相应的数据存储到会话Session
4、切换身份RunAs/多线程身份传播
5、退出
import com.cxh.mall.entity.SysUser;
import com.cxh.mall.service.SysMenuService;
import com.cxh.mall.service.SysRoleService;
import com.cxh.mall.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.StringUtils;import java.util.HashSet;
import java.util.Set;public class LoginRealm extends AuthorizingRealm {@Autowired@Lazyprivate SysUserService sysUserService;@Autowired@Lazyprivate SysRoleService sysRoleService;@Autowired@Lazyprivate SysMenuService sysMenuService;/*** 授权*/@Overrideprotected 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;}/*** 登录认证*/@Overrideprotected 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@ConditionalOnMissingBeanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();defaultAAP.setProxyTargetClass(true);return defaultAAP;}//凭证匹配器, 密码校验交给Shiro的SimpleAuthenticationInfo进行处理@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;hashedCredentialsMatcher.setHashIterations(2);//散列的次数;return hashedCredentialsMatcher;}//将自己的验证方式加入容器@Beanpublic LoginRealm myShiroRealm() {LoginRealm loginRealm = new LoginRealm();//加入密码管理loginRealm.setCredentialsMatcher(hashedCredentialsMatcher());return loginRealm;}//权限管理,配置主要是Realm的管理认证@Beanpublic SecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(myShiroRealm());return securityManager;}//Filter工厂,设置对应的过滤条件和跳转条件// 添加shiro的内置过滤器/*** anon:无需认证就可以访问* authc:必须认证了才能访问* user:必须拥有记住我功能才能用* perms:拥有对某个资源的权限才能访问* role:拥有某个角色的权限才能访问*/@Beanpublic 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;}@Beanpublic 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方法验证,需要自定义realmsubject.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实例
三、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一个属性一样只修改某一项属性值。
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=1800000spring.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.1port: 6379password:#连接超时时间(毫秒)timeout: 36000ms# Redis默认情况下有16个分片,默认0database: 0lettuce:pool:# 连接池最大连接数(使用负值表示没有限制) 默认 8max-active: 8# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1max-wait: -1ms# 连接池中的最大空闲连接 默认 8max-idle: 8# 连接池中的最小空闲连接 默认 0min-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 {@Beanpublic 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;}@Beanpublic 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 {@Autowiredprivate 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;@Autowiredpublic CityServiceImpl(StringRedisTemplate stringRedisTemplate){this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic 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.无,则将空数据写入redisstringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);return null;}// 10.有,则将数据写入redisstringRedisTemplate.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中 classdocker容器 : ----类似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 apkdream@163.com</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