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

2021-02-21 Java後端
來源:zhuanlan.zhihu.com/p/95560389

完整代碼:https://github.com/PuZhiweizuishuai/SpringSecurity-JWT-Vue-Deom

運行展示

後端

主要展示 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

addAuthentication 方法負責返回登陸成功的信息,使用 HTTP Only 的 Cookie 可以有效防止 XSS 攻擊。登陸成功後返回用戶的權限,用戶名,登陸過期時間,可以有效的幫助前端構建合適的用戶界面。getAuthentication 方法負責對用戶的其它請求進行驗證,如果用戶的 JWT 解析正確,則向 Spring Security 返回 usernamePasswordAuthenticationToken 用戶名密碼驗證令牌,告訴 Spring Security 用戶所擁有的權限,並放到當前的 Context 中,然後執行過濾鏈使請求繼續執行下去。ps:其中的 LoginResultDetails 類和 ResultDetails 請看項目源碼,篇幅所限,此處不在贅述。JWT 過濾器配置眾所周知,Spring Security 是藉助一系列的 Servlet Filter 來來實現提供各種安全功能的,所以我們要使用 JWT 就需要自己實現兩個和 JWT 有關的過濾器一個是用戶登錄的過濾器,在用戶的登錄的過濾器中校驗用戶是否登錄成功,如果登錄成功,則生成一個 token 返回給客戶端,登錄失敗則給前端一個登錄失敗的提示。第二個過濾器則是當其他請求發送來,校驗 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 繼承自 AbstractAuthenticationProcessingFilter,並實現其中的三個默認方法,其中的 defaultFilterProcessesUrl 變量就是我們需要設置的登陸路徑attemptAuthentication 方法中,我們從登錄參數中提取出用戶名密碼,然後調用 AuthenticationManager.authenticate() 方法去進行自動校驗。第二步如果校驗成功,就會來到 successfulAuthentication 回調中,在 successfulAuthentication 方法中,使用之前已經寫好的 addAuthentication 來生成 token,並使用 Http Only 的 cookie 寫出到客戶端。第二步如果校驗失敗就會來到 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 官方文檔,配置參考部分依賴包前後端數據傳遞我使用了更為簡單的 fetch api, 當然你也可以選擇兼容性更加好的 axios為了獲取 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);
            }
        });
},

結束暫時就先寫這些吧,如果你有什麼問題或者好的建議,歡迎在評論區提出。最近整理一份面試資料《Java技術棧學習手冊》,覆蓋了Java技術、面試題精選、Spring全家桶、Nginx、SSM、微服務、資料庫、數據結構、架構等等。

獲取方式:點「 在看,關注公眾號 Java後端 並回復 777 領取,更多內容陸續奉上。

喜歡文章,點個在看 

