SpringBoot+JPA+SpringSeurity+JWT
目的:使用这个框架主要就是为了解决高并发环境下登陆操作对数据库及服务器的压力,同时能保证安全性;
- 加载时,SpringSecurity定义拦截器和添加两个Fitler;
- 登陆时,登陆成功,通过传入的信息(例如:用户名+密码)authenticationManager.authenticate()进行认证得到Authentication;
- 认证成功JWT根据规则生成Token(Bearer空格 + Token) 存到Header;认证失败直接抛出提示;
- 执行操作时鉴权。拿到Header,如果没拿到就放行了;
- 拿到了后,先判断Token是否失效,然后解析出用户名,角色,执行SecurityContextHolder.getContext().setAuthentication(UsernamePasswordAuthenticationToken);再次设置认证信息;没设置成功报403,最后放行。
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.1-jre</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.2</version>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
User.java
import lombok.Getter;
import lombok.Setter;
import lombok.ToString; import javax.persistence.*; @Entity
@Table(name = "jd_user")
@Getter
@Setter
@ToString
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id; @Column(name = "username")
private String username; @Column(name = "password")
private String password; @Column(name = "role")
private String role;
}
JwtUser.java
import lombok.AllArgsConstructor;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails; import java.util.Collection;
import java.util.Collections; @AllArgsConstructor
@ToString
public class JwtUser implements UserDetails { private Integer id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities; // 写一个能直接使用user创建jwtUser的构造器
public JwtUser(User user) {
id = user.getId();
username = user.getUsername();
password = user.getPassword();
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getRole()));
} @Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
} @Override
public String getPassword() {
return password;
} @Override
public String getUsername() {
return username;
} @Override
public boolean isAccountNonExpired() {
return true;
} @Override
public boolean isAccountNonLocked() {
return true;
} @Override
public boolean isCredentialsNonExpired() {
return true;
} @Override
public boolean isEnabled() {
return true;
} }
LoginUser.java
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter; @Getter
@Setter
public class LoginUser { private String username;
private String password;
private Integer rememberMe; }
UserRepository.java
import com.hz.entity.User;
import org.springframework.data.repository.CrudRepository; public interface UserRepository extends CrudRepository<User, Integer> {
User findByUsername(String username);
}
UserDetailsServiceImpl.java
import com.hz.entity.JwtUser;
import com.hz.entity.User;
import com.hz.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service; /**
* Created by echisan on 2018/6/23
*/
@Service
public class UserDetailsServiceImpl implements UserDetailsService { @Autowired
private UserRepository userRepository; @Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
return new JwtUser(user);
} }
JwtTokenUtils.java
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm; import java.util.Date;
import java.util.HashMap; /**
* Created by echisan on 2018/6/23
*/
public class JwtTokenUtils { public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer "; private static final String SECRET = "jwtsecretdemo";
private static final String ISS = "echisan"; // 角色的key
private static final String ROLE_CLAIMS = "rol"; // 过期时间是3600秒,既是1个小时
private static final long EXPIRATION = 3600L; // 选择了记住我之后的过期时间为7天
private static final long EXPIRATION_REMEMBER = 604800L; // 创建token
public static String createToken(String username,String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
} // 从token中获取用户名
public static String getUsername(String token){
return getTokenBody(token).getSubject();
} // 获取用户角色
public static String getUserRole(String token){
return (String) getTokenBody(token).get(ROLE_CLAIMS);
} // 是否已过期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
} private static Claims getTokenBody(String token){
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}
SecurityConfig.java
import com.hz.exception.JWTAccessDeniedHandler;
import com.hz.exception.JWTAuthenticationEntryPoint;
import com.hz.filter.JWTAuthenticationFilter;
import com.hz.filter.JWTAuthorizationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource; /**
* Created by echisan on 2018/6/23
*/
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
@Qualifier("userDetailsServiceImpl")
private UserDetailsService userDetailsService; @Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
} @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
} @Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/tasks/**").hasRole("ADMIN")
// 测试用资源,需要验证了的用户才能访问
.antMatchers("/tasks/**").authenticated()
// 其他都放行了
.anyRequest().permitAll()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
// 不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint())
.accessDeniedHandler(new JWTAccessDeniedHandler()); //添加无权限时的处理
} @Bean
CorsConfigurationSource corsConfigurationSource() {
final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
return source;
}
}
JWTAuthenticationFilter.java(校验)
import com.hz.entity.JwtUser;
import com.hz.model.LoginUser;
import com.hz.utils.JwtTokenUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; /**
* Created by echisan on 2018/6/23
*/
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter { private ThreadLocal<Integer> rememberMe = new ThreadLocal<>();
private AuthenticationManager authenticationManager; public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
} @Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException { // 从输入流中获取到登录的信息
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (Exception e) {
e.printStackTrace();
return null;
}
} // 成功验证后调用的方法
// 如果验证成功,就生成token并返回
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException, ServletException { JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
System.out.println("jwtUser:" + jwtUser.toString());
boolean isRemember = rememberMe.get() == 1; String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities){
role = authority.getAuthority();
} String token = JwtTokenUtils.createToken(jwtUser.getUsername(), role, isRemember);
// String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
// 返回创建成功的token
// 但是这里创建的token只是单纯的token
// 按照jwt的规定,最后请求的时候应该是 `Bearer token`
response.setHeader("token", JwtTokenUtils.TOKEN_PREFIX + token);
} @Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.getWriter().write("authentication failed, reason: " + failed.getMessage());
}
}
JWTAuthorizationFilter(鉴权)
import com.hz.exception.TokenIsExpiredException;
import com.hz.utils.JwtTokenUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections; /**
* Created by echisan on 2018/6/23
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter { public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
} @Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException { String tokenHeader = request.getHeader(JwtTokenUtils.TOKEN_HEADER);
// 如果请求头中没有Authorization信息则直接放行了
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtils.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有token,则进行解析,并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
} catch (TokenIsExpiredException e) {
//返回json形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + e.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
response.getWriter().flush();
return;
}
super.doFilterInternal(request, response, chain);
} // 这里从token中获取用户信息并新建一个token
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws TokenIsExpiredException {
String token = tokenHeader.replace(JwtTokenUtils.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtils.isExpiration(token);
if (expiration) {
throw new TokenIsExpiredException("token超时了");
} else {
String username = JwtTokenUtils.getUsername(token);
String role = JwtTokenUtils.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
}
return null;
}
}
异常处理1:TokenIsExpiredException.java
/**
* @description: 自定义异常
*/
public class TokenIsExpiredException extends Exception { public TokenIsExpiredException() {
} public TokenIsExpiredException(String message) {
super(message);
} public TokenIsExpiredException(String message, Throwable cause) {
super(message, cause);
} public TokenIsExpiredException(Throwable cause) {
super(cause);
} public TokenIsExpiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
异常处理2:
JWTAuthenticationEntryPoint.java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; /**
* @description:没有携带token或者token无效
*/
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException { response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + authException.getMessage();
response.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}
异常处理3:JWTAccessDeniedHandler.java
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException; /**
* @description:没有访问权限
*/
public class JWTAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
String reason = "统一处理,原因:" + e.getMessage();
httpServletResponse.getWriter().write(new ObjectMapper().writeValueAsString(reason));
}
}
AuthController.java
import com.hz.entity.User;
import com.hz.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; /**
* Created by echisan on 2018/6/23
*/
@RestController
@RequestMapping("/auth")
public class AuthController { @Autowired
private UserRepository userRepository; @Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder; @PostMapping("/register")
public String registerUser(@RequestParam(value = "username") String username, @RequestParam(value = "password") String password) {
User user = new User();
user.setUsername(username);
user.setPassword(bCryptPasswordEncoder.encode(password));
user.setRole("ROLE_USER");
User save = userRepository.save(user);
return save.toString();
}
}
TaskController.java
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*; /**
* Created by echisan on 2018/6/23
*/
@RestController
@RequestMapping("/tasks")
public class TaskController { @GetMapping
public String listTasks(){
return "任务列表";
} @PostMapping
@PreAuthorize("hasRole('ADMIN')")
public String newTasks(){
return "创建了一个新的任务";
} @PutMapping("/{taskId}")
public String updateTasks(@PathVariable("taskId")Integer id){
return "更新了一下id为:"+id+"的任务";
} @DeleteMapping("/{taskId}")
public String deleteTasks(@PathVariable("taskId")Integer id){
return "删除了id为:"+id+"的任务";
}
}
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=root spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
测试:postman
注册URL : http://localhost:8080/auth/register?username=admin1&password=admin1
说明:这一步没有什么重要作用,只是向数据库中注册了一条用户信息;
登陆URL:http://localhost:8080/login(注意Body — raw 手动填写JSON)
说明:Header中加入token
操作的URL:http://localhost:8080/tasks/11
说明:先Authorization的TYPE为NO AUTH(不加token),如图所示,403异常
操作的URL:http://localhost:8080/tasks/11
说明:把Authorization的TYPE为Bearer Auth,如图所示,访问正常
SpringBoot+JPA+SpringSeurity+JWT的更多相关文章
- Springboot Jpa: [mysql] java.sql.SQLException: Duplicate entry 'XXX' for key 'PRIMARY'
前言 1.问题背景 偶尔会出现登录请求出错的情况,一旦失败就会短时间内再也登录不上,更换浏览器或者刷新可能会暂时解决这个问题. 项目运行日志如下: 2022-07-21 09:43:40.946 DE ...
- 补习系列(19)-springboot JPA + PostGreSQL
目录 SpringBoot 整合 PostGreSQL 一.PostGreSQL简介 二.关于 SpringDataJPA 三.整合 PostGreSQL A. 依赖包 B. 配置文件 C. 模型定义 ...
- 【原】无脑操作:IDEA + maven + Shiro + SpringBoot + JPA + Thymeleaf实现基础授权权限
上一篇<[原]无脑操作:IDEA + maven + Shiro + SpringBoot + JPA + Thymeleaf实现基础认证权限>介绍了实现Shiro的基础认证.本篇谈谈实现 ...
- 【原】无脑操作:IDEA + maven + Shiro + SpringBoot + JPA + Thymeleaf实现基础认证权限
开发环境搭建参见<[原]无脑操作:IDEA + maven + SpringBoot + JPA + Thymeleaf实现CRUD及分页> 需求: ① 除了登录页面,在地址栏直接访问其他 ...
- 带着新人学springboot的应用08(springboot+jpa的整合)
这一节的内容比较简单,是springboot和jpa的简单整合,jpa默认使用hibernate,所以本质就是springboot和hibernate的整合. 说实话,听别人都说spring data ...
- SpringBoot系列 - 集成JWT实现接口权限认证
会飞的污熊 2018-01-22 16173 阅读 spring jwt springboot RESTful API认证方式 一般来讲,对于RESTful API都会有认证(Authenticati ...
- springboot+jpa+mysql+redis+swagger整合步骤
springboot+jpa+MySQL+swagger框架搭建好之上再整合redis: 在电脑上先安装redis: 一.在pom.xml中引入redis 二.在application.yml里配置r ...
- springboot+jpa+mysql+swagger整合
Springboot+jpa+MySQL+swagger整合 创建一个springboot web项目 <dependencies> <dependency> < ...
- SpringBoot JPA + H2增删改查示例
下面的例子是基于SpringBoot JPA以及H2数据库来实现的,下面就开始搭建项目吧. 首先看下项目的整体结构: 具体操作步骤: 打开IDEA,创建一个新的Spring Initializr项目, ...
随机推荐
- 一道Postgresql递归树题
转载请注明出处: https://www.cnblogs.com/funnyzpc/p/13698249.html 也是偶然的一次,群友出了一道题考考大家,当时正值疫情最最严重的三月(借口...),披 ...
- Presto 标量函数注册和调用过程简述
在Presto 函数开发一文中已经介绍过如何进行函数开发,本文主要讲述标量函数(Scalar Function)实现之后,是如何在Presto内部进行注册和被调用的.主要讲述标量函数是因为:三类函数的 ...
- Java 审计之XXE篇
Java 审计之XXE篇 0x00 前言 在以前XXE漏洞了解得并不多,只是有一个初步的认识和靶机里面遇到过.下面来 深入了解一下该漏洞的产生和利用. 0x01 XXE漏洞 当程序在解析XML输入时, ...
- Git入门教程,详解Git文件的四大状态
大家好,欢迎来到周一git专题. git clone 在上一篇文章当中我们聊了怎么在github当中创建一个属于自己的项目(repository),简称repo.除了建立自己的repo之外,我们更多的 ...
- 03 Comments in C Programming C编程中的注释
Comments 注释简介 Let's take a quick break from programming and talk about comments. Comments help progr ...
- 如何使用 dotTrace 来诊断 netcore 应用的性能问题
最近在为 Newbe.Claptrap 做性能升级,因此将过程中使用到的 dotTrace 软件的基础用法介绍给各位开发者. Newbe.Claptrap 是一个用于轻松应对并发问题的分布式开发框架. ...
- 微服务 | Spring Cloud(一):从单体SSM 到 Spring Cloud
系列文章目录 微服务 | Spring Cloud(一):从单体SSM 到 Spring Cloud 目录 系列文章目录 前言 单体式架构 微服务架构 优点 缺点 服务发现与弹性扩展 参考 前言 在微 ...
- 初学者的Android移植:在Debian上建立一个稳定的构建环境
介绍 通过在chrooted环境中设置开发环境,避免依赖冲突和沙箱您的Android开发从您的Debian GNU/Linux系统.这是为通配符类别准备的,因为从源代码构建Android似乎没有在其他 ...
- Redis哨兵知识点总结
1.Redis哨兵介绍 sentinal,中文名是哨兵 A.哨兵是redis集群架构中非常重要的一个组件,主要功能如下 集群监控,负责监控redis master和slave进程是否正常工作 消息通知 ...
- 微信小程序适配iPhone X
1.获取设备型号 App({ // 全局数据 globalData: { // 其他数据定义 ... isIPX: false, // 当前设备是否为 iPhone X }, // 小程序启动入口 o ...