Spring Boot整合Spring Security簡單實(shí)現(xiàn)登入登出從零搭建教程
前言
Spring Security是一個(gè)能夠?yàn)榛赟pring的企業(yè)應(yīng)用系統(tǒng)提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應(yīng)用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉(zhuǎn)Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,為應(yīng)用系統(tǒng)提供聲明式的安全訪問控制功能,減少了為企業(yè)系統(tǒng)安全控制編寫大量重復(fù)代碼的工作。
本文主要給大家介紹了關(guān)于Spring Boot整合Spring Security實(shí)現(xiàn)登入登出的相關(guān)內(nèi)容,下面話不多說了,來一起看看詳細(xì)的介紹吧
技術(shù)棧 : SpringBoot + SpringSecurity + jpa + freemark ,完整項(xiàng)目地址 : https://github.com/EalenXie/spring-security-login (本地下載)
方法如下:
1 . 新建一個(gè)spring-security-login的maven項(xiàng)目 ,pom.xml添加基本依賴 :
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.wuxicloud</groupId> <artifactId>spring-security-login</artifactId> <version>1.0</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.6.RELEASE</version> </parent> <properties> <author>EalenXie</author> <description>SpringBoot整合SpringSecurity實(shí)現(xiàn)簡單登入登出</description> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</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-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!--alibaba--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.24</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.31</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies> </project>
2 . 準(zhǔn)備你的數(shù)據(jù)庫,設(shè)計(jì)表結(jié)構(gòu),要用戶使用登入登出,新建用戶表。
DROP TABLE IF EXISTS `user`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_uuid` varchar(70) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `telephone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `role` int(10) DEFAULT NULL, `image` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `last_ip` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, `last_time` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact; SET FOREIGN_KEY_CHECKS = 1;
3 . 用戶對(duì)象User.java :
import javax.persistence.*;
/**
* Created by EalenXie on 2018/7/5 15:17
*/
@Entity
@Table(name = "USER")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private String user_uuid; //用戶UUID
private String username; //用戶名
private String password; //用戶密碼
private String email; //用戶郵箱
private String telephone; //電話號(hào)碼
private String role; //用戶角色
private String image; //用戶頭像
private String last_ip; //上次登錄IP
private String last_time; //上次登錄時(shí)間
public Integer getId() {
return id;
}
public String getRole() {
return role;
}
public void setRole(String role) {
this.role = role;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getTelephone() {
return telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getUser_uuid() {
return user_uuid;
}
public void setUser_uuid(String user_uuid) {
this.user_uuid = user_uuid;
}
public String getLast_ip() {
return last_ip;
}
public void setLast_ip(String last_ip) {
this.last_ip = last_ip;
}
public String getLast_time() {
return last_time;
}
public void setLast_time(String last_time) {
this.last_time = last_time;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", user_uuid='" + user_uuid + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", telephone='" + telephone + '\'' +
", role='" + role + '\'' +
", image='" + image + '\'' +
", last_ip='" + last_ip + '\'' +
", last_time='" + last_time + '\'' +
'}';
}
}
4 . application.yml配置一些基本屬性
spring: resources: static-locations: classpath:/ freemarker: template-loader-path: classpath:/templates/ suffix: .html content-type: text/html charset: UTF-8 datasource: url: jdbc:mysql://localhost:3306/yourdatabase username: yourname password: yourpass driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource server: port: 8083 error: whitelabel: enabled: true
5 . 考慮我們應(yīng)用的效率 , 可以配置數(shù)據(jù)源和線程池 :
package com.wuxicloud.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.*;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
@Configuration
public class DruidConfig {
private static final String DB_PREFIX = "spring.datasource.";
@Autowired
private Environment environment;
@Bean
@ConfigurationProperties(prefix = DB_PREFIX)
public DataSource druidDataSource() {
Properties dbProperties = new Properties();
Map<String, Object> map = new HashMap<>();
for (PropertySource<?> propertySource : ((AbstractEnvironment) environment).getPropertySources()) {
getPropertiesFromSource(propertySource, map);
}
dbProperties.putAll(map);
DruidDataSource dds;
try {
dds = (DruidDataSource) DruidDataSourceFactory.createDataSource(dbProperties);
dds.init();
} catch (Exception e) {
throw new RuntimeException("load datasource error, dbProperties is :" + dbProperties, e);
}
return dds;
}
private void getPropertiesFromSource(PropertySource<?> propertySource, Map<String, Object> map) {
if (propertySource instanceof MapPropertySource) {
for (String key : ((MapPropertySource) propertySource).getPropertyNames()) {
if (key.startsWith(DB_PREFIX))
map.put(key.replaceFirst(DB_PREFIX, ""), propertySource.getProperty(key));
else if (key.startsWith(DB_PREFIX))
map.put(key.replaceFirst(DB_PREFIX, ""), propertySource.getProperty(key));
}
}
if (propertySource instanceof CompositePropertySource) {
for (PropertySource<?> s : ((CompositePropertySource) propertySource).getPropertySources()) {
getPropertiesFromSource(s, map);
}
}
}
@Bean
public ServletRegistrationBean druidServlet() {
return new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
}
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new WebStatFilter());
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");
return filterRegistrationBean;
}
}
配置線程池 :
package com.wuxicloud.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean
public Executor getExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);//線程池維護(hù)線程的最少數(shù)量
executor.setMaxPoolSize(30);//線程池維護(hù)線程的最大數(shù)量
executor.setQueueCapacity(8); //緩存隊(duì)列
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //對(duì)拒絕task的處理策略
executor.setKeepAliveSeconds(60);//允許的空閑時(shí)間
executor.initialize();
return executor;
}
}
6.用戶需要根據(jù)用戶名進(jìn)行登錄,訪問數(shù)據(jù)庫 :
import com.wuxicloud.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Created by EalenXie on 2018/7/11 14:23
*/
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}
7.構(gòu)建真正用于SpringSecurity登錄的安全用戶(UserDetails),我這里使用新建了一個(gè)POJO來實(shí)現(xiàn) :
package com.wuxicloud.security;
import com.wuxicloud.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
public class SecurityUser extends User implements UserDetails {
private static final long serialVersionUID = 1L;
public SecurityUser(User user) {
if (user != null) {
this.setUser_uuid(user.getUser_uuid());
this.setUsername(user.getUsername());
this.setPassword(user.getPassword());
this.setEmail(user.getEmail());
this.setTelephone(user.getTelephone());
this.setRole(user.getRole());
this.setImage(user.getImage());
this.setLast_ip(user.getLast_ip());
this.setLast_time(user.getLast_time());
}
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
String username = this.getUsername();
if (username != null) {
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(username);
authorities.add(authority);
}
return authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
8 . 核心配置,配置SpringSecurity訪問策略,包括登錄處理,登出處理,資源訪問,密碼基本加密。
package com.wuxicloud.config;
import com.wuxicloud.dao.UserRepository;
import com.wuxicloud.model.User;
import com.wuxicloud.security.SecurityUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* Created by EalenXie on 2018/1/11.
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
@Override
protected void configure(HttpSecurity http) throws Exception { //配置策略
http.csrf().disable();
http.authorizeRequests().
antMatchers("/static/**").permitAll().anyRequest().authenticated().
and().formLogin().loginPage("/login").permitAll().successHandler(loginSuccessHandler()).
and().logout().permitAll().invalidateHttpSession(true).
deleteCookies("JSESSIONID").logoutSuccessHandler(logoutSuccessHandler()).
and().sessionManagement().maximumSessions(10).expiredUrl("/login");
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
auth.eraseCredentials(false);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() { //密碼加密
return new BCryptPasswordEncoder(4);
}
@Bean
public LogoutSuccessHandler logoutSuccessHandler() { //登出處理
return new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
try {
SecurityUser user = (SecurityUser) authentication.getPrincipal();
logger.info("USER : " + user.getUsername() + " LOGOUT SUCCESS ! ");
} catch (Exception e) {
logger.info("LOGOUT EXCEPTION , e : " + e.getMessage());
}
httpServletResponse.sendRedirect("/login");
}
};
}
@Bean
public SavedRequestAwareAuthenticationSuccessHandler loginSuccessHandler() { //登入處理
return new SavedRequestAwareAuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
User userDetails = (User) authentication.getPrincipal();
logger.info("USER : " + userDetails.getUsername() + " LOGIN SUCCESS ! ");
super.onAuthenticationSuccess(request, response, authentication);
}
};
}
@Bean
public UserDetailsService userDetailsService() { //用戶登錄實(shí)現(xiàn)
return new UserDetailsService() {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
User user = userRepository.findByUsername(s);
if (user == null) throw new UsernameNotFoundException("Username " + s + " not found");
return new SecurityUser(user);
}
};
}
}
9.至此,已經(jīng)基本將配置搭建好了,從上面核心可以看出,配置的登錄頁的url 為/login,可以創(chuàng)建基本的Controller來驗(yàn)證登錄了。
package com.wuxicloud.web;
import com.wuxicloud.model.User;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
* Created by EalenXie on 2018/1/11.
*/
@Controller
public class LoginController {
@RequestMapping(value = "/login", method = RequestMethod.GET)
public String login() {
return "login";
}
@RequestMapping("/")
public String root() {
return "index";
}
public User getUser() { //為了session從獲取用戶信息,可以配置如下
User user = new User();
SecurityContext ctx = SecurityContextHolder.getContext();
Authentication auth = ctx.getAuthentication();
if (auth.getPrincipal() instanceof UserDetails) user = (User) auth.getPrincipal();
return user;
}
public HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
}
11 . SpringBoot基本的啟動(dòng)類 Application.class
package com.wuxicloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Created by EalenXie on 2018/7/11 15:01
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
11.根據(jù)Freemark和Controller里面可看出配置的視圖為 /templates/index.html和/templates/index.login。所以創(chuàng)建基本的登錄頁面和登錄成功頁面。
login.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>用戶登錄</title> </head> <body> <form action="/login" method="post"> 用戶名 : <input type="text" name="username"/> 密碼 : <input type="password" name="password"/> <input type="submit" value="登錄"> </form> </body> </html>
注意 : 這里方法必須是POST,因?yàn)镚ET在controller被重寫了,用戶名的name屬性必須是username,密碼的name屬性必須是password
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首頁</title>
<#assign user=Session.SPRING_SECURITY_CONTEXT.authentication.principal/>
</head>
<body>
歡迎你,${user.username}<br/>
<a href="/logout">注銷</a>
</body>
</html>
注意 : 為了從session中獲取到登錄的用戶信息,根據(jù)配置SpringSecurity的用戶信息會(huì)放在Session.SPRING_SECURITY_CONTEXT.authentication.principal里面,根據(jù)FreeMarker模板引擎的特點(diǎn),可以通過這種方式進(jìn)行獲取 : <#assign user=Session.SPRING_SECURITY_CONTEXT.authentication.principal/>
12 . 為了方便測試,我們?cè)跀?shù)據(jù)庫中插入一條記錄,注意,從WebSecurity.java配置可以知道密碼會(huì)被加密,所以我們插入的用戶密碼應(yīng)該是被加密的。
這里假如我們使用的密碼為admin,則加密過后的字符串是 $2a$04$1OiUa3yEchBXQBJI8JaMyuKZNlwzWvfeQjKAHnwAEQwnacjt6ukqu
測試類如下 :
package com.wuxicloud.security;
import org.junit.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Created by EalenXie on 2018/7/11 15:13
*/
public class TestEncoder {
@Test
public void encoder() {
String password = "admin";
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4);
String enPassword = encoder.encode(password);
System.out.println(enPassword);
}
}
測試登錄,從上面的加密的密碼我們插入一條數(shù)據(jù)到數(shù)據(jù)庫中。
INSERT INTO `USER` VALUES (1, 'd242ae49-4734-411e-8c8d-d2b09e87c3c8', 'EalenXie', '$2a$04$petEXpgcLKfdLN4TYFxK0u8ryAzmZDHLASWLX/XXm8hgQar1C892W', 'SSSSS', 'ssssssssss', 1, 'g', '0:0:0:0:0:0:0:1', '2018-07-11 11:26:27');
13 . 啟動(dòng)項(xiàng)目進(jìn)行測試 ,訪問 localhost:8083

