整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          Spring Security+JWT+Vue實現一個前后端分離無狀態認證Demo

          行展示

          后端

          主要展示 Spring Security 與 JWT 結合使用構建后端 API 接口。

          主要功能包括登陸(如何在 Spring Security 中添加驗證碼登陸),查找,創建,刪除并對用戶權限進行區分等等。

          ps:由于只是 Demo,所以沒有調用數據庫,以上所說增刪改查均在 HashMap 中完成。

          前端

          展示如何使用 Vue 構建前端后與后端的配合,包括跨域的設置,前端登陸攔截

          并實現 POST,GET,DELETE 請求。包括如何在 Vue 中使用后端的 XSRF-TOKEN 防范 CSRF 攻擊

          技術棧


          實現細節

          創建 Spring boot 項目,添加 JJWT 和 Spring Security 的項目依賴,這個非常簡單,有很多的教程都有塊內容,唯一需要注意的是,如果你使用的 Java 版本是 11,那么你還需要添加以下依賴,使用 Java8 則不需要。

          <dependency>
             <groupId>javax.xml.bind</groupId>
             <artifactId>jaxb-api</artifactId>
             <version>2.3.0</version>
          </dependency>

          要使用 Spring Security 實現對用戶的權限控制,首先需要實現一個簡單的 User 對象實現 UserDetails 接口,UserDetails 接口負責提供核心用戶的信息,如果你只需要用戶登陸的賬號密碼,不需要其它信息,如驗證碼等,那么你可以直接使用 Spring Security 默認提供的 User 類,而不需要自己實現。

          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 本質上就是用自己自定義的登陸過濾器,去替換 Spring Security 原生的登陸過濾器,這樣的話,原生的記住我功能就會無法使用,所以我在 User 對象里添加了記住我的信息,用來自己實現這個功能。

          JWT 令牌認證工具

          首先我們來新建一個 TokenAuthenticationHelper 類,用來處理認證過程中的驗證和請求

          public class TokenAuthenticationHelper {
              /**
               * 未設置記住我時 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";
          
              /**
               * 設置登陸成功后令牌返回
               * */
              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 設置用戶名
                          .setSubject(authResult.getName())
                          // 設置用戶權限
                          .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);
          
                  // 向前端寫入數據
                  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();
          
                      // 獲取用戶權限
                      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

          1. addAuthentication 方法負責返回登陸成功的信息,使用 HTTP Only 的 Cookie 可以有效防止 XSS 攻擊。
          2. 登陸成功后返回用戶的權限,用戶名,登陸過期時間,可以有效的幫助前端構建合適的用戶界面。
          3. getAuthentication 方法負責對用戶的其它請求進行驗證,如果用戶的 JWT 解析正確,則向 Spring Security 返回 usernamePasswordAuthenticationToken 用戶名密碼驗證令牌,告訴 Spring Security 用戶所擁有的權限,并放到當前的 Context 中,然后執行過濾鏈使請求繼續執行下去。

          至此,我們的基本登陸與驗證所需要的方法就寫完了

          ps:其中的 LoginResultDetails 類和 ResultDetails 請看項目源碼,篇幅所限,此處不在贅述。

          JWT 過濾器配置

          眾所周知,Spring Security 是借助一系列的 Servlet Filter 來來實現提供各種安全功能的,所以我們要使用 JWT 就需要自己實現兩個和 JWT 有關的過濾器

          1. 一個是用戶登錄的過濾器,在用戶的登錄的過濾器中校驗用戶是否登錄成功,如果登錄成功,則生成一個 token 返回給客戶端,登錄失敗則給前端一個登錄失敗的提示。
          2. 第二個過濾器則是當其他請求發送來,校驗 token 的過濾器,如果校驗成功,就讓請求繼續執行。

          這兩個過濾器,我們分別來看,先看第一個:

          在項目下新建一個包,名為 filter, 在 filter 下新建一個類名為 JwtLoginFilter, 并使其繼承 AbstractAuthenticationProcessingFilter 類,這個類是一個基于瀏覽器的基于 HTTP 的身份驗證請求的抽象處理器。

          public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
              private final VerifyCodeService verifyCodeService;
          
              private final LoginCountService loginCountService;
          
              /**
               * @param defaultFilterProcessesUrl 配置要過濾的地址,即登陸地址
               * @param authenticationManager 認證管理器,校驗身份時會用到
               * @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;
              }
          
          
          
              /**
               * 提取用戶賬號密碼進行驗證
               * */
              @Override
              public Authentication attemptAuthentication(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException, ServletException {
                  // 判斷是否要拋出 登陸請求過快的異常
                  loginCountService.judgeLoginCount(httpServletRequest);
                  // 獲取 User 對象
                  // readValue 第一個參數 輸入流,第二個參數 要轉換的對象
                  User user = new ObjectMapper().readValue(httpServletRequest.getInputStream(), User.class);
                  // 驗證碼驗證
                  verifyCodeService.verify(httpServletRequest.getSession().getId(), user.getVerifyCode());
                  // 對 html 標簽進行轉義,防止 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()));
                  // 進行登陸驗證
                  return getAuthenticationManager().authenticate(token);
              }
          
              /**
               * 登陸成功回調
               * */
              @Override
              protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
                  loginCountService.cleanLoginCount(request);
                  // 登陸成功
                  TokenAuthenticationHelper.addAuthentication(request, response ,authResult);
              }
          
              /**
               * 登陸失敗回調
               * */
              @Override
              protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
                  // 錯誤請求次數加 1
                  loginCountService.addLoginCount(request, 1);
                  // 向前端寫入數據
                  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

          這個類主要有以下幾個作用

          1. 自定義 JwtLoginFilter 繼承自 AbstractAuthenticationProcessingFilter,并實現其中的三個默認方法,其中的 defaultFilterProcessesUrl 變量就是我們需要設置的登陸路徑
          2. attemptAuthentication 方法中,我們從登錄參數中提取出用戶名密碼,然后調用 AuthenticationManager.authenticate() 方法去進行自動校驗。
          3. 第二步如果校驗成功,就會來到 successfulAuthentication 回調中,在 successfulAuthentication 方法中,使用之前已經寫好的 addAuthentication 來生成 token,并使用 Http Only 的 cookie 寫出到客戶端。
          4. 第二步如果校驗失敗就會來到 unsuccessfulAuthentication 方法中,在這個方法中返回一個錯誤提示給客戶端即可。

          ps:其中的 verifyCodeService 與 loginCountService 方法與本文關系不大,其中的代碼實現請看源碼

          唯一需要注意的就是

          驗證碼異常需要繼承 AuthenticationException 異常,

          可以看到這是一個 Spring Security 各種異常的父類,寫一個驗證碼異常類繼承 AuthenticationException,然后直接將驗證碼異常拋出就好。

          以下完整代碼位于 com.bugaugaoshu.security.service.impl.DigitsVerifyCodeServiceImpl 類下

          @Override
          public void verify(String key, String code) {
                  String lastVerifyCodeWithTimestamp = verifyCodeRepository.find(key);
                  // 如果沒有驗證碼,則隨機生成一個
                  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 獲取到的用戶進行校驗
                      SecurityContextHolder.getContext().setAuthentication(authentication);
                      filterChain.doFilter(httpServletRequest, httpServletResponse);
                  } catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException |
                          SignatureException | IllegalArgumentException e) {
                      httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token expired,登陸已過期");
                  }
              }
          }

          這個就很簡單了,將拿到的用戶 Token 進行解析,如果正確,就將當前用戶加入到 SecurityContext 的上下文中,授予用戶權限,否則返回 Token 過期的異常

          Spring Security 配置

          接下來我們來配置 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);
                  // 設置允許跨域訪問的 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()
                          // 添加過濾器鏈,前一個參數過濾器, 后一個參數過濾器添加的地方
                          // 登陸過濾器
                          .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 {
                  // 在內存中寫入用戶數據
                  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;
              }

          以上代碼的注釋很詳細,我就不多說了,重點說一下兩個地方一個是 csrf 的問題,另一個就是 inMemoryAuthentication 在內存中寫入用戶的部分。

          首先說 csrf 的問題:我看了看網上有很多 Spring Security 的教程,都會將 .csrf()設置為 .disable() , 這種設置雖然方便,但是不夠安全,忽略了使用安全框架的初衷所以為了安全起見,我還是開啟了這個功能,順便學習一下如何使用 XSRF-TOKEN

          因為這個項目是一個 Demo, 不涉及數據庫部分,所以我選擇了在內存中直接寫入用戶,網上的向內存中寫入用戶如上代碼注釋部分,這樣寫雖然簡單,但是有一些問題,在打個斷點我們就能知道種方式調用的是 Spring Security 的是 ProviderManager 這個方法,這種方法不方便我們拋出入用戶名不存在或者其異常,它都會拋出 Bad Credentials 異常,不會提示其它錯誤, 如下圖所示。

          Spring Security 為了安全考慮,會把所有的登陸異常全部歸結為 Bad Credentials 異常,所以為了能拋出像用戶名不存在的這種異常,如果采用 Spring Security 默認的登陸方式的話, 可以采用像 GitHub 項目 Vhr 里的這種處理方式,但是因為這個項目使用 Jwt 替換掉了默認的登陸方式,想要實現詳細的異常信息拋出就比較復雜了,我找了好久也沒找到比較簡單且合適的方法。如果你有好的方法,歡迎分享。

          最后我的解決方案是使用 Spring Security 的 DaoAuthenticationProvider 這個類來成為認證提供者,這個類實現了 AbstractUserDetailsAuthenticationProvider 這一個抽象的用戶詳細信息身份驗證功能,查看注釋我們可以知道 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 對象的基本身份驗證提供程序。該類旨在響應 UsernamePasswordAuthenticationToken 身份驗證請求。)

          通過配置自定義的用戶查詢實現類,我們可以直接在 CustomUserDetailsService 里拋出沒有發現用戶名的異常,然后再設置 hideUserNotFoundExceptions 為 false 這樣就可以區別是密碼錯誤,還是用戶名不存在的錯誤了,

          但是這種方式還是有一個問題,不能拋出像賬戶被鎖定這種異常,理論上這種功能可以繼承 AbstractUserDetailsAuthenticationProvider 這個抽象類然后自己重寫的登陸方法來實現,我看了看好像比較復雜,一個 Demo 沒必要,我就放棄了。

          另外據說安全信息暴露的越少越好,所以暫時就先這樣吧。(算是給自己找個理由)

          用戶查找服務

          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
                          // 但是這樣的話,只有后臺服務啟動后第一次登陸會有效
                          // 推出后第二次登陸會出現 Empty encoded password 的錯誤,導致無法登陸
                          // 這樣寫就不會出現這種問題了
                          // 因為在第一次驗證后,用戶的密碼會被清除,導致第二次登陸系統拿到的是空密碼
                          // 所以需要new一個對象或將原對象復制一份
                          // 這個解決方案來自 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("用戶名不存在,請檢查用戶名或注冊!");
              }
          }

          這部分就比較簡單了,唯一的注意點我在注釋中已經寫的很清楚了,當然你要是使用連接數據庫的話,這個問題就不存在了。

          UserDetailsService 這個接口就是 Spring Security 為其它的數據訪問策略做支持的。

          至此,一個基本的 Spring Security + JWT 登陸的后端就完成了,你可以寫幾個 controller 然后用 postman 測試功能了。

          其它部分的代碼因為比較簡單,你可以參照源碼自行實現你需要的功能。

          前端搭建

          創建 Vue 項目的方式網上有很多,此處也不再贅述,我只說一點,過去 Vue 項目創建完成后,在項目目錄下會生成一個 config 文件夾,用來存放 vue 的配置,但現在默認創建的項目是不會生成這個文件夾的,需要你手動在項目根目錄下創建 vue.config.js 作為配置文件。

          此處請參考:Vue CLI 官方文檔,配置參考部分

          附:使用 Vue CIL 創建 Vue 項目

          依賴包

          前后端數據傳遞我使用了更為簡單的 fetch api, 當然你也可以選擇兼容性更加好的 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)
          // 后臺服務地址
          Vue.prototype.SERVER_API_URL = "http://127.0.0.1:8088/api";
          
          
          new Vue({
              router,
              store,
              render: h => h(App)
          }).$mount('#app')

          前端跨域配置

          在創建 vue.config.js 完成后,你需要在里面輸入以下內容,用來完成 Vue 的跨域配置

          module.exports = {
              // options...
              devServer: {
                proxy: {
                    '/api': {
                        target: 'http://127.0.0.1:8088',
                        changeOrigin: true,
                        ws: true,
                        pathRewrite:{
                          '^/api':'' 
                       }
                    }
                }
            }
          }

          一些注意事項

          頁面設計這些沒有什么可寫的了,需要注意的一點就是在對后端服務器進行 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項目做了統計,統計出最常用的16個開源工具類及其方法。

          大部分方法可以望文知意,請務必瀏覽一遍,知道有哪些好用的工具類,不必自己造輪子了。

          絕對的好東西,直接收藏吧

          下面是已經按照使用次數排序的列表,

          1. org.apache.commons.io.IOUtils

          • closeQuietly ( )
          • toString ( )
          • copy ( )
          • toByteArray ( )
          • write ( )
          • toInputStream ( )
          • readLines ( )
          • copyLarge ( )
          • lineIterator ( )
          • readFully ( )

          2. org.apache.commons.io.FileUtils

          • deleteDirectory ( )
          • readFileToString ( )
          • deleteQuietly ( )
          • copyFile ( )
          • writeStringToFile ( )
          • forceMkdir ( )
          • write ( )
          • listFiles ( )
          • copyDirectory ( )
          • forceDelete ( )

          3. org.apache.commons.lang.StringUtils

          • isBlank ( )
          • isNotBlank ( )
          • isEmpty ( )
          • isNotEmpty ( )
          • equals ( )
          • join ( )
          • split ( )
          • EMPTY
          • trimToNull ( )
          • replace ( )

          4. org.apache.http.util.EntityUtils

          • toString ( )
          • consume ( )
          • toByteArray ( )
          • consumeQuietly ( )
          • getContentCharSet ( )

          5. org.apache.commons.lang3.StringUtils

          • isBlank ( )
          • isNotBlank ( )
          • isEmpty ( )
          • isNotEmpty ( )
          • join ( )
          • equals ( )
          • split ( )
          • EMPTY
          • replace ( )
          • capitalize ( )

          6. org.apache.commons.io.FilenameUtils

          • getExtension ( )
          • getBaseName ( )
          • getName ( )
          • concat ( )
          • removeExtension ( )
          • normalize ( )
          • wildcardMatch ( )
          • separatorsToUnix ( )
          • getFullPath ( )
          • isExtension ( )

          7. org.springframework.util.StringUtils

          • hasText ( )
          • hasLength ( )
          • isEmpty ( )
          • commaDelimitedListToStringArray ( )
          • collectionToDelimitedString ( )
          • replace ( )
          • delimitedListToStringArray ( )
          • uncapitalize ( )
          • collectionToCommaDelimitedString ( )
          • tokenizeToStringArray ( )

          8. org.apache.commons.lang.ArrayUtils

          • contains ( )
          • addAll ( )
          • clone ( )
          • isEmpty ( )
          • add ( )
          • EMPTY_BYTE_ARRAY
          • subarray ( )
          • indexOf ( )
          • isEquals ( )
          • toObject ( )

          9. org.apache.commons.lang.StringEscapeUtils

          • escapeHtml ( )
          • unescapeHtml ( )
          • escapeXml ( )
          • escapeSql ( )
          • unescapeJava ( )
          • escapeJava ( )
          • escapeJavaScript ( )
          • unescapeXml ( )
          • unescapeJavaScript ( )

          10. org.apache.http.client.utils.URLEncodedUtils

          • format ( )
          • parse ( )

          11. org.apache.commons.codec.digest.DigestUtils

          • md5Hex ( )
          • shaHex ( )
          • sha256Hex ( )
          • sha1Hex ( )
          • sha ( )
          • md5 ( )
          • sha512Hex ( )
          • sha1 ( )

          12. org.apache.commons.collections.CollectionUtils

          • isEmpty ( )
          • isNotEmpty ( )
          • select ( )
          • transform ( )
          • filter ( )
          • find ( )
          • collect ( )
          • forAllDo ( )
          • addAll ( )
          • isEqualCollection ( )

          13. org.apache.commons.lang3.ArrayUtils

          • contains ( )
          • isEmpty ( )
          • isNotEmpty ( )
          • add ( )
          • clone ( )
          • addAll ( )
          • subarray ( )
          • indexOf ( )
          • EMPTY_OBJECT_ARRAY
          • EMPTY_STRING_ARRAY

          14. org.apache.commons.beanutils.PropertyUtils

          • getProperty ( )
          • setProperty ( )
          • getPropertyDescriptors ( )
          • isReadable ( )
          • copyProperties ( )
          • getPropertyDescriptor ( )
          • getSimpleProperty ( )
          • isWriteable ( )
          • setSimpleProperty ( )
          • getPropertyType ( )

          15. org.apache.commons.lang3.StringEscapeUtils

          • unescapeHtml4 ( )
          • escapeHtml4 ( )
          • escapeXml ( )
          • unescapeXml ( )
          • escapeJava ( )
          • escapeEcmaScript ( )
          • unescapeJava ( )
          • escapeJson ( )
          • escapeXml10 ( )

          16. org.apache.commons.beanutils.BeanUtils

          • copyProperties ( )
          • getProperty ( )
          • setProperty ( )
          • describe ( )
          • populate ( )
          • copyProperty ( )
          • cloneBean ( )


          . org.apache.commons.io.IOUtils

          closeQuietly:關閉一個IO流、socket、或者selector且不拋出異常,通常放在finally塊
          toString:轉換IO流、 Uri、 byte[]為String
          copy:IO流數據復制,從輸入流寫到輸出流中,最大支持2GB
          toByteArray:從輸入流、URI獲取byte[]
          write:把字節. 字符等寫入輸出流
          toInputStream:把字符轉換為輸入流
          readLines:從輸入流中讀取多行數據,返回List<String>
          copyLarge:同copy,支持2GB以上數據的復制
          lineIterator:從輸入流返回一個迭代器,根據參數要求讀取的數據量,全部讀取,如果數據不夠,則失敗

          二. org.apache.commons.io.FileUtils

          deleteDirectory:刪除文件夾
          readFileToString:以字符形式讀取文件內容
          deleteQueitly:刪除文件或文件夾且不會拋出異常
          copyFile:復制文件
          writeStringToFile:把字符寫到目標文件,如果文件不存在,則創建
          forceMkdir:強制創建文件夾,如果該文件夾父級目錄不存在,則創建父級
          write:把字符寫到指定文件中
          listFiles:列舉某個目錄下的文件(根據過濾器)
          copyDirectory:復制文件夾
          forceDelete:強制刪除文件

          三. org.apache.commons.lang.StringUtils

          isBlank:字符串是否為空 (trim后判斷)
          isEmpty:字符串是否為空 (不trim并判斷)
          equals:字符串是否相等
          join:合并數組為單一字符串,可傳分隔符
          split:分割字符串
          EMPTY:返回空字符串
          trimToNull:trim后為空字符串則轉換為null
          replace:替換字符串

          四. org.apache.http.util.EntityUtils

          toString:把Entity轉換為字符串
          consume:確保Entity中的內容全部被消費。可以看到源碼里又一次消費了Entity的內容,假如用戶沒有消費,那調用Entity時候將會把它消費掉
          toByteArray:把Entity轉換為字節流
          consumeQuietly:和consume一樣,但不拋異常
          getContentCharset:獲取內容的編碼

          五. org.apache.commons.lang3.StringUtils

          isBlank:字符串是否為空 (trim后判斷)
          isEmpty:字符串是否為空 (不trim并判斷)
          equals:字符串是否相等
          join:合并數組為單一字符串,可傳分隔符
          split:分割字符串
          EMPTY:返回空字符串
          replace:替換字符串
          capitalize:首字符大寫

          六. org.apache.commons.io.FilenameUtils

          getExtension:返回文件后綴名
          getBaseName:返回文件名,不包含后綴名
          getName:返回文件全名
          concat:按命令行風格組合文件路徑(詳見方法注釋)
          removeExtension:刪除后綴名
          normalize:使路徑正常化
          wildcardMatch:匹配通配符
          seperatorToUnix:路徑分隔符改成unix系統格式的,即/
          getFullPath:獲取文件路徑,不包括文件名
          isExtension:檢查文件后綴名是不是傳入參數(List<String>)中的一個

          七. org.springframework.util.StringUtils

          hasText:檢查字符串中是否包含文本
          hasLength:檢測字符串是否長度大于0
          isEmpty:檢測字符串是否為空(若傳入為對象,則判斷對象是否為null)
          commaDelimitedStringToArray:逗號分隔的String轉換為數組
          collectionToDelimitedString:把集合轉為CSV格式字符串
          replace 替換字符串
          7. delimitedListToStringArray:相當于split
          uncapitalize:首字母小寫
          collectionToDelimitedCommaString:把集合轉為CSV格式字符串
          tokenizeToStringArray:和split基本一樣,但能自動去掉空白的單詞

          八. org.apache.commons.lang.ArrayUtils

          contains:是否包含某字符串
          addAll:添加整個數組
          clone:克隆一個數組
          isEmpty:是否空數組
          add:向數組添加元素
          subarray:截取數組
          indexOf:查找某個元素的下標
          isEquals:比較數組是否相等
          toObject:基礎類型數據數組轉換為對應的Object數組

          九. org.apache.commons.lang.StringEscapeUtils

          參考十五:org.apache.commons.lang3.StringEscapeUtils

          十. org.apache.http.client.utils.URLEncodedUtils

          format:格式化參數,返回一個HTTP POST或者HTTP PUT可用application/x-www-form-urlencoded字符串
          parse:把String或者URI等轉換為List<NameValuePair>

          十一. org.apache.commons.codec.digest.DigestUtils

          md5Hex:MD5加密,返回32位字符串
          sha1Hex:SHA-1加密
          sha256Hex:SHA-256加密
          sha512Hex:SHA-512加密
          md5:MD5加密,返回16位字符串

          十二. org.apache.commons.collections.CollectionUtils

          isEmpty:是否為空
          select:根據條件篩選集合元素
          transform:根據指定方法處理集合元素,類似List的map()
          filter:過濾元素,雷瑟List的filter()
          find:基本和select一樣
          collect:和transform 差不多一樣,但是返回新數組
          forAllDo:調用每個元素的指定方法
          isEqualCollection:判斷兩個集合是否一致

          十三. org.apache.commons.lang3.ArrayUtils

          contains:是否包含某個字符串
          addAll:添加整個數組
          clone:克隆一個數組
          isEmpty:是否空數組
          add:向數組添加元素
          subarray:截取數組
          indexOf:查找某個元素的下標
          isEquals:比較數組是否相等
          toObject:基礎類型數據數組轉換為對應的Object數組

          十四. org.apache.commons.beanutils.PropertyUtils

          getProperty:獲取對象屬性值
          setProperty:設置對象屬性值
          getPropertyDiscriptor:獲取屬性描述器
          isReadable:檢查屬性是否可訪問
          copyProperties:復制屬性值,從一個對象到另一個對象
          getPropertyDiscriptors:獲取所有屬性描述器
          isWriteable:檢查屬性是否可寫
          getPropertyType:獲取對象屬性類型

          十五. org.apache.commons.lang3.StringEscapeUtils

          unescapeHtml4:轉義html
          escapeHtml4:反轉義html
          escapeXml:轉義xml
          unescapeXml:反轉義xml
          escapeJava:轉義unicode編碼
          escapeEcmaScript:轉義EcmaScript字符
          unescapeJava:反轉義unicode編碼
          escapeJson:轉義json字符
          escapeXml10:轉義Xml10

          這個現在已經廢棄了,建議使用commons-text包里面的方法。

          十六. org.apache.commons.beanutils.BeanUtils

          copyPeoperties:復制屬性值,從一個對象到另一個對象
          getProperty:獲取對象屬性值
          setProperty:設置對象屬性值
          populate:根據Map給屬性復制
          copyPeoperty:復制單個值,從一個對象到另一個對象
          cloneBean:克隆bean實例

          關注

          感謝閱讀,如果這篇文章幫助了您,歡迎 點贊收藏,關注轉發 喲。您的幫助是我們前行的動力,我們會提供更多有價值的內容給大家... 謝謝!


          主站蜘蛛池模板: 高清一区二区三区视频| 亚洲一区二区三区久久久久| 熟女少妇精品一区二区| 亚洲无线码一区二区三区| 免费高清av一区二区三区| 免费视频精品一区二区三区| 亚洲中文字幕在线无码一区二区| 无码人妻一区二区三区在线水卜樱| 国产福利电影一区二区三区| 国产精品毛片一区二区 | 日韩一区二区在线免费观看| 自慰无码一区二区三区| 国产一区二区三区在线免费观看 | 久久久精品人妻一区二区三区蜜桃| 国产第一区二区三区在线观看 | 国产成人高清精品一区二区三区| 亚洲一区精彩视频| 国产成人精品一区二区三区无码| 国产精品va一区二区三区| 伊人无码精品久久一区二区| 久久综合精品不卡一区二区| 日韩精品在线一区二区| 一区二区三区免费在线视频| 无码中文字幕一区二区三区| 国产成人精品日本亚洲专一区| 国产精品伦一区二区三级视频| 久久亚洲色一区二区三区| 国产av一区二区三区日韩 | 一区视频在线播放| 国产日韩精品一区二区在线观看| 夜夜精品视频一区二区| 亚洲欧美一区二区三区日产| 日韩免费一区二区三区在线播放 | 麻豆国产一区二区在线观看 | 激情爆乳一区二区三区| 在线不卡一区二区三区日韩| 精品国产一区在线观看| 亚洲av高清在线观看一区二区| 一区二区三区观看| 无码人妻一区二区三区免费n鬼沢 无码人妻一区二区三区免费看 | 一区二区在线播放视频|