Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
行展示
主要展示 Spring Security 與 JWT 結(jié)合使用構(gòu)建后端 API 接口。
主要功能包括登陸(如何在 Spring Security 中添加驗證碼登陸),查找,創(chuàng)建,刪除并對用戶權(quán)限進(jìn)行區(qū)分等等。
ps:由于只是 Demo,所以沒有調(diào)用數(shù)據(jù)庫,以上所說增刪改查均在 HashMap 中完成。
展示如何使用 Vue 構(gòu)建前端后與后端的配合,包括跨域的設(shè)置,前端登陸攔截
并實現(xiàn) POST,GET,DELETE 請求。包括如何在 Vue 中使用后端的 XSRF-TOKEN 防范 CSRF 攻擊
實現(xiàn)細(xì)節(jié)
創(chuàng)建 Spring boot 項目,添加 JJWT 和 Spring Security 的項目依賴,這個非常簡單,有很多的教程都有塊內(nèi)容,唯一需要注意的是,如果你使用的 Java 版本是 11,那么你還需要添加以下依賴,使用 Java8 則不需要。
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
要使用 Spring Security 實現(xiàn)對用戶的權(quán)限控制,首先需要實現(xiàn)一個簡單的 User 對象實現(xiàn) UserDetails 接口,UserDetails 接口負(fù)責(zé)提供核心用戶的信息,如果你只需要用戶登陸的賬號密碼,不需要其它信息,如驗證碼等,那么你可以直接使用 Spring Security 默認(rèn)提供的 User 類,而不需要自己實現(xiàn)。
public class User implements UserDetails {
private String username;
private String password;
private Boolean rememberMe;
private String verifyCode;
private String power;
private Long expirationTime;
private List<GrantedAuthority> authorities;
/**
* 省略其它的 get set 方法
*/
@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;
}
}
User
這個就是我們要使用到的 User 對象,其中包含了 記住我,驗證碼等登陸信息,因為 Spring Security 整合 Jwt 本質(zhì)上就是用自己自定義的登陸過濾器,去替換 Spring Security 原生的登陸過濾器,這樣的話,原生的記住我功能就會無法使用,所以我在 User 對象里添加了記住我的信息,用來自己實現(xiàn)這個功能。
首先我們來新建一個 TokenAuthenticationHelper 類,用來處理認(rèn)證過程中的驗證和請求
public class TokenAuthenticationHelper {
/**
* 未設(shè)置記住我時 token 過期時間
* */
private static final long EXPIRATION_TIME = 7200000;
/**
* 記住我時 cookie token 過期時間
* */
private static final int COOKIE_EXPIRATION_TIME = 1296000;
private static final String SECRET_KEY = "ThisIsASpringSecurityDemo";
public static final String COOKIE_TOKEN = "COOKIE-TOKEN";
public static final String XSRF = "XSRF-TOKEN";
/**
* 設(shè)置登陸成功后令牌返回
* */
public static void addAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
// 獲取用戶登陸角色
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
// 遍歷用戶角色
StringBuffer stringBuffer = new StringBuffer();
authorities.forEach(authority -> {
stringBuffer.append(authority.getAuthority()).append(",");
});
long expirationTime = EXPIRATION_TIME;
int cookExpirationTime = -1;
// 處理登陸附加信息
LoginDetails loginDetails = (LoginDetails) authResult.getDetails();
if (loginDetails.getRememberMe() != null && loginDetails.getRememberMe()) {
expirationTime = COOKIE_EXPIRATION_TIME * 1000;
cookExpirationTime = COOKIE_EXPIRATION_TIME;
}
String jwt = Jwts.builder()
// Subject 設(shè)置用戶名
.setSubject(authResult.getName())
// 設(shè)置用戶權(quán)限
.claim("authorities", stringBuffer)
// 過期時間
.setExpiration(new Date(System.currentTimeMillis() + expirationTime))
// 簽名算法
.signWith(SignatureAlgorithm.HS512, SECRET_KEY)
.compact();
Cookie cookie = new Cookie(COOKIE_TOKEN, jwt);
cookie.setHttpOnly(true);
cookie.setPath("/");
cookie.setMaxAge(cookExpirationTime);
response.addCookie(cookie);
// 向前端寫入數(shù)據(jù)
LoginResultDetails loginResultDetails = new LoginResultDetails();
ResultDetails resultDetails = new ResultDetails();
resultDetails.setStatus(HttpStatus.OK.value());
resultDetails.setMessage("登陸成功!");
resultDetails.setSuccess(true);
resultDetails.setTimestamp(LocalDateTime.now());
User user = new User();
user.setUsername(authResult.getName());
user.setPower(stringBuffer.toString());
user.setExpirationTime(System.currentTimeMillis() + expirationTime);
loginResultDetails.setResultDetails(resultDetails);
loginResultDetails.setUser(user);
loginResultDetails.setStatus(200);
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(loginResultDetails));
out.flush();
out.close();
}
/**
* 對請求的驗證
* */
public static Authentication getAuthentication(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, COOKIE_TOKEN);
String token = cookie != null ? cookie.getValue() : null;
if (token != null) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
// 獲取用戶權(quán)限
Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get("authorities").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
String userName = claims.getSubject();
if (userName != null) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userName, null, authorities);
usernamePasswordAuthenticationToken.setDetails(claims);
return usernamePasswordAuthenticationToken;
}
return null;
}
return null;
}
}
TokenAuthenticationHelper
至此,我們的基本登陸與驗證所需要的方法就寫完了
ps:其中的 LoginResultDetails 類和 ResultDetails 請看項目源碼,篇幅所限,此處不在贅述。
眾所周知,Spring Security 是借助一系列的 Servlet Filter 來來實現(xiàn)提供各種安全功能的,所以我們要使用 JWT 就需要自己實現(xiàn)兩個和 JWT 有關(guān)的過濾器
這兩個過濾器,我們分別來看,先看第一個:
在項目下新建一個包,名為 filter, 在 filter 下新建一個類名為 JwtLoginFilter, 并使其繼承 AbstractAuthenticationProcessingFilter 類,這個類是一個基于瀏覽器的基于 HTTP 的身份驗證請求的抽象處理器。
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
private final VerifyCodeService verifyCodeService;
private final LoginCountService loginCountService;
/**
* @param defaultFilterProcessesUrl 配置要過濾的地址,即登陸地址
* @param authenticationManager 認(rèn)證管理器,校驗身份時會用到
* @param loginCountService */
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager,
VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
this.loginCountService = loginCountService;
// 為 AbstractAuthenticationProcessingFilter 中的屬性賦值
setAuthenticationManager(authenticationManager);
this.verifyCodeService = verifyCodeService;
}
/**
* 提取用戶賬號密碼進(jìn)行驗證
* */
@Override
public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
// 判斷是否要拋出 登陸請求過快的異常
loginCountService.judgeLoginCount(httpServletRequest);
// 獲取 User 對象
// readValue 第一個參數(shù) 輸入流,第二個參數(shù) 要轉(zhuǎn)換的對象
User user = new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
// 驗證碼驗證
verifyCodeService.verify(httpServletRequest.getSession().getId(), user.getVerifyCode());
// 對 html 標(biāo)簽進(jìn)行轉(zhuǎn)義,防止 XSS 攻擊
String username = user.getUsername();
username = HtmlUtils.htmlEscape(username);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
username,
user.getPassword(),
user.getAuthorities()
);
// 添加驗證的附加信息
// 包括驗證碼信息和是否記住我
token.setDetails(new LoginDetails(user.getRememberMe(), user.getVerifyCode()));
// 進(jìn)行登陸驗證
return getAuthenticationManager().authenticate(token);
}
/**
* 登陸成功回調(diào)
* */
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
loginCountService.cleanLoginCount(request);
// 登陸成功
TokenAuthenticationHelper.addAuthentication(request, response ,authResult);
}
/**
* 登陸失敗回調(diào)
* */
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
// 錯誤請求次數(shù)加 1
loginCountService.addLoginCount(request, 1);
// 向前端寫入數(shù)據(jù)
ErrorDetails errorDetails = new ErrorDetails();
errorDetails.setStatus(HttpStatus.UNAUTHORIZED.value());
errorDetails.setMessage("登陸失敗!");
errorDetails.setError(failed.getLocalizedMessage());
errorDetails.setTimestamp(LocalDateTime.now());
errorDetails.setPath(request.getServletPath());
response.setContentType("application/json; charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(new ObjectMapper().writeValueAsString(errorDetails));
out.flush();
out.close();
}
}
JwtLoginFilter
這個類主要有以下幾個作用
ps:其中的 verifyCodeService 與 loginCountService 方法與本文關(guān)系不大,其中的代碼實現(xiàn)請看源碼
唯一需要注意的就是
驗證碼異常需要繼承 AuthenticationException 異常,
可以看到這是一個 Spring Security 各種異常的父類,寫一個驗證碼異常類繼承 AuthenticationException,然后直接將驗證碼異常拋出就好。
以下完整代碼位于 com.bugaugaoshu.security.service.impl.DigitsVerifyCodeServiceImpl 類下
@Override
public void verify(String key, String code) {
String lastVerifyCodeWithTimestamp = verifyCodeRepository.find(key);
// 如果沒有驗證碼,則隨機(jī)生成一個
if (lastVerifyCodeWithTimestamp == null) {
lastVerifyCodeWithTimestamp = appendTimestamp(randomDigitString(verifyCodeUtil.getLen()));
}
String[] lastVerifyCodeAndTimestamp = lastVerifyCodeWithTimestamp.split("#");
String lastVerifyCode = lastVerifyCodeAndTimestamp[0];
long timestamp = Long.parseLong(lastVerifyCodeAndTimestamp[1]);
if (timestamp + VERIFY_CODE_EXPIRE_TIMEOUT < System.currentTimeMillis()) {
throw new VerifyFailedException("驗證碼已過期!");
} else if (!Objects.equals(code, lastVerifyCode)) {
throw new VerifyFailedException("驗證碼錯誤!");
}
}
DigitsVerifyCodeServiceImpl
異常代碼在 com.bugaugaoshu.security.exception.VerifyFailedException 類下
第二個用戶過濾器
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
Authentication authentication = TokenAuthenticationHelper.getAuthentication(httpServletRequest);
// 對用 token 獲取到的用戶進(jìn)行校驗
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException |
SignatureException | IllegalArgumentException e) {
httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired,登陸已過期");
}
}
}
這個就很簡單了,將拿到的用戶 Token 進(jìn)行解析,如果正確,就將當(dāng)前用戶加入到 SecurityContext 的上下文中,授予用戶權(quán)限,否則返回 Token 過期的異常
接下來我們來配置 Spring Security, 代碼如下
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
public static String ADMIN = "ROLE_ADMIN";
public static String USER = "ROLE_USER";
private final VerifyCodeService verifyCodeService;
private final LoginCountService loginCountService;
/**
* 開放訪問的請求
*/
private final static String[] PERMIT_ALL_MAPPING = {
"/api/hello",
"/api/login",
"/api/home",
"/api/verifyImage",
"/api/image/verify",
"/images/**"
};
public WebSecurityConfig(VerifyCodeService verifyCodeService, LoginCountService loginCountService) {
this.verifyCodeService = verifyCodeService;
this.loginCountService = loginCountService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 跨域配置
*/
@Bean
public CorsConfigurationSource corsConfigurationSource() {
// 允許跨域訪問的 URL
List<String> allowedOriginsUrl = new ArrayList<>();
allowedOriginsUrl.add("http://localhost:8080");
allowedOriginsUrl.add("http://127.0.0.1:8080");
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 設(shè)置允許跨域訪問的 URL
config.setAllowedOrigins(allowedOriginsUrl);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers(PERMIT_ALL_MAPPING)
.permitAll()
.antMatchers("/api/user/**", "/api/data", "/api/logout")
// USER 和 ADMIN 都可以訪問
.hasAnyAuthority(USER, ADMIN)
.antMatchers("/api/admin/**")
// 只有 ADMIN 才可以訪問
.hasAnyAuthority(ADMIN)
.anyRequest()
.authenticated()
.and()
// 添加過濾器鏈,前一個參數(shù)過濾器, 后一個參數(shù)過濾器添加的地方
// 登陸過濾器
.addFilterBefore(new JwtLoginFilter("/api/login", authenticationManager(), verifyCodeService, loginCountService), UsernamePasswordAuthenticationFilter.class)
// 請求過濾器
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
// 開啟跨域
.cors()
.and()
// 開啟 csrf
.csrf()
// .disable();
.ignoringAntMatchers(PERMIT_ALL_MAPPING)
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 在內(nèi)存中寫入用戶數(shù)據(jù)
auth.
authenticationProvider(daoAuthenticationProvider());
//.inMemoryAuthentication();
// .withUser("user")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_USER")
// .and()
// .withUser("admin")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_ADMIN")
// .and()
// .withUser("block")
// .password(passwordEncoder().encode("123456"))
// .authorities("ROLE_USER")
// .accountLocked(true);
}
@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setHideUserNotFoundExceptions(false);
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(new CustomUserDetailsService());
return provider;
}
以上代碼的注釋很詳細(xì),我就不多說了,重點說一下兩個地方一個是 csrf 的問題,另一個就是 inMemoryAuthentication 在內(nèi)存中寫入用戶的部分。
首先說 csrf 的問題:我看了看網(wǎng)上有很多 Spring Security 的教程,都會將 .csrf()設(shè)置為 .disable() , 這種設(shè)置雖然方便,但是不夠安全,忽略了使用安全框架的初衷所以為了安全起見,我還是開啟了這個功能,順便學(xué)習(xí)一下如何使用 XSRF-TOKEN
因為這個項目是一個 Demo, 不涉及數(shù)據(jù)庫部分,所以我選擇了在內(nèi)存中直接寫入用戶,網(wǎng)上的向內(nèi)存中寫入用戶如上代碼注釋部分,這樣寫雖然簡單,但是有一些問題,在打個斷點我們就能知道種方式調(diào)用的是 Spring Security 的是 ProviderManager 這個方法,這種方法不方便我們拋出入用戶名不存在或者其異常,它都會拋出 Bad Credentials 異常,不會提示其它錯誤, 如下圖所示。
Spring Security 為了安全考慮,會把所有的登陸異常全部歸結(jié)為 Bad Credentials 異常,所以為了能拋出像用戶名不存在的這種異常,如果采用 Spring Security 默認(rèn)的登陸方式的話, 可以采用像 GitHub 項目 Vhr 里的這種處理方式,但是因為這個項目使用 Jwt 替換掉了默認(rèn)的登陸方式,想要實現(xiàn)詳細(xì)的異常信息拋出就比較復(fù)雜了,我找了好久也沒找到比較簡單且合適的方法。如果你有好的方法,歡迎分享。
最后我的解決方案是使用 Spring Security 的 DaoAuthenticationProvider 這個類來成為認(rèn)證提供者,這個類實現(xiàn)了 AbstractUserDetailsAuthenticationProvider 這一個抽象的用戶詳細(xì)信息身份驗證功能,查看注釋我們可以知道 AbstractUserDetailsAuthenticationProvider 提供了 A base AuthenticationProvider that allows subclasses to override and work with UserDetails objects. The class is designed to respond to UsernamePasswordAuthenticationToken authentication requests.(允許子類重寫和使用 UserDetails 對象的基本身份驗證提供程序。該類旨在響應(yīng) UsernamePasswordAuthenticationToken 身份驗證請求。)
通過配置自定義的用戶查詢實現(xiàn)類,我們可以直接在 CustomUserDetailsService 里拋出沒有發(fā)現(xiàn)用戶名的異常,然后再設(shè)置 hideUserNotFoundExceptions 為 false 這樣就可以區(qū)別是密碼錯誤,還是用戶名不存在的錯誤了,
但是這種方式還是有一個問題,不能拋出像賬戶被鎖定這種異常,理論上這種功能可以繼承 AbstractUserDetailsAuthenticationProvider 這個抽象類然后自己重寫的登陸方法來實現(xiàn),我看了看好像比較復(fù)雜,一個 Demo 沒必要,我就放棄了。
另外據(jù)說安全信息暴露的越少越好,所以暫時就先這樣吧。(算是給自己找個理由)
用戶查找服務(wù)
public class CustomUserDetailsService implements UserDetailsService {
private List<UserDetails> userList = new ArrayList<>();
public CustomUserDetailsService() {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
UserDetails user = User.withUsername("user").password(passwordEncoder.encode("123456")).authorities(WebSecurityConfig.USER).build();
UserDetails admin = User.withUsername("admin").password(passwordEncoder.encode("123456")).authorities(WebSecurityConfig.ADMIN).build();
userList.add(user);
userList.add(admin);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
for (UserDetails userDetails : userList) {
if (userDetails.getUsername().equals(username)) {
// 此處我嘗試過直接返回 user
// 但是這樣的話,只有后臺服務(wù)啟動后第一次登陸會有效
// 推出后第二次登陸會出現(xiàn) Empty encoded password 的錯誤,導(dǎo)致無法登陸
// 這樣寫就不會出現(xiàn)這種問題了
// 因為在第一次驗證后,用戶的密碼會被清除,導(dǎo)致第二次登陸系統(tǒng)拿到的是空密碼
// 所以需要new一個對象或?qū)⒃瓕ο髲?fù)制一份
// 這個解決方案來自 https://stackoverflow.com/questions/43007763/spring-security-encoded-password-gives-me-bad-credentials/43046195#43046195
return new User(userDetails.getUsername(), userDetails.getPassword(), userDetails.getAuthorities());
}
}
throw new UsernameNotFoundException("用戶名不存在,請檢查用戶名或注冊!");
}
}
這部分就比較簡單了,唯一的注意點我在注釋中已經(jīng)寫的很清楚了,當(dāng)然你要是使用連接數(shù)據(jù)庫的話,這個問題就不存在了。
UserDetailsService 這個接口就是 Spring Security 為其它的數(shù)據(jù)訪問策略做支持的。
至此,一個基本的 Spring Security + JWT 登陸的后端就完成了,你可以寫幾個 controller 然后用 postman 測試功能了。
其它部分的代碼因為比較簡單,你可以參照源碼自行實現(xiàn)你需要的功能。
創(chuàng)建 Vue 項目的方式網(wǎng)上有很多,此處也不再贅述,我只說一點,過去 Vue 項目創(chuàng)建完成后,在項目目錄下會生成一個 config 文件夾,用來存放 vue 的配置,但現(xiàn)在默認(rèn)創(chuàng)建的項目是不會生成這個文件夾的,需要你手動在項目根目錄下創(chuàng)建 vue.config.js 作為配置文件。
此處請參考:Vue CLI 官方文檔,配置參考部分
附:使用 Vue CIL 創(chuàng)建 Vue 項目
前后端數(shù)據(jù)傳遞我使用了更為簡單的 fetch api, 當(dāng)然你也可以選擇兼容性更加好的 axios
Ui 為 ElementUI
為了獲取 XSRF-TOKEN,還需要 VueCookies
最后為了在項目的首頁展示介紹,我還引入了 mavonEditor,一個基于 vue 的 Markdown 插件
引入以上包之后,你與要修改 src 目錄下的 main.js 文件如下。
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import mavonEditor from 'mavon-editor';
import 'mavon-editor/dist/css/index.css';
import VueCookies from 'vue-cookies'
import axios from 'axios'
// 讓ajax攜帶cookie
axios.defaults.withCredentials=true;
// 注冊 axios 為全局變量
Vue.prototype.$axios = axios
// 使用 vue cookie
Vue.use(VueCookies)
Vue.config.productionTip = false
// 使用 ElementUI 組件
Vue.use(ElementUI)
// markdown 解析編輯工具
Vue.use(mavonEditor)
// 后臺服務(wù)地址
Vue.prototype.SERVER_API_URL = "http://127.0.0.1:8088/api";
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在創(chuàng)建 vue.config.js 完成后,你需要在里面輸入以下內(nèi)容,用來完成 Vue 的跨域配置
module.exports = {
// options...
devServer: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8088',
changeOrigin: true,
ws: true,
pathRewrite:{
'^/api':''
}
}
}
}
}
頁面設(shè)計這些沒有什么可寫的了,需要注意的一點就是在對后端服務(wù)器進(jìn)行 POST,DELETE,PUT 等操作時,請在請求頭中帶上 "X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN'), 如果不帶,那么哪怕你登陸了,后臺也會返回 403 異常的。
credentials: "include" 這句也不能少,這是攜帶 Cookie 所必須的語句。如果不加這一句,等于沒有攜帶 Cookie,也就等于沒有登陸了。
舉個例子:
deleteItem(data) {
fetch(this.SERVER_API_URL + "/admin/data/" + data.id, {
headers: {
"Content-Type": "application/json; charset=UTF-8",
"X-XSRF-TOKEN": this.$cookies.get('XSRF-TOKEN')
},
method: "DELETE",
credentials: "include"
}).then(response => response.json())
.then(json => {
if (json.status === 200) {
this.systemDataList.splice(data.id, 1);
this.$message({
message: '刪除成功',
type: 'success'
});
} else {
window.console.log(json);
this.$message.error(json.message);
}
});
},
作者:陜西顏值扛把子 來源:知乎 github完整代碼可私信獲取!
人對超過50000個github上的開源java項目做了統(tǒng)計,統(tǒng)計出最常用的16個開源工具類及其方法。
大部分方法可以望文知意,請務(wù)必瀏覽一遍,知道有哪些好用的工具類,不必自己造輪子了。
絕對的好東西,直接收藏吧
下面是已經(jīng)按照使用次數(shù)排序的列表,
1. org.apache.commons.io.IOUtils
2. org.apache.commons.io.FileUtils
3. org.apache.commons.lang.StringUtils
4. org.apache.http.util.EntityUtils
5. org.apache.commons.lang3.StringUtils
6. org.apache.commons.io.FilenameUtils
7. org.springframework.util.StringUtils
8. org.apache.commons.lang.ArrayUtils
9. org.apache.commons.lang.StringEscapeUtils
10. org.apache.http.client.utils.URLEncodedUtils
11. org.apache.commons.codec.digest.DigestUtils
12. org.apache.commons.collections.CollectionUtils
13. org.apache.commons.lang3.ArrayUtils
14. org.apache.commons.beanutils.PropertyUtils
15. org.apache.commons.lang3.StringEscapeUtils
16. org.apache.commons.beanutils.BeanUtils
. org.apache.commons.io.IOUtils
closeQuietly:關(guān)閉一個IO流、socket、或者selector且不拋出異常,通常放在finally塊 toString:轉(zhuǎn)換IO流、 Uri、 byte[]為String copy:IO流數(shù)據(jù)復(fù)制,從輸入流寫到輸出流中,最大支持2GB toByteArray:從輸入流、URI獲取byte[] write:把字節(jié). 字符等寫入輸出流 toInputStream:把字符轉(zhuǎn)換為輸入流 readLines:從輸入流中讀取多行數(shù)據(jù),返回List<String> copyLarge:同copy,支持2GB以上數(shù)據(jù)的復(fù)制 lineIterator:從輸入流返回一個迭代器,根據(jù)參數(shù)要求讀取的數(shù)據(jù)量,全部讀取,如果數(shù)據(jù)不夠,則失敗
deleteDirectory:刪除文件夾 readFileToString:以字符形式讀取文件內(nèi)容 deleteQueitly:刪除文件或文件夾且不會拋出異常 copyFile:復(fù)制文件 writeStringToFile:把字符寫到目標(biāo)文件,如果文件不存在,則創(chuàng)建 forceMkdir:強(qiáng)制創(chuàng)建文件夾,如果該文件夾父級目錄不存在,則創(chuàng)建父級 write:把字符寫到指定文件中 listFiles:列舉某個目錄下的文件(根據(jù)過濾器) copyDirectory:復(fù)制文件夾 forceDelete:強(qiáng)制刪除文件
isBlank:字符串是否為空 (trim后判斷) isEmpty:字符串是否為空 (不trim并判斷) equals:字符串是否相等 join:合并數(shù)組為單一字符串,可傳分隔符 split:分割字符串 EMPTY:返回空字符串 trimToNull:trim后為空字符串則轉(zhuǎn)換為null replace:替換字符串
toString:把Entity轉(zhuǎn)換為字符串 consume:確保Entity中的內(nèi)容全部被消費。可以看到源碼里又一次消費了Entity的內(nèi)容,假如用戶沒有消費,那調(diào)用Entity時候?qū)阉M掉 toByteArray:把Entity轉(zhuǎn)換為字節(jié)流 consumeQuietly:和consume一樣,但不拋異常 getContentCharset:獲取內(nèi)容的編碼
isBlank:字符串是否為空 (trim后判斷) isEmpty:字符串是否為空 (不trim并判斷) equals:字符串是否相等 join:合并數(shù)組為單一字符串,可傳分隔符 split:分割字符串 EMPTY:返回空字符串 replace:替換字符串 capitalize:首字符大寫
getExtension:返回文件后綴名 getBaseName:返回文件名,不包含后綴名 getName:返回文件全名 concat:按命令行風(fēng)格組合文件路徑(詳見方法注釋) removeExtension:刪除后綴名 normalize:使路徑正常化 wildcardMatch:匹配通配符 seperatorToUnix:路徑分隔符改成unix系統(tǒng)格式的,即/ getFullPath:獲取文件路徑,不包括文件名 isExtension:檢查文件后綴名是不是傳入?yún)?shù)(List<String>)中的一個
hasText:檢查字符串中是否包含文本 hasLength:檢測字符串是否長度大于0 isEmpty:檢測字符串是否為空(若傳入為對象,則判斷對象是否為null) commaDelimitedStringToArray:逗號分隔的String轉(zhuǎn)換為數(shù)組 collectionToDelimitedString:把集合轉(zhuǎn)為CSV格式字符串 replace 替換字符串 7. delimitedListToStringArray:相當(dāng)于split uncapitalize:首字母小寫 collectionToDelimitedCommaString:把集合轉(zhuǎn)為CSV格式字符串 tokenizeToStringArray:和split基本一樣,但能自動去掉空白的單詞
contains:是否包含某字符串 addAll:添加整個數(shù)組 clone:克隆一個數(shù)組 isEmpty:是否空數(shù)組 add:向數(shù)組添加元素 subarray:截取數(shù)組 indexOf:查找某個元素的下標(biāo) isEquals:比較數(shù)組是否相等 toObject:基礎(chǔ)類型數(shù)據(jù)數(shù)組轉(zhuǎn)換為對應(yīng)的Object數(shù)組
參考十五:org.apache.commons.lang3.StringEscapeUtils
format:格式化參數(shù),返回一個HTTP POST或者HTTP PUT可用application/x-www-form-urlencoded字符串 parse:把String或者URI等轉(zhuǎn)換為List<NameValuePair>
md5Hex:MD5加密,返回32位字符串 sha1Hex:SHA-1加密 sha256Hex:SHA-256加密 sha512Hex:SHA-512加密 md5:MD5加密,返回16位字符串
isEmpty:是否為空 select:根據(jù)條件篩選集合元素 transform:根據(jù)指定方法處理集合元素,類似List的map() filter:過濾元素,雷瑟List的filter() find:基本和select一樣 collect:和transform 差不多一樣,但是返回新數(shù)組 forAllDo:調(diào)用每個元素的指定方法 isEqualCollection:判斷兩個集合是否一致
contains:是否包含某個字符串 addAll:添加整個數(shù)組 clone:克隆一個數(shù)組 isEmpty:是否空數(shù)組 add:向數(shù)組添加元素 subarray:截取數(shù)組 indexOf:查找某個元素的下標(biāo) isEquals:比較數(shù)組是否相等 toObject:基礎(chǔ)類型數(shù)據(jù)數(shù)組轉(zhuǎn)換為對應(yīng)的Object數(shù)組
getProperty:獲取對象屬性值 setProperty:設(shè)置對象屬性值 getPropertyDiscriptor:獲取屬性描述器 isReadable:檢查屬性是否可訪問 copyProperties:復(fù)制屬性值,從一個對象到另一個對象 getPropertyDiscriptors:獲取所有屬性描述器 isWriteable:檢查屬性是否可寫 getPropertyType:獲取對象屬性類型
unescapeHtml4:轉(zhuǎn)義html escapeHtml4:反轉(zhuǎn)義html escapeXml:轉(zhuǎn)義xml unescapeXml:反轉(zhuǎn)義xml escapeJava:轉(zhuǎn)義unicode編碼 escapeEcmaScript:轉(zhuǎn)義EcmaScript字符 unescapeJava:反轉(zhuǎn)義unicode編碼 escapeJson:轉(zhuǎn)義json字符 escapeXml10:轉(zhuǎn)義Xml10
這個現(xiàn)在已經(jīng)廢棄了,建議使用commons-text包里面的方法。
copyPeoperties:復(fù)制屬性值,從一個對象到另一個對象 getProperty:獲取對象屬性值 setProperty:設(shè)置對象屬性值 populate:根據(jù)Map給屬性復(fù)制 copyPeoperty:復(fù)制單個值,從一個對象到另一個對象 cloneBean:克隆bean實例
感謝閱讀,如果這篇文章幫助了您,歡迎 點贊 ,收藏,關(guān)注,轉(zhuǎn)發(fā) 喲。您的幫助是我們前行的動力,我們會提供更多有價值的內(nèi)容給大家... 謝謝!
*請認(rèn)真填寫需求信息,我們會在24小時內(nèi)與您取得聯(lián)系。