點(diǎn)擊登錄,登錄失敗會(huì)留在當(dāng)前頁面重新登錄,成功則進(jìn)入index.html
登錄如果成功,可以看到后臺(tái)打印登錄成功的日志 :

頁面進(jìn)入index.html :

點(diǎn)擊注銷 ,則回重新跳轉(zhuǎn)到login.html,后臺(tái)也會(huì)打印登出成功的日志 :

總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Springboot使用redis進(jìn)行api防刷限流過程詳解
這篇文章主要介紹了Springboot使用redis進(jìn)行api防刷限流過程詳解,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
簡單了解Spring Cloud搭建Config過程實(shí)例
這篇文章主要介紹了簡單了解Spring Cloud搭建Config過程實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
Java Swing GroupLayout分組布局的實(shí)現(xiàn)代碼
這篇文章主要介紹了Java Swing GroupLayout分組布局的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12
Lombok使用@Tolerate實(shí)現(xiàn)沖突兼容問題
這篇文章主要介紹了Lombok使用@Tolerate實(shí)現(xiàn)沖突兼容問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
解決springboot 2.x集成log4j2調(diào)試日志無法關(guān)閉的問題
這篇文章主要介紹了解決springboot 2.x集成log4j2調(diào)試日志無法關(guān)閉的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
spring利用squertz實(shí)現(xiàn)定時(shí)任務(wù)
spring squertz是一個(gè)強(qiáng)大的定時(shí)任務(wù)處理方式。下面這篇文章主要介紹了spring利用squertz實(shí)現(xiàn)定時(shí)任務(wù)的相關(guān)資料,文中介紹的很詳細(xì),對(duì)大家具有一定的參考借鑒價(jià)值,需要的朋友們下面來一起看看吧。2017-01-01

