Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring Security+JWT+Vue 手撸一个前后端分离无状态认证 Demo

Spring Security+JWT+Vue 手撸一个前后端分离无状态认证 Demo

作者头像
JavaFish
发布于 2020-07-03 09:41:28
发布于 2020-07-03 09:41:28
5.8K00
代码可运行
举报
运行总次数:0
代码可运行

完整代码: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 则不需要。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<dependency>
   <groupId>javax.xml.bind</groupId>
   <artifactId>jaxb-api</artifactId>
   <version>2.3.0</version>
</dependency>

要使用 Spring Security 实现对用户的权限控制,首先需要实现一个简单的 User 对象实现 UserDetails 接口,UserDetails 接口负责提供核心用户的信息,如果你只需要用户登陆的账号密码,不需要其它信息,如验证码等,那么你可以直接使用 Spring Security 默认提供的 User 类,而不需要自己实现。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 类,用来处理认证过程中的验证和请求

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的身份验证请求的抽象处理器。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 类下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 类下

第二个用户过滤器

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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, 代码如下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@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 没必要,我就放弃了。

另外据说安全信息暴露的越少越好,所以暂时就先这样吧。(算是给自己找个理由)

用户查找服务

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 文件如下。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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 的跨域配置

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,也就等于没有登陆了。

举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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);
            }
        });
},

作者:陕西颜值扛把子 来源:zhuanlan.zhihu.com/p/95560389

-END-

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一个优秀的废人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
WordPress个人博客搭建(一):在 Ubuntu 系统安装/卸载 1Panel 面板
1Panel 是一款基于 Go 语言开发的现代化开源服务器管理面板(类似宝塔面板),专注于容器化(Docker)和云原生环境管理,提供可视化界面简化服务器运维操作。
云惑雨问
2025/05/16
4560
WordPress个人博客搭建(一):在 Ubuntu 系统安装/卸载 1Panel 面板
1Panel V2.0 震撼来袭!PHP运行环境支持扩展管理和Debian镜像
‌1Panel是一款现代化、开源的Linux服务器运维管理面板‌,由杭州飞致云信息科技有限公司开发,提供Web图形化界面以简化服务器管理任务,如主机监控、文件管理、数据库和容器管理等。‌‌
Tinywan
2025/06/08
1250
1Panel V2.0 震撼来袭!PHP运行环境支持扩展管理和Debian镜像
低成本搭建高质量 WordPress 博客实践指南
近期,作者发现之前撰写的文章,在外网被各种途径转载,很难进行版权保护。因此,萌生了搭建个人博客网站的想法。
rileycai
2022/07/13
3.2K1
低成本搭建高质量 WordPress 博客实践指南
5分钟玩转Lighthouse|零基础也能拥有WordPress个人博客
【5分钟玩转Lighthouse】系列文章将为大家分享轻量应用服务器使用教程,提供丰富的实践指南,帮助大家快速上手并获得最佳产品体验。本期主要介绍如何借力 Lighthouse 实例,快速搭建基于 WordPress 的个人博客网站。 背景概述 腾讯云轻量应用服务器 Lighthouse 是新一代面向中小企业及开发者的云服务器产品,简单易用,一站式融合多款云服务,能帮助用户在云端快速构建网站、博客、电商、论坛等各类应用以及各类开发测试环境。 WordPress 作为全球最流行的开源博客和内容管理网站的
腾讯云计算产品团队
2020/09/25
2.6K1
WordPress - 基于 Ubuntu 16.04 搭建个人博客
购买了腾讯云服务器后, 准备开始搭建个人博客了, 这里根据网上的各种资源整理一下.(终端下进行) 如果是学习的话强烈推荐 基于 Ubuntu 搭建 WordPress 个人博客 . 这也是这里主要参考的. 提供了实验环境.
AIHGF
2019/02/27
2.7K0
利用WordPress搭建属于自己的网站
怎么用WordPress给自己搭建了一个网站?可能很多人都想拥有属于自己的网站,这篇文章就找你怎么利用WordPress搭建属于自己的网站。如果你也正好有搭建个人网站的想法,那么本文会给你一个参考,我尽量写的比较详细,给自己做一个记录,也给大家一个参考。
张飞的猪
2022/12/28
1.4K1
利用WordPress搭建属于自己的网站
如何使用Cloudways搭建WordPress网站
如今,搭建网站已经变得非常简单,这主要得益于开源的CMS建站系统的兴起。即使是不懂编程的人也能轻松搭建自己的网站,这些CMS系统提供了丰富的主题模板和插件,使用户可以通过简单的拖放和配置操作来建立自己的网站。
无代码Dev
2024/05/27
2640
如何使用Cloudways搭建WordPress网站
手动搭建 WordPress 个人站点(Windows)
WordPress 是一款使用 PHP 语言开发的博客平台,您可使用通过 WordPress 搭建属于个人的博客平台。本文以 Windows Server 2012 操作系统的腾讯云云服务器为例,手动搭建 WordPress 个人站点。
用户10230909
2023/04/25
11.2K1
我的Serverless实战—基于Serverless搭建WordPress个人博客图文教程
serverless中的server是服务器的意思,less是没有的意思,顾名思义也就是无服务器
炒香菇的书呆子
2021/05/06
1.8K0
【5分钟玩转Lighthouse】搭建WordPress博客
我们本期【5分钟玩转Lighthouse】系列教程中,将为大家介绍如何借力腾讯云Lighthouse实例,快速搭建基于WordPress的个人博客网站。
溪歪歪
2020/09/04
19.4K19
【5分钟玩转Lighthouse】搭建WordPress博客
香橙派开发板玩法分享:Docker部署1Panel打造全能控制台远程访问
详细很多人一听到到要远程管理服务器就头疼得不得了,特别是没有公网IP或路由器设置复杂时,简直让人抓狂!但今天我要告诉大家一个绝招——在CasaOS香橙派Zero3上用Docker部署1Panel开源运维面板,并结合cpolar内网穿透实现远程访问。想象一下,无论你是在世界的哪个角落,还是躺在家里的沙发上,都能轻松管理服务器,是不是感觉自己瞬间高大上了?对于运维人员和个人站长来说,这简直就是一大福音!最近看到不少小伙伴在fnOS上分享1Panel的使用体验,真心觉得它让Docker容器管理变得简单又高效。今天就来手把手教大家如何在这台小巧但强大的设备上部署1Panel,并通过cpolar内网穿透实现远程访问。准备好了吗?让我们一起开启这场运维革命吧!
FGGIT
2025/04/01
2140
香橙派开发板玩法分享:Docker部署1Panel打造全能控制台远程访问
好物分享38-用宝塔面板快速搭建个人博客
最近我在尝试使用七牛云图床服务使用自定义域名,由于域名需要备案,在这个过程中发现,备案初审前至管局审核需注意什么? (aliyun.com)[2]
北野茶缸子
2022/12/10
1.1K0
好物分享38-用宝塔面板快速搭建个人博客
『云产品最佳实践』1Panel 搭建操作指南
随着云计算的普及,利用云服务器快速搭建 1Panel 运维面板 已成为一种高效且灵活的解决方案。尤其是 轻量应用服务器,以其 高性价比 和 简单易用 的优势,成为搭建运维环境的理想选择。
程序员NEO
2024/11/17
6830
『云产品最佳实践』1Panel 搭建操作指南
远程管理服务器从未如此简单:1Panel面板结合cpolar内网穿透轻松搞定
不知道大家是不是经常为远程管理服务器头疼?特别是当你没有公网IP或者复杂的路由器设置时,想要轻松搞定服务器运维简直就是天方夜谭!但今天我要给大家介绍一个超级实用的神器组合——在CasaOS轻NAS系统的香橙派Zero3上使用Docker本地部署1Panel开源Linux服务器运维管理面板,并结合cpolar内网穿透实现浏览器远程访问。
命运之光
2025/01/07
4160
远程管理服务器从未如此简单:1Panel面板结合cpolar内网穿透轻松搞定
新手级Wordpress博客搭建三:Wordpress、LNMP、SSL证书安装部署
输入账号密码登录,将会出现一个协议,阅读完协议(如果不阅读,直接拉到底就可以)才能继续下一步。
Evo
2022/06/07
1.2K0
新手级Wordpress博客搭建三:Wordpress、LNMP、SSL证书安装部署
腾讯云轻量应用服务器 | 宝塔Linux面板快速搭建和配置网站指南
使用腾讯云轻量应用服务器结合宝塔Linux面板搭建网站是一种高效、便捷的方式,适合初学者和需要快速上线的网站管理员。
数字扫地僧
2024/11/12
1.5K0
腾讯云轻量应用服务器 | 宝塔Linux面板快速搭建和配置网站指南
通过搭建wordpress博客来学习云服务器的详细使用方法
云服务器越来越普及,很多企业及个人都纷纷上云,从传统的虚拟主机或者托管服务器上把网站和应用迁移到腾讯云、阿里云、百度云等这些云服务器上。
尊托云数
2019/07/25
3.2K0
通过搭建wordpress博客来学习云服务器的详细使用方法
开源免费的Linux服务器管理面板分享
江湖有缘
2024/03/02
4.3K0
开源免费的Linux服务器管理面板分享
轻松搭建高效文件管理系统:轻量云服务器+1Panel一键安装Alist
随着云计算技术的不断发展,越来越多的个人开发者、团队和企业开始使用云服务器来部署各种应用。腾讯云的轻量云服务器(LCS)因其价格亲民、操作简便、配置灵活的特点,受到了许多开发者的青睐。而在众多轻量应用中,Alist作为一个开源的文件管理工具,具有高度的可定制性和简洁的用户界面,成为了很多用户的首选。本文将介绍如何在腾讯云轻量云服务器上使用1Panel,快速搭建Alist文件管理系统。
不惑
2024/11/19
1K0
轻松搭建高效文件管理系统:轻量云服务器+1Panel一键安装Alist
Typecho 入门指南:个人博客网站保姆级攻略!
Typecho 是一款基于 PHP + MySQL/SQLite 的 轻量级开源博客系统,专为内容创作者设计。其核心目标是提供 简洁、快速、高效 的博客搭建体验。
云惑雨问
2025/03/21
3330
Typecho 入门指南:个人博客网站保姆级攻略!
推荐阅读
相关推荐
WordPress个人博客搭建(一):在 Ubuntu 系统安装/卸载 1Panel 面板
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验