相關焦點

  • SpringSecurity+JWT實現前後端分離的使用
    SpringSecurity+JWT實現前後端分離的使用
  • SpringBoot整合SpringSecurity實現JWT認證
    /jpgzhu/article/details/105200598前言微服務架構,前後端分離目前已成為網際網路項目開發的業界標準,其核心思想就是前端(APP、小程序、H5 頁面等)通過調用後端的 API 接口,提交及返回 JSON 數據進行交互。
  • Spring Security 結合 Jwt 實現無狀態登錄
    1 無狀態登錄1.1 什麼是有狀態有狀態服務,即服務端需要記錄每次會話的客戶端信息,從而識別客戶端身份,根據用戶身份進行請求的處理,典型的設計如Tomcat中的Session。例如登錄:用戶登錄後,我們把用戶的信息保存在服務端session中,並且給用戶一個cookie值,記錄對應的session,然後下次請求,用戶攜帶cookie值來(這一步有瀏覽器自動完成),我們就能識別到對應session,從而找到用戶的信息。這種方式目前來看最方便,但是也有一些缺陷,如下:1.2 什麼是無狀態微服務集群中的每個服務,對外提供的都使用 RESTful 風格的接口。
  • Spring Security 真正的前後分離實現
    Spring Security網絡上很多前後端分離的示例很多都不是完全的前後分離,而且大家實現的方式各不相同,有的是靠自己寫攔截器去自己校驗權限的,有的頁面是使用themleaf來實現的不是真正的前後分離,看的越多對Spring Security越來越疑惑,此篇文章要用最簡單的示例實現出真正的前後端完全分離的權限校驗實現。
  • 整合SpringSecurity和JWT實現登錄認證和授權
    前後端分離的開發模式中,接口的認證與授權是重中之重。由於接口都是無狀態的,那勢必就需要一套認證和授權框架來解決「你是誰」以及「你能幹什麼」的問題,所以,這篇文章就來聊一聊如何整合SpringSecurity和JWT實現登錄認證和授權。
  • 超全的springboot+springsecurity實現前後端分離簡單實現!
    通過各種方式學習springsecurity,在B站、騰訊課堂、網易課堂、慕課網沒有springsecurity的前後端分離的教學視頻,那我就去csdn去尋找springsecurity博客,發現幾個問題:  實在不行我又跑去github上找開源項目學習,github由於是外國網站,國內訪問速度有點慢!!
  • SpringBoot Security 集成JWT實現接口可信賴認證
    =levispring.security.user.password=123456MVC Security默認的安全配置在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中實現。
  • Spring Security 簡單教程以及實現完全前後端分離
    在新手入門使用時,只需要簡單的配置,即可實現登錄以及權限的管理,無需自己寫功能邏輯代碼。但是對於現在大部分前後端分離的web程序,尤其是前端普遍使用ajax請求時,spring security自帶的登錄系統就有一些不滿足需求了。因為spring security有自己默認的登錄頁,自己默認的登錄控制器。而登錄成功或失敗,都會返回一個302跳轉。
  • Spring Boot 整合Spring Security示例實現前後分離權限註解+JWT登錄認證
    前言SpringSecurity是一個用於Java企業級應用程式的安全框架,主要包含用戶認證和用戶授權兩個方面。
  • SpringBoot整合SpringSecurity示例實現前後分離權限註解+JWT登錄認證
    <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency>
  • Springboot + Vue + shiro 實現前後端分離、權限控制
    一、前後端分離思想前端從後端剝離,形成一個前端工程,前端只利用Json來和後端進行交互,後端不返回頁面,只返回Json數據。前後端之間完全通過public API約定。二、後端 SpringbootSpringboot就不再贅述了,Controller層返回Json數據。
  • 手把手教你實現JWT Token
    前言Json Web Token (JWT) 近幾年是前後端分離常用的 Token 技術,是目前最流行的跨域身份驗證解決方案。你可以通過文章 一文了解web無狀態會話token技術JWT 來了解 JWT。今天我們來手寫一個通用的 JWT 服務。
  • 教你玩轉Vue和Django的前後端分離
    本文教你玩轉 django 及 vue 的前後端分離。有問題請關注公眾號 somenzz,後臺留言與我交流。></script>說到這裡,什麼是前後端分離,有一個簡潔的判斷標準:第一次在瀏覽器中輸入網址請求伺服器,此時伺服器返回一個 html 頁面,即首頁,一般是 index.html,在後續的請求中,只要伺服器端返回 html 頁面,就不是前後端分離,只要伺服器返回的是純數據,就是前後端分離,跟所用的語言,框架,沒有任何關係。
  • 在實戰中學習Springboot+Security+redis+jwt的登錄流程
    -- spring-security --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId
  • Django+JWT實現Token認證
    基於Token的鑑權機制越來越多的用在了項目中,尤其是對於純後端只對外提供API沒有web頁面的項目,例如我們通常所講的前後端分離架構中的純後端服務,只提供API給前端,前端通過API提供的數據對頁面進行渲染展示或增加修改等,我們知道HTTP是一種無狀態的協議,也就是說後端服務並不知道是誰發來的請求,那麼如何校驗請求的合法性呢?
  • mallcloud商城 `Spring Cloud Finchley`和`Spring Cloud Alibaba`
    * 基於`Spring Boot 2.0.X`、`Spring Cloud Finchley`和`Spring Cloud Alibaba`* 深度定製`Spring Security`真正實現了基於`RBAC`、`jwt`和`oauth2`的無狀態統一權限認證的解決方案* 提供應用管理,方便第三方系統接入* 引入組件化的思想實現高內聚低耦合,
  • SpringBoot Security + JWT Hello World示例
    在下一個教程中,我們將實現Spring Boot + JWT + MySQL JPA,用於存儲和獲取用戶憑證。任何用戶只有擁有有效的JSON Web Token(JWT)才能使用此API。在之前的教程中,我們學習了《什麼是JWT?》 以及何時並如何使用它。
  • SpringCloud Alibaba微服務實戰二十一 - JWT增強
    ;import org.springframework.security.oauth2.common.OAuth2AccessToken;import org.springframework.security.oauth2.provider.OAuth2Authentication;import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter