Shiro
- Shiro
-
- Shiro理论
-
- 什么是shiro
- 功能
- 组件
- shiro注解
- shiro的优点
- 运行流程
- 前端标签
-
- jsp页面
- Thymeleaf
- 在spring框架中集成shiro
-
- Pom文件进行依赖配置
- 集成Shiro
-
- web.xml中的设置
- shiro的bean配置文件,
- Spring Boot Shiro
-
- POM文件
- 在application.properties设置shiro配置
- MyRealm.java
- ShiroConfig.java
- 身份验证
-
- Role 配合页面标签,控制不同用户访问不同页面
- Resource 配合控制器注解,控制某个特殊的授权权限
Shiro Shiro理论 什么是shiro
Shiro是一个强大易用的java安全,提供了认证、授权、加密、会话管理、与web集成、缓存等功能,对于任何一个应用程序,都可以提供全面的安全服务,相比其他安全框架,shiro要简单的多。
功能
- 身份验证(核心功能);
- 资源授权(核心功能);
- 密码加密(非核心功能);
- 会话管理(非核心功能);
- Remember Me(非核心功能);
- Subject:应用层和shiro框架交互的对象
- Realm:实现身份验证和资源授权的核心组件:域,shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
- RememberMeManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是shiro的核心, SecurityManager相当于spring mvc中的dispatcherServlet前端控制器。
- SimpleCookie:记住我——Cookie;
- CookieRememberMeManager:记住我—— Cookie 管理器;
- SessionManager
- SimpleCookie:会话管理—— Cookie;
- DefaultWebSessionManager:会话配置;
- SecurityManager:安全管理器,它负责与 Shiro 的其他组件进行交互,需要注入 Realm、RememberMeManager、SessionManager 等组件,且管理着所有的 Subject;
- ShiroFilterFactoryBean:Shiro 过滤器工厂,注入 SecurityManager、定义过滤器、指定路径拦截规则等;
- ShiroDialect:Shiro 方言,支持 Thymeleaf 页面 Shiro 标签;
- DefaultAdvisorAutoProxyCreator:Advisor 代理类生成器;
- AuthorizationAttributeSourceAdvisor:创建 Advisor 代理类,扫描 Shiro 注解;
- 应用代码通过 Subject 来进行认证和授权,Subject 委托给 SecurityManager,我们需要给 Shiro 的 SecurityManager 注入 Realm,从而让 SecurityManager 能对用户及其权限进行判断;
文章图片
shiro注解
- @RequiresAuthentication : 表示当前 Subject 需要登录;
- @RequiresUser : 表示当前 Subject 需要登录或记住我;
- @RequiresGuest : 表示当前 Subject 是游客身份;
- @RequiresRoles(value=https://www.it610.com/article/{“admin”, “user”}, logical=Logical.AND):当前 Subject 需要的角色;
- @RequiresPermissions (value=https://www.it610.com/article/{““,””}, logical= Logical.OR) :当前 Subject 需要拥有的资源;
既可以用在controller中,也可以用在service中建议将shiro注解放入controller,因为如果service层使用了spring的事物注解,那么shiro注解将无效shiro的优点
1、 简单的身份验证,支持多种数据源
2、对角色的简单授权,支持细粒度的授权(方法)
3、支持一级缓存,以提升应用程序的性能
4、内置基于POJO的企业会话管理,适用于web及非web环境
5、非常简单的API加密
6、不跟任何框架绑定,可以独立运行
运行流程
- 首先调用
Subject.login(token)
进行登录,其会自动委托给Security Manager
,调用之前必须通过SecurityUtils.setSecurityManager()
设置; SecurityManager
负责真正的身份验证逻辑;它会委托给Authenticator
进行身份验证;Authenticator
才是真正的身份验证者,Shiro API
中核心的身份认证入口点,此处可以自定义插入自己的实现;Authenticator
可能会委托给相应的AuthenticationStrategy
进行多 Realm 身份验证,默认ModularRealmAuthenticator
会调用AuthenticationStrategy
进行多 Realm 身份验证;Authenticator
会把相应的token
传入Realm
,从Realm
获取身份验证信息,如果没有返回 则抛出异常表示身份验证失败了。此处可以配置多个Realm
,将按照相应的顺序及策略进行访问。
jsp页面
首先要导入标签库;
:导入标签库;:游客访问;:用户需要登录或记住我;:用户需要登录;:用户需要拥有某种角色访问;:用户需要拥有某些角色;:用户需要拥有某资源;进行显示
Thymeleaf
导入shiro:guest:游客访问;
shiro:user:用户需要登录或记住我;
shiro:authenticated:用户需要登录;
shiro:hasRole:用户需要某角色;
shiro:hasAnyRoles:用户需要某些角色;
shiro:hasPermission:用户需要某权限;
-----------------------------------------------------------
:用户需要 admin 或 manager 角色可访问; shiro:principal/> :身份验证器包装的用户名时,页面获取用户名;shiro:principal property="loginName"/>:身份验证器中包装 user 对象时,页面获取用户名;:页面获取 session 中 user 对象;
在spring框架中集成shiro Pom文件进行依赖配置
可以在maven仓库包https://mvnrepository.com/,根据三要素进行查找
>1.7.1
org.apache.shiro
shiro-core
${shiro.version}
org.apache.shiro
shiro-spring
${shiro.version}
org.apache.shiro
shiro-web
${shiro.version}
org.apache.shiro
shiro-aspectj
${shiro.version}
org.apache.shiro
shiro-ehcache
${shiro.version}
项目跑起来了,只是控制台输出了
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
需要配置日志包
shiro使用slf4j作为日志框架,所以必需配置slf4j。同时,使用log4j作为底层的日志实现框架。
org.slf4j
slf4j-api
1.7.25
org.slf4j
slf4j-log4j12
1.7.25
log4j
log4j
1.2.17
集成Shiro
在Spring框架中集成Shiro,本质上是与Spring IoC容器和Spring MVC框架集成.
web.xml中的设置
shiroFilter
org.springframework.web.filter.DelegatingFilterProxy
targetFilterLifecycle
true
shiroFilter
/*
shiro的bean配置文件, 其中包含过滤设置。此文件名为spring-shiro.xml,里面定义了需要的Bean,完成诸多功能。
/login/view = anon
/back/student/zzz =authc,roles[admin]
/back/** = authc
或者:springShiro.xml
/static/**=anon/register=anon/login=anon/forgot=anon/logout=logout/api/**=anon/**=authc
必须是在MVC配置文件中用
将spring-shiro.xml载入。
【#|Shiro学习与笔记】
文章图片
例子:
- 身份验证
UserRealm
package com.qq.realm; import com.qq.model.User; import com.qq.service.UserService; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; /** * @description:com.qq.realm_studyssm * @author: 霏宇 * @time: 2022/8/5,9:55 */ public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; //授权: @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); String name =(String)principalCollection.getPrimaryPrincipal(); if (name.equals("admin")){ info.addStringPermission("admin"); }else if (name.equals("lisi")){ info.addStringPermission("worker"); info.addRole("admin"); } return info; } /** * 认证 * @param token * @return * @throws AuthenticationException */@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //一旦此用户不存在就主动抛出异常 UsernamePasswordToken upToken = (UsernamePasswordToken)token; String name =upToken.getUsername(); String password=new String(upToken.getPassword()); User user =userService.selectByName(name); if(null==user){ throw new UnknownAccountException("你输入的账户不存在!"); }else { User user2 =userService.selectByNamePassword(name,password); if(null==user2){ throw new IncorrectCredentialsException("密码错误"); }else { returnnew SimpleAuthenticationInfo(token.getPrincipal(),token.getCredentials(),this.getName()); } }//if(!name.equals("list")&&!name.equals("admin")){ //throw new UnknownAccountException("你输入的账户不存在!"); //}else { ////这里假设系统的用户只有两个 (zhangsan,123456)、(lisi,888999) //if(name.equals("list")&&password.equals("789") ||name.equals("admin")&&password.equals("123456")){ // //returnnew SimpleAuthenticationInfo(token.getPrincipal(),token.getCredentials(),this.getName()); // //}else { //throw new IncorrectCredentialsException("用户不存在"); //} // //}} }
或者:MyRealm.java
public class MyRealm extends AuthorizingRealm {@Autowiredprivate UserService userService; @Autowiredprivate RoleService roleService; @Autowiredprivate ResourceService resourceService; /*** -资源授权器*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {// 授权类SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); User user = (User) principals.getPrimaryPrincipal(); if (user == null) {return null; }List
roles = Optional.ofNullable(roleService.getRolesByUserId(user.getId())).orElse(Collections.emptyList()); roles.stream().forEach(role -> {authorizationInfo.addRole(role.getRoleName()); List resources = Optional.ofNullable(resourceService.getResourcesByRoleId(role.getId())).orElse(Collections.emptyList()); resources.stream().forEach(resource -> {authorizationInfo.addStringPermission(resource.getPermission()); }); }); return authorizationInfo; }/*** -身份验证器*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {String userName = (String) token.getPrincipal(); User user = userService.getUserByUserName(userName); if (user == null) {throw new UnknownAccountException("The account do not exist."); }// realmName: 当前 realm 对象的唯一名字. 调用父类的 getName() 方法return new SimpleAuthenticationInfo(user, user.getPassword(), getName()); }}
LoginController
package com.qq.controller; import com.qq.model.User; import com.qq.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import javax.servlet.http.HttpSession; /** * @description:com.qq.controller_studyssm * @author: 霏宇 * @time: 2022/8/4,14:02 */ @Controller @RequestMapping("/login") public class LoginController { @Autowired UserService userService; @RequestMapping("/view") String index(){ return "login/view"; }@RequestMapping("/enter") String enter(String name, String password, HttpSession session, Model model){Subject subject = SecurityUtils.getSubject(); // UsernamePasswordToken token = new UsernamePasswordToken(name, password); //对(用户名、密码)登录方式,用UsernamePasswordToken封装即可 try { subject.login(token); //调用login()方法。 有可能遭遇两种异常,代表登陆失败。 User user =userService.selectByNamePassword(name,password); session.setAttribute("USER",user); } catch (UnknownAccountException e) { e.printStackTrace(); model.addAttribute("message", "用户名错误!"); return "redirect:view"; } catch (IncorrectCredentialsException e) { e.printStackTrace(); model.addAttribute("message", "密码错误"); return "redirect:view"; }System.out.println(subject.isAuthenticated()); //打印下看看是否已认证成功return "redirect:/back/hui/index"; //User user = userService.selectByNamePassword(name, password); //if(user==null){ // //return "redirect:index"; //}else { //session.setAttribute("USER", user); //return "redirect:/back/book/index"; //}} @RequestMapping("/logout") String logout(HttpSession session){ session.removeAttribute("USER"); Subject subject =SecurityUtils.getSubject(); subject.logout(); // session.invalidate(); return "redirect:view"; }}
或者在
UserService
public Result
login(User user);
```java
@Service
public class UserServiceImpl implements UserService{@Overridepublic ResultEntity login(User user) {Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(user.getUserName(), MD5Util.getMD5(user.getPassword()));
token.setRememberMe(user.getRememberMe());
return new Result(Result.ResultStatus.SUCCESS.code, "Success", user);
try {subject.login(token);
subject.checkRoles();
} catch (Exception e) {e.printStackTrace();
return new Result(Result.ResultStatus.FAILD.code, e.getMessage());
}Session session = subject.getSession();
session.setAttribute("user", subject.getPrincipal());
}@GetMapping("/logout")public String logout() {
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login";
}}
```
Spring Boot Shiro POM文件
org.apache.shiro
shiro-core
1.4.0
org.apache.shiro
shiro-spring
1.4.0
org.springframework.boot
spring-boot-starter-thymeleaf
com.github.theborakompanioni
thymeleaf-extras-shiro
2.0.0
在application.properties设置shiro配置
#for shiro
#开启Shiro配置,默认为true
shiro.web.enabled=true#server.servlet.context-path=/shiro
#spring.application.name=shiro
MyRealm.java
package com.feiyu.sprtingboot.config.shiro;
import com.feiyu.sprtingboot.modules.account.entity.Resource;
import com.feiyu.sprtingboot.modules.account.entity.Role;
import com.feiyu.sprtingboot.modules.account.entity.User;
import com.feiyu.sprtingboot.modules.account.service.ResourceService;
import com.feiyu.sprtingboot.modules.account.service.RoleService;
import com.feiyu.sprtingboot.modules.account.service.UserService;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* @description:com.feiyu.sprtingboot.config.shiro_sprtingboot
* @author: 霏宇
* @time: 2022/8/31,9:41
*/
@Component
public class MyRealmextends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private ResourceService resourceService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {SimpleAuthorizationInfo authorization = new SimpleAuthorizationInfo();
// 从认证中获取当前的信息
User user = (User) principals.getPrimaryPrincipal();
// 从数据库查询当前用户的角色列表,并装载到资源授权器里
List roles = roleService.getRolesByUserId(user.getId());
roles.stream().forEach(item -> {
authorization.addRole(item.getRoleName());
//authorization.addStringPermission(item.getRoleName());
// 再去查询每个角色拥有的资源列表,并装载到资源授权器里
List resources = resourceService.getResourcesByRoleId(item.getId());
resources.stream().forEach(it -> {
authorization.addStringPermission(it.getPermission());
System.out.println(it.getPermission());
});
});
return authorization;
}@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
// 获取用户名
String userName = (String) token.getPrincipal();
// 通过用户名查找数据库的 user 信息
User user = userService.getUserByUserName(userName);
if (user == null) {
throw new UnknownAccountException("User name is not exit.");
}// 封装身份验证器
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
}
ShiroConfig.java
package com.feiyu.sprtingboot.config.shiro;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
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.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @description:com.feiyu.sprtingboot.config.shiro_sprtingboot
* @author: 霏宇
* @time: 2022/8/31,10:21
*/
@Configuration
public class ShiroConfig {
@Autowired
private MyRealm myRealm;
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
return securityManager;
}/**
* anon:匿名访问,无需登录 ---- AnonymousFilter
* authc:登录后才能访问 ---- FormAuthenticationFilter
* user:登录过能访问 ---- UserFilter
* logout:登出 ---- LogoutFilter
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean() {
ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
// 注入安全管理器
filterFactory.setSecurityManager(securityManager());
// 设置登录页面、登录成功页面
filterFactory.setLoginUrl("/login");
filterFactory.setSuccessUrl("/test/thymeleafTest");
// 设置其余地址的访问策略
Map filterMap = new LinkedHashMap<>();
// 匿名策略
// 登录注册
filterMap.put("/login", "anon");
filterMap.put("/register", "anon");
// 静态资源
filterMap.put("/favicon.ico", "anon");
filterMap.put("/css/**", "anon");
filterMap.put("/images/**", "anon");
filterMap.put("/js/**", "anon");
filterMap.put("/vendors/**", "anon");
filterMap.put("/static/**", "anon");
// 测试模块
filterMap.put("/test/**", "anon");
// api
filterMap.put("/api/**", "anon");
// 非匿名策略
filterMap.put("/**", "authc");
filterFactory.setFilterChainDefinitionMap(filterMap);
return filterFactory;
}/**
* - 注册shiro方言,让 thymeleaf 支持 shiro 标签
*/
@Bean
public ShiroDialect shiroDialect(){
return new ShiroDialect();
}@Bean(name="lifecycleBeanPostProcessor")
public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}/**
* DefaultAdvisorAutoProxyCreator, Advisor 代理类生成器
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
}/**
* - 创建 AuthorizationAttributeSourceAdvisor,扫描 Shiro 注解
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(){
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}}
如果需要加入rememberMeCookie
/**
* -- Remember Me Cookie
*/
@Beanpublic SimpleCookie rememberMeCookie() {//这个参数是 cookie 的名称SimpleCookie simpleCookie = new SimpleCookie("rememberMe");
//如果 httyOnly 设置为 true,则客户端不会暴露给客户端脚本代码,//使用 HttpOnly cookie 有助于减少某些类型的跨站点脚本攻击;simpleCookie.setHttpOnly(true);
//记住我 cookie 生效时间,单位是秒simpleCookie.setMaxAge(1 * 24 * 60 * 60);
return simpleCookie;
}/**
* -- 管理器
*/@Bean
public CookieRememberMeManager rememberMeManager() {CookieRememberMeManager cookieRememberMeManager = new CookieRememberMeManager();
cookieRememberMeManager.setCookie(rememberMeCookie());
byte[] cipherKey = Base64.decode("wGiHplamyXlVB11UXWol8g==");
cookieRememberMeManager.setCipherService(new AesCipherService());
cookieRememberMeManager.setCipherKey(cipherKey);
return cookieRememberMeManager;
}/*** sessionCookie*/@Bean
public SimpleCookie sessionCookie() {SimpleCookie simpleCookie = new SimpleCookie("shiro.sesssion");
simpleCookie.setPath("/");
simpleCookie.setHttpOnly(true);
simpleCookie.setMaxAge(1 * 24 * 60 * 60);
return simpleCookie;
}
在DefaultWebSessionManager 加入
/**
* DefaultAdvisorAutoProxyCreator, Advisor 代理类生成器
Shiro 默认 Cookie 名称是 JSESSIONID,与 Tomcat 等默认JSESSIONID 冲突,我们需要为
Shiro 指定一个不同名称的 Session id,否则抛出 UnknownSessionException: There is no session with id 异常
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
sessionManager.setSessionIdCookie(sessionCookie());
advisorAutoProxyCreator.setProxyTargetClass(true);
// 相隔多久检查一次 session 的有效性
// sessionManager.setSessionValidationInterval(1 * 24 * 60 * 60 * 1000);
// session 有效时间
// sessionManager.setGlobalSessionTimeout(1 * 24 * 60 * 60 * 1000);
return advisorAutoProxyCreator;
}在SecurityManager 加入
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myRealm);
securityManager.setRememberMeManager(rememberMeManager());
securityManager.setSessionManager(sessionManager());
return securityManager;
}
身份验证
UserServiceImpl
@Override
public Result login(User user) {// 得到 subject
Subject subject = SecurityUtils.getSubject();
// 封装一个登录令牌(装载用户名和密码)
UsernamePasswordToken token = new UsernamePasswordToken(
user.getUserName(),
MD5Util.encode(user.getPassword()));
try {
// subject.login()
subject.login(token);
subject.checkRoles();
// 获取当前用户,并将之设置到 session 中
User temp = (User) subject.getPrincipal();
Session session = subject.getSession();
session.setAttribute("user", temp);
return new Result(Result.ResultStatus.SUCCESS.code, "Success", temp);
} catch (Exception e) {
e.printStackTrace();
LOGGER.debug(e.getMessage());
return new Result(Result.ResultStatus.FAILD.code, e.getMessage());
}}
Role 配合页面标签,控制不同用户访问不同页面
/**
* 127.0.0.1/account/users---- get
*/
@RequiresRoles(value = https://www.it610.com/article/{"admin","manager"},logical = Logical.OR)
@GetMapping(value = "https://www.it610.com/account/users")
public String userPage(ModelMap modelMap) {
modelMap.addAttribute("template",
"account/users");
return "index";
}
Resource 配合控制器注解,控制某个特殊的授权权限
@RequiresPermissions("/api/user/{id}")
public Result
Html页面
文章图片
部分转载于http://www.sfac.xyz:8000/notes/Java/Apache_Shiro.html
推荐阅读
- java|【JAVA】多线程篇章之进程与线程的区别与联系【面试热问题】
- 数据结构与算法|哈工大《机器学习》最小二乘法曲线拟合——实验一
- 队列|数字人技术在直播场景下的应用
- 编程语言|Node 之父斥责 Oracle(你们也不用,那请交出 JavaScript 商标!)
- 分布式|随笔(分布式锁的一点思想)
- 错误|记录(There is no getter for property named ‘null‘ in ‘class)
- SQL|MVCC在重复读和读已提交场景以及幻读的解决
- spring|SpringCloudAlibaba+Vue2.0搭建博客项目
- #|「SpringCloud」08 Config分布式配置中心