Spring系列复习(二)
相关导航
Spring系列一品境之金刚境
Spring指玄境导航
前言
本博文重在夯实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
@MapperScan(“cn.clboy.springbootmybatis.mapper”)//扫描某个包下的所有Mapper接口
2)XML方式
- 创建Mybatis全局配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 开启数据库中列名和pojp的驼峰命名映射 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
- 创建Mapper接口
@Mapper 或者 @MapperScan将接口扫描装配到容器中
public interface EmployeeMapper {
public Employee getEmpById(@Param("id") Integer id);
public void insertEmp(Employee employee);
}
- 创建映射文件mapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.zy.mapper.EmployeeMapper">
<select id="getEmpById" resultType="com.zy.pojo.Employee">
SELECT * FROM employee WHERE id = #{id};
</select>
<insert id="insertEmp">
INSERT INTO employee (lastName, email, gender, d_id) VALUSE (#{lastName}, #{email}, #{gender}, #{dId})
</insert>
</mapper>
- 配置文件application.yaml
# 加载mybati的全局配置文件
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
- 调用
@RestController
public class EmpController {
@Resource
private EmployeeMapper employeeMapper;//通过这个方式注入mapper
@GetMapping("/emp/{id}")
public Employee getEmp(@PathVariable("id") Integer id) {
return employeeMapper.getEmpById(id);
}
}
3.6 实战经验
<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应用)
/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;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
- JWTFilter
import com.demo.ops.mgt.util.EncryptUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
/**
* jwt过滤器,核心实现类
* @author zz
**/
@Slf4j
public class JWTFilter extends BasicHttpAuthenticationFilter {
public static final String TOKEN = "X-Token";
private static String whiteList;
private static Set<String> whiteSet = new HashSet<>();
private static List<String> prefixSet = new ArrayList<>();
public synchronized void init() {
whiteList = "/sys/login,/sys/logout,/v2/*";
initWhiteSet(whiteList);
}
private static void initWhiteSet(String whiteList) {
if (whiteList != null) {
log.info("reset whiteList: {}", whiteList);
Set<String> set = new HashSet<>();
List<String> prefixs = new ArrayList<>();
Arrays.stream(whiteList.split("\\s*,\\s*"))
.forEach((s) -> {
if (s.endsWith("*")) {
prefixs.add(s.substring(0, s.length() - 1));
} else {
set.add(s);
}
});
prefixSet = prefixs;
whiteSet = set;
}
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws UnauthorizedException {
if (whiteList == null) {
init();
}
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String path = httpServletRequest.getServletPath();
if (whiteSet.contains(path)) {
return true;
}
for(String whitePrefix : prefixSet){
if(path.startsWith(whitePrefix)){
return true;
}
}
if (isLoginAttempt(request, response)) {
return executeLogin(request, response);
}
return false;
}
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader(TOKEN);
return token != null;
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(TOKEN);
JWTToken jwtToken = new JWTToken(decryptToken(token));
try {
getSubject(request, response).login(jwtToken);
return true;
} catch (Exception e) {
log.debug("登录检查异常!异常信息:{}", e.getMessage(), e);
return false;
}
}
/**
* 对跨域提供支持
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
// 跨域时会首先发送一个 option请求,这里我们给 option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
@Override
protected boolean sendChallenge(ServletRequest request, ServletResponse response) {
log.debug("认证401!");
HttpServletResponse httpResponse = WebUtils.toHttp(response);
httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
httpResponse.setCharacterEncoding("utf-8");
httpResponse.setContentType("application/json; charset=utf-8");
final String message = "请先登录";
try (PrintWriter out = httpResponse.getWriter()) {
String responseJson = "{\"msg\":\"" + message + "\",\"symbol\":false}";
out.print(responseJson);
} catch (IOException e) {
log.error("登录检查输出信息异常!异常信息:", e);
}
return false;
}
/**
* token 加密
* @param token token
* @return 加密后的 token
*/
public static String encryptToken(String token) {
try {
EncryptUtil encryptUtil = new EncryptUtil(AnthenticationConstants.TOKEN_CACHE_PREFIX);
return encryptUtil.encrypt(token);
} catch (Exception e) {
log.error("token加密异常!异常信息:", e);
return null;
}
}
/**
* token 解密
* @param encryptToken 加密后的 token
* @return 解密后的 token
*/
public static String decryptToken(String encryptToken) {
try {
EncryptUtil encryptUtil = new EncryptUtil(AnthenticationConstants.TOKEN_CACHE_PREFIX);
return encryptUtil.decrypt(encryptToken);
} catch (Exception e) {
log.error("token解密异常!异常信息:", e);
return null;
}
}
}
- JWTUtil
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.demo.boot.util.SpringContextUtil;
import com.demo.ops.mgt.entity.SysUser;
import com.demo.ops.mgt.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* jwt工具类
* @author zz
**/
@Slf4j
public class JWTUtil {
private static final long EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7;
/**
* 校验 token是否正确
* @param token 密钥
* @param secret 用户的密码
* @return 是否正确
*/
public static boolean verify(String token, String username, String secret) {
try {
Algorithm algorithm = Algorithm.HMAC256(secret);
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("username", username)
.build();
verifier.verify(token);
return true;
} catch (Exception e) {
log.debug("token过期!过期信息:{}", e.getMessage());
return false;
}
}
/**
* 从token中获取用户名
* @return token中包含的用户名
*/
public static String getUsername(String token) {
try {
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim("username").asString();
} catch (JWTDecodeException e) {
log.debug("从token中获取用户名异常!异常信息:{}", e.getMessage());
return null;
}
}
/**
* 生成token
* @param username 用户名
* @param secret 用户的密码
* @return token
*/
public static String sign(String username, String secret) {
try {
username = StringUtils.lowerCase(username);
Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
Algorithm algorithm = Algorithm.HMAC256(secret);
return JWT.create()
.withClaim("username", username)
.withExpiresAt(date)
.sign(algorithm);
} catch (Exception e) {
log.error("生成token异常!异常信息:{}", e.getMessage());
return null;
}
}
/**
* 获取当前系统用户
* @param httpServletRequest
* @return
*/
public static SysUser getCurrentSysUser(HttpServletRequest httpServletRequest) {
String token = httpServletRequest.getHeader(JWTFilter.TOKEN);
if (StringUtils.isBlank(token)) {
return null;
}
String decryptToken = JWTFilter.decryptToken(token);
String userName = JWTUtil.getUsername(decryptToken);
ISysUserService sysUserService = (ISysUserService) SpringContextUtil.getBean("sysUserService");
return sysUserService.selectUserByUsername(userName);
}
}
2.1.2 导入依赖
修改pom.xml,导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
2.1.3 自定义Realm类
继承自AuthorizingRealm,并结合Service层,可以写多个Realm,分别对应不同功能
1)AuthenticationToken
- 收集用户提交的身份信息(如用户名和凭据(如密码))的接口。
- 扩展接口RememberMeAuthenticationToken:提供boolean isRememberMe()实现记住我功能。
- 扩展接口HostAuthenticationToken:提供String getHost()获取用户主机。
- 内置实现类UsernamePasswordToken:仅保存
用户名、密码
,并实现了以上两个接口,可以实现记住我
和主机验证
的支持。
2)AuthenticationInfo
- 封装验证通过的身份信息,主要包括Object属性principal(一般存储用户名)和credentials(密码)。
- MergableAuthenticationInfo子接口:在多Realm时合并AuthenticationInfo,主要合并Principal,如果是其他信息如credentialsSalt,则会后合并进来的AuthenticationInfo覆盖。
- SaltedAuthenticationInfo子接口:比如HashedCredentialsMatcher,在验证时会判断AuthenticationInfo是否是SaltedAuthenticationInfo的子类,是则获取其盐。
- Account子接口:相当于我们之前的[users],SimpleAccount是其实现。在IniRealm、PropertiesRealm这种静态创建账号的场景中使用,它们继承了SimpleAccountRealm,其中就有API用于增删查改SimpleAccount。适用于账号不是特别多的情况。
- SimpleAuthenticationInfo:一般都是返回这个类型。
3)PincipalCollection
-
Principal前缀:应该是上面AuthenticationInfo的属性principal。
-
PincipalCollection:是一个身份集合,保存
登录成功
的用户
的身份信息
。因为我们可以在Shiro中同时配置多个Realm
,所以身份信息就有多个
。可以传给doGetAuthorizationInfo()方法为登录成功的用户授权。 -
示例
准备三个Realm,命名分别为a,b,c,身份凭证只有细微差别。
public class MyRealm1 implements Realm {
@Override
public String getName() {
return "a"; //realm name 为 “a”
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
return new SimpleAuthenticationInfo(
"zhang", //身份 字符串类型
"123", //凭据
getName() //Realm Name
);
}
}
//和1完全一样,只是命名为b
public class MyRealm2 implements Realm {
@Override
public String getName() {
return "b"; //realm name 为 “b”
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
return new SimpleAuthenticationInfo(
"zhang", //身份 字符串类型
"123", //凭据
getName() //Realm Name
);
}
}
//除了命名不同,只是Principal类型为User,而不是简单的String
public class MyRealm3 implements Realm {
@Override
public String getName() {
return "c"; //realm name 为 “c”
}
@Override
public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
User user=new User("zhang","123");
return new SimpleAuthenticationInfo(
user, //身份 User类型
"123", //凭据
getName() //Realm Name
);
}
}
public class PrincipalCollectionTest extends BaseTest {
@Test
public void testPrincipalCollection(){
login("classpath:config/shiro-multirealm.ini",
"zhang","123");
Subject subject=subject();
//获取Map中第一个Principal,即PrimaryPrincipal
Object primaryPrincipal1=subject.getPrincipal();
//获取PrincipalCollection
PrincipalCollection principalCollection=subject.getPrincipals();
//也是获取PrimaryPrincipal
Object primaryPrincipal2=principalCollection.getPrimaryPrincipal();
//获取所有身份验证成功的Realm名字
Set<String> realmNames=principalCollection.getRealmNames();
for(String realmName:realmNames)
System.out.println(realmName);
//将身份信息转换为Set/List(实际转换为List也是先转为Set)
List<Object> principals=principalCollection.asList();
/*返回集合包含两个String类、一个User类,但由于两个String类都是"zhang",
所以只只剩下一个,转为List结果也是一样*/
for(Object principal:principals)
System.out.println("set:"+principal);
//根据realm名字获取身份,因为realm名字可以重复,
//所以可能有多个身份,建议尽量不要重复
Collection<User> users=principalCollection.fromRealm("c");
for(User user:users)
System.out.println("c:user="+user.getUsername()+user.getPassword());
Collection<String> usernames=principalCollection.fromRealm("b");
for(String username:usernames)
System.out.println("b:username="+username);
}
}
4)AuthorizationInfo
- 封装权限信息,主要是doGetAuthorizationInfo()时封装授权信息然后返回的。
- SimpleAuthorizationInfo:实现类,大多数时候使用这个。主要增加了以下方法
authorizationInfo.addRole("role1"); //添加角色到内部维护的role集合;
添加角色后调用MyRolePermissionResolver解析出权限
authorizationInfo.setRoles(Set<String> roles); //将内部维护的role集合设置为入参
authorizationInfo.addObjectPermission(new BitPermission("+user1+10")); //添加对象型权限
authorizationInfo.addObjectPermission(new WildcardPermission("user1:*"));
authorizationInfo.addStringPermission("+user2+10"); //字符串型权限
authorizationInfo.addStringPermission("user2:*");
authorizationInfo.setStringPermissions(Set<String> permissions);
5)Subject
- Shiro核心对象,基本所有身份验证、授权都是通过Subject完成的。
//获取身份信息
Object getPrincipal(); //Primary Principal
PrincipalCollection getPrincipals(); // PrincipalCollection
//身份验证
void login(AuthenticationToken token) throws AuthenticationException; //调用各种方法;
登录失败抛AuthenticationException,成功则调用isAuthenticated()返回true
boolean isAuthenticated(); //与isRemembered()一个为true一个为false
boolean isRemembered(); //返回true表示是通过记住我登录到额而不是调用login方法
//角色验证
boolean hasRole(String roleIdentifier); //返回true或false表示成功与否
boolean[] hasRoles(List<String> roleIdentifiers);
boolean hasAllRoles(Collection<String> roleIdentifiers);
void checkRole(String roleIdentifier) throws AuthorizationException; //失败抛异常
void checkRoles(Collection<String> roleIdentifiers) throws AuthorizationException;
void checkRoles(String... roleIdentifiers) throws AuthorizationException;
//权限验证
boolean isPermitted(String permission);
boolean isPermitted(Permission permission);
boolean[] isPermitted(String... permissions);
boolean[] isPermitted(List<Permission> permissions);
boolean isPermittedAll(String... permissions);
boolean isPermittedAll(Collection<Permission> permissions);
void checkPermission(String permission) throws AuthorizationException;
void checkPermission(Permission permission) throws AuthorizationException;
void checkPermissions(String... permissions) throws AuthorizationException;
void checkPermissions(Collection<Permission> permissions) throws AuthorizationException;
//会话(登录成功相当于建立了会话,然后调用getSession获取
Session getSession(); //相当于getSession(true)
Session getSession(boolean create); //当create=false,如果没有会话将返回null,
当create=true,没有也会强制创建一个
//退出
void logout();
//RunAs
void runAs(PrincipalCollection principals)
throws NullPointerException, IllegalStateException; //实现允许A作为B进行访问,
调用runAs(b)即可
boolean isRunAs(); //此时此方法返回true
PrincipalCollection getPreviousPrincipals(); //得到a的身份信息,
而getPrincipals()得到b的身份信息
PrincipalCollection releaseRunAs(); //不需要了RunAs则调用这个
//多线程
<V> V execute(Callable<V> callable) throws ExecutionException;
void execute(Runnable runnable);
<V> Callable<V> associateWith(Callable<V> callable);
Runnable associateWith(Runnable runnable);
- Subject的获取 一般不需要我们创建,直接通过SecurityUtils获取即可
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return subject;
}
- 首先查看当前线程是否绑定了Subject,没有则通过Subject.BUilder构建一个并绑定到线程返回。如果想自定义Subject实例的创建,代码如下
new Subject.Builder().principals(身份).authenticated(true/false).buildSubject()
- 一般用法
1、身份验证login()
2、授权hasRole*()/isPermitted*/checkRole*()/checkPermission*()
3、将相应的数据存储到会话Session
4、切换身份RunAs/多线程身份传播
5、退出
import com.cxh.mall.entity.SysUser;
import com.cxh.mall.service.SysMenuService;
import com.cxh.mall.service.SysRoleService;
import com.cxh.mall.service.SysUserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.util.StringUtils;
import java.util.HashSet;
import java.util.Set;
public class LoginRealm extends AuthorizingRealm {
@Autowired
@Lazy
private SysUserService sysUserService;
@Autowired
@Lazy
private SysRoleService sysRoleService;
@Autowired
@Lazy
private SysMenuService sysMenuService;
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) {
String username = (String) arg0.getPrimaryPrincipal();
SysUser sysUser = sysUserService.getUserByName(username);
// 角色列表
Set<String> roles = new HashSet<String>();
// 功能列表
Set<String> menus = new HashSet<String>();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
roles = sysRoleService.listByUser(sysUser.getId());
menus = sysMenuService.listByUser(sysUser.getId());
// 角色加入AuthorizationInfo认证对象
info.setRoles(roles);
// 权限加入AuthorizationInfo认证对象
info.setStringPermissions(menus);
return info;
}
/**
* 登录认证
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
if (StringUtils.isEmpty(authenticationToken.getPrincipal())) {
return null;
}
//获取用户信息
String username = authenticationToken.getPrincipal().toString();
if (username == null || username.length() == 0)
{
return null;
}
//获取用户信息
SysUser user = sysUserService.getUserByName(username);
if (user == null)
{
throw new UnknownAccountException(); //未知账号
}
//判断账号是否被锁定,状态(0:禁用;1:锁定;2:启用)
if(user.getStatus() == 0)
{
throw new DisabledAccountException(); //帐号禁用
}
if (user.getStatus() == 1)
{
throw new LockedAccountException(); //帐号锁定
}
//盐
String salt = "123456";
//验证
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
username, //用户名
user.getPassword(), //密码
ByteSource.Util.bytes(salt), //盐
getName() //realm name
);
return authenticationInfo;
}
public static void main(String[] args) {
String originalPassword = "123456"; //原始密码
String hashAlgorithmName = "MD5"; //加密方式
int hashIterations = 2; //加密的次数
//盐
String salt = "123456";
//加密
SimpleHash simpleHash = new SimpleHash(hashAlgorithmName, originalPassword, salt, hashIterations);
String encryptionPassword = simpleHash.toString();
//输出加密密码
System.out.println(encryptionPassword);
}
}
2.1.4 编写Shiro配置类
使用@Configuration注解注入
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
@Bean
@ConditionalOnMissingBean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAAP = new DefaultAdvisorAutoProxyCreator();
defaultAAP.setProxyTargetClass(true);
return defaultAAP;
}
//凭证匹配器, 密码校验交给Shiro的SimpleAuthenticationInfo进行处理
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("MD5");//散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2);//散列的次数;
return hashedCredentialsMatcher;
}
//将自己的验证方式加入容器
@Bean
public LoginRealm myShiroRealm() {
LoginRealm loginRealm = new LoginRealm();
//加入密码管理
loginRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return loginRealm;
}
//权限管理,配置主要是Realm的管理认证
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
//Filter工厂,设置对应的过滤条件和跳转条件
// 添加shiro的内置过滤器
/**
* anon:无需认证就可以访问
* authc:必须认证了才能访问
* user:必须拥有记住我功能才能用
* perms:拥有对某个资源的权限才能访问
* role:拥有某个角色的权限才能访问
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String, String> map = new HashMap<>();
//登出
map.put("/logout", "logout");
//登录
map.put("/loginSubmit", "anon");
//静态文件包
map.put("/res/**", "anon");
//对所有用户认证
map.put("/**", "authc");
//登录
shiroFilterFactoryBean.setLoginUrl("/login");
//首页
shiroFilterFactoryBean.setSuccessUrl("/index");
//错误页面,认证不通过跳转
shiroFilterFactoryBean.setUnauthorizedUrl("/error");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
}
2.1.5 Controller登录逻辑
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
@Controller
@Slf4j
public class LoginController {
/**
* 登录页面
*/
@GetMapping(value={"/", "/login"})
public String login(){
return "admin/loginPage";
}
/**
* 登录操作
*/
@RequestMapping("/loginSubmit")
public String login(String username, String password, ModelMap modelMap)
{
//参数验证
if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password))
{
modelMap.addAttribute("message", "账号密码必填!");
return "admin/loginPage";
}
//账号密码令牌
AuthenticationToken token = new UsernamePasswordToken(username, password);
//获得当前用户到登录对象,现在状态为未认证
Subject subject = SecurityUtils.getSubject();
try
{
//将令牌传到shiro提供的login方法验证,需要自定义realm
subject.login(token);
//没有异常表示验证成功,进入首页
return "admin/homePage";
}
catch (IncorrectCredentialsException ice)
{
modelMap.addAttribute("message", "用户名或密码不正确!");
}
catch (UnknownAccountException uae)
{
modelMap.addAttribute("message", "未知账户!");
}
catch (LockedAccountException lae)
{
modelMap.addAttribute("message", "账户被锁定!");
}
catch (DisabledAccountException dae)
{
modelMap.addAttribute("message", "账户被禁用!");
}
catch (ExcessiveAttemptsException eae)
{
modelMap.addAttribute("message", "用户名或密码错误次数太多!");
}
catch (AuthenticationException ae)
{
modelMap.addAttribute("message", "验证未通过!");
}
catch (Exception e)
{
modelMap.addAttribute("message", "验证未通过!");
}
//返回登录页
return "admin/loginPage";
}
/**
* 登出操作
*/
@RequestMapping("/logout")
public String logout()
{
//登出清除缓存
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login";
}
}
-------
前端请求
<div id="div_main">
<div id="div_head"><p>cxh电商平台管理后台</p></div>
<div id="div_content">
<form id="form_login" name="loginForm" method="post" action="/cxh/loginSubmit" onsubmit="return SubmitLogin()" autocomplete="off">
<input type="text" class="form-control form_control" name="username" placeholder="用户名" id="input_username" title="请输入用户名"/>
<input type="password" class="form-control form_control" name="password" placeholder="密码" id="input_password" title="请输入密码" autocomplete="on">
<span id="error_msg" style="color: red;">${message}</span>
<input type="submit" class="btn btn-danger" id="btn_login" value="登录"/>
</form>
</div>
</div>
//提交登录
function SubmitLogin() {
//判断用户名是否为空
if (!loginForm.username.value) {
alert("请输入用户姓名!");
loginForm.username.focus();
return false;
}
//判断密码是否为空
if (!loginForm.password.value) {
alert("请输入登录密码!");
loginForm.password.focus();
return false;
}
return true;
}
2.1.6 Shiro加密与解密
1)密码比对
通过
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=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服务
3.3 启动类配置
@SpringBootApplication
@MapperScan(basePackages = "com.arbor.mall.model.dao")
@EnableCaching // 加上此注解
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
3.4 Redis配置类
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
3.5 Redis工具类
@Component
public class RedisUtils {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public boolean del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
return redisTemplate.delete(key[0]);
}
return redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key)) > 0 ? true : false;
}
return false;
}
/**
* 匹配所有的key
*
* @param pettern
* @return
*/
public Set<String> keys(String pettern) {
if (pettern.trim() != "" && pettern != null) {
return redisTemplate.keys(pettern);
}
return null;
}
// ============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @param timeUnit 过期时间单位
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
}
3.6 业务使用
public Result login(UserDto userDto) {
//判断空
String uname = userDto.getUname();
String upassword = userDto.getUpassword();
if(StringUtils.isBlank(uname) || StringUtils.isBlank(upassword)){
return Result.error("300","参数错误");
}
//获取用户信息
User one = getControllerInfo(userDto);
if(!Objects.equals(one,null)){
BeanUtil.copyProperties(one,userDto,true);
String token = TokenUtils.genToken(one.getUid().toString(), one.getUpassword());
//存入到redis中 直接存token值到Redis中
//redisUtils.set(GlobalConstant.REDIS_KEY_TOKEN+one.getUid(),token);
//设置过期时间对应时间单位
redisUtils.set(GlobalConstant.REDIS_KEY_TOKEN+one.getUid(),token,GlobalConstant.REDIS_KEY_TOKEN_TIME, TimeUnit.HOURS);
userDto.setToken(token);
//设置动态菜单
String role = one.getRole();
List<Menu> menuList=getMenuListByRole(role);
userDto.setMenuList(menuList);
return Result.success(userDto);
}
return Result.error("300","用户名或者密码错误");
}
------------
@Service
public class CategoryServiceImpl implements CategoryService {
@Override
// 方法加上此注解,value是在Redis存储时key的值
@Cacheable(value = "listCategoryForCustomer")
public List<CategoryVO> listCategoryForCustomer() {
ArrayList<CategoryVO> categoryVOList = new ArrayList<>();
recursivelyFindCategories(categoryVOList, 0);
return categoryVOList;
}
}
3.7 补充
修改密码,删除用户,退出登录进行删除对应的token
//删除Redis中token值
redisUtils.del(GlobalConstant.REDIS_KEY_TOKEN+cid);
3.8 常用注解
3.8.1 @Cacheable
根据方法对
其返回结果
进行缓存
,下次请求时,如果缓存存在
,则直接读取缓存数据
返回;如果缓存不存在
,则执行方法
,并把返回的结果存入缓存
中。一般用在查询
方法上。
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
3.8.2 @CachePut
使用该
注解
标志的方法
,每次
都会执行
,并将结果
存入指定的缓存
中。其他方法可以直接从响应的缓存
中读取缓存数据
,而不需要再去查询数据库
。一般用在新增方法
上。
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
3.8.3 @CacheEvict
使用该注解标志的方法,会
清空指定的缓存
。一般用在更新
或者删除
方法上
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
3.9 存在问题
- 关闭防火墙
- 找到redis配置文件, 注释一行配置 注释掉:#bind 127.0.0.1
4、技术升级
4.1 数据持久化
由于Redis的强大性能很大程度上是因为所有数据都是存储在内存
中,然而当出现服务器宕机、redis重启等特殊场景,所有存储在内存中的数据将会丢失,这是无法容忍的事情,所以必须将内存数据持久化
。例如:将redis作为数据库使用的;将redis作为缓存服务器使用等场景。
目前持久化存在两种方式:RDB方式和AOF方式。
4.1.1 RDB方式
RDB持久化是把
当前进程数据
生成快照
保存到硬盘
的过程, 触发RDB持久化过程分为手动触发
和自动触发
。
一般存在以下情况会对数据进行快照。
- 根据配置规则进行自动快照;
- 用户执行SAVE, BGSAVE命令;
- 执行FLUSHALL命令;
- 执行复制(replication)时。
优缺点:恢复数据较AOF更快;
4.1.2 AOF方式
以
独立日志
的方式记录每次写命令
(写入的内容直接是文本协议格式 ),重启时
再重新执行
AOF文件中的命令
达到恢复数据的目的。
- AOF的工作流程操作: 命令写入(append) 、 文件同步(sync) 、 文件重写(rewrite) 、 重启加载(load)
- 优点:实时性较好
4.2 雪崩
4.2.1 雪崩定义
缓存雪崩是指Redis
缓存层
由于某种原因宕机后
(有一种情况就是,缓存中大批量数据到过期时间,而查询数据量巨大
,引起数据库压力过大
甚至宕机
),所有的请求会涌向存储层
,短时间内的高并发请求
可能会导致存储层挂机
,称之为“Redis雪崩”。
4.2.2 规避方案
- 缓存数据的
过期时间
设置随机
,防止同一时间大量数据过期现象发生。 - 使用
Redis集群
,如果缓存数据库是分布式部署
,将热点数据均匀分布
在不同的缓存数据库
中。 - 设置
热点数据
永远不过期
- 限流
- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 事后:redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
4.3 击穿
4.3.1 击穿定义
缓存击穿是指,在Redis获取某一key时, 由于
key不存在
在缓存中但数据库中有
, 而必须向DB发起一次请求的行为, 这时由于并发用户特别多
,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大
,造成过大压力,称为“Redis击穿”。
4.3.2 发生原因
- 第一次访问
- 恶意访问不存在的Key
- Key过期
4.3.3 规避方案
- 服务器启动时,
提前写入
对应的key
规范
key的命名
, 通过中间件拦截- 对某些高频访问的Key,设置
合理的TT
L或永不过期
- 加互斥锁
互斥锁案例
- 常量类
package com.wl.standard.common.result.constants;
/**
* redis常量
* @author wl
* @date 2022/3/17 16:09
*/
public interface RedisConstants {
/**
* 空值缓存过期时间(分钟)
*/
Long CACHE_NULL_TTL = 2L;
/**
* 城市redis缓存key
*/
String CACHE_CITY_KEY = "cache:city:";
/**
* 城市redis缓存过期时间(分钟)
*/
Long CACHE_CITY_TTL = 30L;
/**
* 城市redis互斥锁key
*/
String LOCK_CITY_KEY = "lock:city:";
}
- Service实现层
package com.wl.standard.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.wl.standard.common.result.constants.RedisConstants;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import com.wl.standard.mapper.CityMapper;
import com.wl.standard.entity.City;
import com.wl.standard.service.CityService;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @author wl
* @date 2021/11/18
*/
@Service
@Slf4j
public class CityServiceImpl extends ServiceImpl<CityMapper, City> implements CityService{
private StringRedisTemplate stringRedisTemplate;
@Autowired
public CityServiceImpl(StringRedisTemplate stringRedisTemplate){
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public City getByCode(String cityCode) {
String key = RedisConstants.CACHE_CITY_KEY+cityCode;
return queryCityWithMutex(key, cityCode);
}
/**
* 通过互斥锁机制查询城市信息
* @param key
*/
private City queryCityWithMutex(String key, String cityCode) {
City city = null;
// 1.查询缓存
String cityJson = stringRedisTemplate.opsForValue().get(key);
// 2.判断缓存是否有数据
if (StringUtils.isNotBlank(cityJson)) {
// 3.有,则返回
city = JSONObject.parseObject(cityJson, City.class);
return city;
}
// 4.无,则获取互斥锁
String lockKey = RedisConstants.LOCK_CITY_KEY + cityCode;
Boolean isLock = tryLock(lockKey);
// 5.判断获取锁是否成功
try {
if (!isLock) {
// 6.获取失败, 休眠并重试
Thread.sleep(100);
return queryCityWithMutex(key, cityCode);
}
// 7.获取成功, 查询数据库
city = baseMapper.getByCode(cityCode);
// 8.判断数据库是否有数据
if (city == null) {
// 9.无,则将空数据写入redis
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 10.有,则将数据写入redis
stringRedisTemplate.opsForValue().set(key, JSONObject.toJSONString(city), RedisConstants.CACHE_CITY_TTL, TimeUnit.MINUTES);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// 11.释放锁
unLock(lockKey);
}
// 12.返回数据
return city;
}
/**
* 获取互斥锁
* @return
*/
private Boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtils.isTrue(flag);
}
/**
* 释放锁
* @param key
*/
private void unLock(String key) {
stringRedisTemplate.delete(key);
}
}
四、Ngnix
1、负载均衡定义
当一台服务器的性能达到极限时,我们可以使用
服务器集群
来提高网站的整体性能。
那么,在服务器集群中,需要有一台服务器充当调度者
的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况
将请求分配
给某一台后端服务器
去处理。
2、反向代理负载均衡
反向代理服务器是一个位于实际服务器之前的服务器,所有向我们网站发来的请求都首先要经过反向代理服务器。
服务器根据用户的请求要么直接将结果返回给用户
,要么将请求交给后端服务器处理
,再返回给用户
。
3、Ngnix
俄罗斯人开发的一个高性能的 HTTP和反向代理服务器。
由于Nginx 超越 Apache 的高性能和稳定性
,使得国内使用 Nginx 作为 Web 服务器的网站也越来越多,其中包括新浪博客、新浪播客、网易新闻、腾讯网、搜狐博客等门户网站频道等,在3w以上的高并发环境下,ngnix处理能力相当于apache的10倍。
4、SpringBoot集成Ngnix
Nginx代理服务器
搭配多台Tomcat服务器
即多个SpringBoot容器
,利用负载均衡策略实现tomcat集群
的部署。
SpringBoot容器可以相同,也可以不同
Ngnix通过配置 upstream 节点分发请求,达到负载均衡的效果
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镜像: ----类似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 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