相关导航
Spring系列一品境之金刚境
本博文重在夯实Spring全家桶的知识点,回归书本,夯实基础,学深学精
Java相关基础已复习完毕,现在就到了Spring全家桶系列了,欲练神功,先固内功。之前做项目对Spring全家桶学的一知半解,好多基础概念都不清楚,正好借此机会梳理一下相关知识点。
参考书籍:《Spring In Action 5th EDITION》与《多线程与高并发 马士兵丛书》
本博文主要归纳整理Spring全家桶中SpringBoot整合Mybatis
、Mybatis-plus
、Docker
、Redis
、Shiro
、Ngnix
的一些方法。
其实简单来说,可以用发展的眼光去看待Mybatis。
其是对JDBC改进和完善
主要元素
Mapper代理开发方法,只需要编写Mapper接口,但需要遵循开发规范
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.20</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
spring:
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
// 指定这是一个操作数据库的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;
}
@MapperScan
@MapperScan(“cn.clboy.springbootmybatis.mapper”)//扫描某个包下的所有Mapper接口
<?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 或者 @MapperScan将接口扫描装配到容器中
public interface EmployeeMapper {
public Employee getEmpById(@Param("id") Integer id);
public void insertEmp(Employee employee);
}
<?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>
# 加载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);
}
}
<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();
}
}
在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联系起来的媒介
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druid-spring-boot-starterartifactId>
<version>1.1.9version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.59version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>6.0.6version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.1version>
dependency>
@Mapper
就不能
写@Component
注解了,会产生注解冲突类
,而@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
是一个功能强大且易于使用的Java安全
框架。
使用Shiro易于理解的API,您可以快速轻松地保护
任何应用程序—从最小的移动应用程序到最大的web和企业应用程序
认证授权校验
的相关的代码http://shiro.apache.org/download.html
shiro-all 是shiro的所有功能jar包
shiro-core 是shiro的基本功能包
shiro-web 和web集成的包
shiro-spring shrio和spring集成的包
Subject:登陆的这个用户(用户、程序) 、谁认证那么这个主体就是谁
Principal:用户名(还可以是用户信息的封装)
Credential:密码
Token:令牌(用户名+密码的封装)----进行进行认证的封装对象
这个的对象并不是前后分离的这个token
Security Manager:安全管理器(只要使用了shiro框架那么这个对象都是必不可少的)
Authenticator:认证器(主要做用户身份认证、简单跟你说就是用来登陆的时候做身份校验的)
Authrizer:授权器(简单的说就是用来做用户的授权的)
Realm:用户认证和授权的时候 和数据库交互的对象(这里面干的事情就是从数据库查询数据 封装成token然后取进行认证和授权)
认证
主要是进行身份的认证
(可以说局限在登入认证
这一块)认证成功后
,获取
用户的权限
(给该用户分配对应的权限);访问资源时候,进行授权校验:用访问资源需要的权限去用户权限列表查找,如果存在,则有权限访问资源。(权限拦截
)Subject subject = SecurityUtils.getSubject();
subject.checkPermission("部门管理");
/system/user/list.do = perms["用户管理"]
@RequiresPermissions(“”)
<shiro:hasPermission name="用户管理">
<a href="#">用户管理</a>
</shiro:hasPermission>
尝试用第四种方式
Json Web token(JWT)
是为了在网络应用环境
间传递声明而执行的一种基于JSON的开放标准
(RFC 7519)。
它是客户端和服务端安全传递
以及身份认证
的一种解决方案,可以用在登录上。该token可以被加密
,可以在上面添加一些业务信息供识别
组成主要有三个部分,头部,载荷和签证
浏览器通过http请求发送用户名和密码到服务器
服务器进行验证,验证通过后创建一个jwt token(携带用户信息)
将该token返回给浏览器,由浏览器保存
下次请求时,浏览器会带上当前token
服务器对该token进行验签,通过后从token中获取用户信息
根据当前获取的用户信息,做出响应,返回对应的数据
和Cookie的区别(开发中尽量尝试token
)
状态
的 ;这个token只需要存在客户端服务器在收到数据后,进行解析,token是无状态的1、 支持跨域访问 ,将token置于请求头中,而cookie是不支持跨域访问的;
2、 无状态化, 服务端无需存储token ,只需要验证token信息是否正确即可,而session需要在服务端存储,一般是通过cookie中的sessionID在服务端查找对应的session;
3、 无需绑定到一个特殊的身份验证 方案(传统的用户名密码登陆),只需要生成的token是符合我们预期设定的即可;
4、 更适用于移动端 (Android,iOS,小程序等等),像这种原生平台不支持cookie,比如说微信小程序,每一次请求都是一次会话,当然我们可以每次去手动为他添加cookie,详情请查看博主另一篇博客;
5、 避免CSRF跨站伪造攻击 ,还是因为不依赖cookie;
6、 非常适用于RESTful API ,这样可以轻易与各种后端(java,.net,python…)相结合,去耦合
生成token
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
<dependency>
<groupId>javax.xml.bindgroupId>
<artifactId>jaxb-apiartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-implartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>com.sun.xml.bindgroupId>
<artifactId>jaxb-coreartifactId>
<version>2.3.0version>
dependency>
<dependency>
<groupId>javax.activationgroupId>
<artifactId>activationartifactId>
<version>1.1.1version>
dependency>
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;
}
}
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;
}
}
}
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);
}
}
修改pom.xml,导入依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
继承自AuthorizingRealm,并结合Service层,可以写多个Realm,分别对应不同功能
用户名、密码
,并实现了以上两个接口,可以实现记住我
和主机验证
的支持。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);
}
}
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);
//获取身份信息
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);
public static Subject getSubject() {
Subject subject = ThreadContext.getSubject();
if (subject == null) {
subject = (new Subject.Builder()).buildSubject();
ThreadContext.bind(subject);
}
return 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);
}
}
使用@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;
}
}
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;
}
通过
AuthenticatingRealm
的credentialsMatcher
属性来进行密码的比对!
盐值加密
主要为了防止相同密码
出现相同密文
的情况,通过随机盐产生不同的密文
放入数据库
。
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的开发标准
@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,"");
}
@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,"");
}
1).在shiroConfig中对接口添加需要授权
/**
* 为add接口添加授权过滤器
* 注意: 当授权拦截后,shiro会自动跳转到未授权页面
*/
map.put("/add","perms[user:add]");
2). 设置未授权提示页面
//设置未授权提示页面
shiroFilterFactoryBean.setUnauthorizedUrl("/unAuth"); //跳转到的controller接口
3). 编写跳转接口以及接口中定义跳转的页面
@RequestMapping("unAuth")
public String unAuth(){
return "user/unAuth";
}
@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;
}
Redis是现在最受欢迎的
NoSQL数据库
之一,Redis是一个使用ANSI C编写的开源、包含多种数据结构、支持网络、基于内存
、可选持久性的键值对存储
数据库。
底层代码执行效率高
,依赖性低
,没有太多运行时的依赖,而且系统的兼容性好
,稳定性高
基于内存
的数据里,可避免磁盘IO
,因此也被称作缓存工具
key-value
的方式进行存储,也就是使用hash结构
进行操作,数据的操作时间复杂度是O(1)
单进程单线程
的模型,可以避免上下文切换和线程之间引起的资源竞争。而且Redis还采用了IO多路复用技术,这里的多路复用是指多个socket网络连接,复用是指一个线程中处理多个IO请求,这样可以减少网络IO的消耗,大幅度提升效率应用场景浓缩为 高性能、高并发
Redis提供的数据类型主要分为5种自有类型和一种自定义类型,这5种自有类型包括:String
类型、哈希
类型、列表
类型、集合
类型和顺序集合
类型。
• 获取字符串长度
• 设置和获取字符串的某一段内容
• 设置及获取字符串的某一位(bit)
• 设置及获取字符串的某一位
• 批量设置一系列字符串的内容
自动排重
的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内
的重要接口,这个也是list所不能提供的。是不会自动有序的
。set不是自动有序的
,而sorted set
可以通过用户额外提供一个优先级(score)
的参数来为成员排序
,并且是插入有序的
,即自动排序
。当你需要一个有序的并且不重复
的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。Redis缓存是公共应用,可以把依赖与配置添加到了common模块下面
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.6.0version>
dependency>
在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
Redis的安装与使用
@SpringBootApplication
@MapperScan(basePackages = "com.arbor.mall.model.dao")
@EnableCaching // 加上此注解
public class MallApplication {
public static void main(String[] args) {
SpringApplication.run(MallApplication.class, args);
}
}
@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;
}
}
@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);
}
}
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;
}
}
修改密码,删除用户,退出登录进行删除对应的token
//删除Redis中token值
redisUtils.del(GlobalConstant.REDIS_KEY_TOKEN+cid);
根据方法对
其返回结果
进行缓存
,下次请求时,如果缓存存在
,则直接读取缓存数据
返回;如果缓存不存在
,则执行方法
,并把返回的结果存入缓存
中。一般用在查询
方法上。
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
使用该
注解
标志的方法
,每次
都会执行
,并将结果
存入指定的缓存
中。其他方法可以直接从响应的缓存
中读取缓存数据
,而不需要再去查询数据库
。一般用在新增方法
上。
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
使用该注解标志的方法,会
清空指定的缓存
。一般用在更新
或者删除
方法上
属性值如下
属性/方法 | 解释 |
---|---|
value | 缓存名,必填,它指定了你的缓存存放在哪块命名空间 |
cacheNames | 与 value 差不多,二选一即可 |
key | 可选属性,可以使用 SpEL 标签自定义缓存的key |
allEntries | 是否清空所有缓存,默认为 false。如果指定为 true,则方法调用后将立即清空所有的缓存 |
beforeInvocation | 是否在方法执行前就清空,默认为 false。如果指定为 true,则在方法执行前就会清空缓存 |
由于Redis的强大性能很大程度上是因为所有数据都是存储在内存
中,然而当出现服务器宕机、redis重启等特殊场景,所有存储在内存中的数据将会丢失,这是无法容忍的事情,所以必须将内存数据持久化
。例如:将redis作为数据库使用的;将redis作为缓存服务器使用等场景。
目前持久化存在两种方式:RDB方式和AOF方式。
RDB持久化是把
当前进程数据
生成快照
保存到硬盘
的过程, 触发RDB持久化过程分为手动触发
和自动触发
。
一般存在以下情况会对数据进行快照。
优缺点:恢复数据较AOF更快;
以
独立日志
的方式记录每次写命令
(写入的内容直接是文本协议格式 ),重启时
再重新执行
AOF文件中的命令
达到恢复数据的目的。
缓存雪崩是指Redis
缓存层
由于某种原因宕机后
(有一种情况就是,缓存中大批量数据到过期时间,而查询数据量巨大
,引起数据库压力过大
甚至宕机
),所有的请求会涌向存储层
,短时间内的高并发请求
可能会导致存储层挂机
,称之为“Redis雪崩”。
过期时间
设置随机
,防止同一时间大量数据过期现象发生。Redis集群
,如果缓存数据库是分布式部署
,将热点数据均匀分布
在 不同的缓存数据库
中。热点数据
永远不过期
缓存击穿是指,在Redis获取某一key时, 由于
key不存在
在缓存中但数据库中有
, 而必须向DB发起一次请求的行为, 这时由于并发用户特别多
,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大
,造成过大压力,称为“Redis击穿”。
提前写入
对应的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:";
}
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);
}
}
当一台服务器的性能达到极限时,我们可以使用
服务器集群
来提高网站的整体性能。
那么,在服务器集群中,需要有一台服务器充当调度者
的角色,用户的所有请求都会首先由它接收,调度者再根据每台服务器的负载情况
将请求分配
给某一台后端服务器
去处理。
反向代理服务器是一个位于实际服务器之前的服务器,所有向我们网站发来的请求都首先要经过反向代理服务器。
服务器根据用户的请求要么直接将结果返回给用户
,要么将请求交给后端服务器处理
,再返回给用户
。
俄罗斯人开发的一个高性能的 HTTP和反向代理服务器。
由于Nginx 超越 Apache 的高性能和稳定性
,使得国内使用 Nginx 作为 Web 服务器的网站也越来越多,其中包括新浪博客、新浪播客、网易新闻、腾讯网、搜狐博客等门户网站频道等,在3w以上的高并发环境下,ngnix处理能力相当于apache的10倍。
Nginx代理服务器
搭配多台Tomcat服务器
即多个SpringBoot容器
,利用负载均衡策略实现tomcat集群
的部署。
SpringBoot容器可以相同,也可以不同
Ngnix通过配置 upstream 节点分发请求,达到负载均衡的效果
官方网址
启动Docker服务 systemctl start docker.service
拉取 nginx 最新镜像 docker pull nginx
运行ngnix docker run -d --name mynginx01 -p 80:80 nginx
启动
start nginx
;
关闭nginx -s stop
;
重启nginx -s reload
;(先启动才能重启)
以linux为例,启动多个相同的容器或者Jar包
容器方式
docker修改容器内ngnix配置文件
简单来说,cp复制一个conf文件
更简单
docker cp /etc/nginx/conf.d/***.conf 96f7f14e99ab:/nginx/conf.d/***.conf
docker stop mynginx
docker start mynginx
locate nginx.conf
#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 端口有别的应用在使用
应用容器引擎
,基于 Go 语言 并遵从Apache2.0协议开源。打包他们的应用
以及依赖包到一个轻量级
、可移植
的容器
中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化
。沙箱机制
,相互之间
不会有任何接口
(类似 iPhone 的 app),更重要的是容器性能开销极低
。apt install docker.io #安装docker
docker -v #查看版本
Docker基本命令
docker镜像: ----类似java中 class
docker容器 : ----类似java中 class new 出来的实例对象
将 SpringBoot 项目打包成 Docker 镜像,其主要通过
Maven plugin
插件来进行构建
。
现在已经升级出现了新的插件
dockerfile-maven-plugin
<plugin>
<groupId>com.spotifygroupId>
<artifactId>dockerfile-maven-pluginartifactId>
<version>1.4.13version>
<executions>
<execution>
<id>defaultid>
<goals>
<goal>buildgoal>
<goal>pushgoal>
goals>
execution>
executions>
<configuration>
<repository>${docker.image.prefix}/${project.artifactId}repository>
<tag>${project.version}tag>
<buildArgs>
<JAR_FILE>${project.build.finalName}.jarJAR_FILE>
buildArgs>
configuration>
plugin>
另外,可以在execution中同时指定build和push目标。当运行mvn package时,会自动执行build目标,构建Docker镜像。
DockerFile 文件需要放置在项目 pom.xml同级目录下
内容如下
FROM java:8
EXPOSE 8080
ARG JAR_FILE
ADD target/${JAR_FILE} /niceyoo.jar
ENTRYPOINT ["java", "-jar","/niceyoo.jar"]
SpringBoot项目构建 docker 镜像并推送到远程仓库
<plugin>
<groupId>com.spotifygroupId>
<artifactId>docker-maven-pluginartifactId>
<version>1.0.0version>
<configuration>
<imageName>10.211.55.4:5000/${project.artifactId}imageName>
<imageTags>
<imageTag>latestimageTag>
imageTags>
<registryUrl>10.211.55.4:5000registryUrl>
<pushImage>truepushImage>
<baseImage>javabaseImage>
<maintainer>niceyoo [email protected]maintainer>
<workdir>/ROOTworkdir>
<cmd>["java","-version"]cmd>
<entryPoint>["java","-jar","${project.build.finalName}.jar"]entryPoint>
<dockerHost>http://10.211.55.4:2375dockerHost>
<resources>
<resource>
<targetPath>/ROOTtargetPath>
<directory>${project.build.directory}directory>
<include>${project.build.finalName}.jarinclude>
resource>
resources>
configuration>
plugin>
执行 mvn package docker:build
,即可完成打包至 docker 镜像中。【使用docker-maven-plugin
】
使用
dockerfile-maven-plugin
Dockerfile 就不一样了,从我们开始编写 Dockerfile 文件 FROM 命令开始,我们就发现,这个必须依赖于Docker,但问题就是,假设我本地跟 Docker 并不在一台机器上,那么我是没法执行 dockerfile 的,如果在本地不安装 docker 环境下,是没法执行打包操作的,那么就可以将代码拉取到 Docker 所在服务器,执行打包操作。
mvn clean package dockerfile:build -Dmaven.test.skip=true
执行 docker images 查看
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