前端vue+axios与后端springboot+security整合


前言

项目中使用的springboot版本为2.3.8.RELEASE,spring security的版本为5.3.x。
记录了前后的分离项目如何配置cors跨域请求、security如何认证鉴权、以及开启csrf前后端的配置。


一、依赖导入

1、前端引入axios

npm install axios -S

vue安装axios并添加到package.json的依赖中。

2、后端pom添加security依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

不需要注明版本,根据springboot的版本会导入对应的security版本。

二、自定义WebSecurityConfigurerAdapter类

WebSecurityConfigurerAdapter 类是spring security的适配器, 需要我们自己写个配置类去继承他,然后编写自己所特殊需要的配置。

1、创建SecurityConfig类继承WebSecurityConfigurerAdapter

package com.pp.paopao.config.security;

import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.pp.paopao.domain.response.ExecuteResult;
import com.pp.paopao.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.CorsUtils;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Author hdq
 * @Date 2021/2/4
 * @Description
 **/
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Autowired
    private AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler;

    @Bean
    public UserDetailsService userDetailsService() {
        //获取用户账号密码及权限
        return new UserDetailsServiceImpl();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        //设置默认的加密方式(强hash加密方式)
        return new BCryptPasswordEncoder();
    }

    //跨域配置1
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        //修改为添加而不是设置,* 最好改为实际的需要
        configuration.addAllowedOrigin("*");
        //修改为添加而不是设置
        configuration.addAllowedMethod("*");
        //这里很重要,起码需要允许 Access-Control-Allow-Origin
        configuration.addAllowedHeader("*");
        configuration.setAllowCredentials(true);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //配置认证方式等
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //http相关的配置,包括登入登出、异常处理、会话管理等
        http.authorizeRequests()
                // 跨域配置2:cors 预检请求放行,让Spring security 放行所有preflight request(cors 预检请求)
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                //拦截所有请求,需要认证之后才能访问
                .anyRequest().authenticated()
                .and()
                //表单提交
                .formLogin()
                //登录url
                .loginProcessingUrl("/login")
                .permitAll()
                .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        StringBuffer sb = new StringBuffer();
                        sb.append("{\"status\":\"error\",\"msg\":\"");
                        if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
                            sb.append("用户名或密码输入错误,登录失败!");
                        } else if (e instanceof DisabledException) {
                            sb.append("账户被禁用,登录失败,请联系管理员!");
                        } else {
                            sb.append("登录失败!");
                        }
                        sb.append("\"}");
                        out.write(sb.toString());
                        out.flush();
                        out.close();
                    }
                })
                .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        httpServletResponse.setContentType("application/json;charset=utf-8");
                        PrintWriter out = httpServletResponse.getWriter();
                        ObjectMapper objectMapper = new ObjectMapper();
                        ExecuteResult result = userService.getRoleByName(authentication.getName());
                        String s = "{\"status\":\"success\",\"msg\":" +
                                objectMapper.writeValueAsString(JSONObject.toJSONString(result.getData()))
                                + "}";
                        out.write(s);
                        out.flush();
                        out.close();
                    }
                })
                .and()
                .logout()
                .logoutUrl("/logout")
                .permitAll()
                .and()
                .exceptionHandling()
                //自定义权限不足处理器
                .accessDeniedHandler(authenticationAccessDeniedHandler);

        //跨域配置3
        http.cors(Customizer.withDefaults());


        http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .requireCsrfProtectionMatcher(new CsrfSecurityRequestMatcher());
    }
}

以上SecurityConfig类中做了几个事:

1.1允许跨域

①CorsConfigurationSource 配置了跨域请求的请求源地址、请求方法、请求头;底层方法getCorsFilter会读取这个bean
②http.authorizeRequests()security请求授权中配置了不拦截跨域请求的预请求;
③http.cors(Customizer.withDefaults());配置使用自定义的规则;

1.2自定义了用户登录校验和密码加密与校验

	@Bean
    public UserDetailsService userDetailsService() {
        //获取用户账号密码及权限
        return new UserDetailsServiceImpl();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        //设置默认的加密方式(强hash加密方式)
        return new BCryptPasswordEncoder();
    }

其中UserDetailsServiceImpl()是自己写的实现类,代码下面会贴出。通过spring容器引入,因为程序在启动时就需要使用到,不能在用的地方直接new,同样的passwordEncoder也是。
引入之后,需要在重写的方法configure(AuthenticationManagerBuilder auth)中配置。
注:创建用户时密码加密保存也是导入passwordEncoder进行加密。

1.3spring security的认证配置

通过重写configure(HttpSecurity http)方法,首先拦截了所有的请求(这里不包括上面说的跨域预请求,因为跨域预请求写在前面,security过滤的时候会从上往下进行匹配),在对登入、登出做过滤。
这里因为传输的账号和密码参数与默认的一致,我就省略了,否则需要配置 .usernameParameter()、.passwordParameter()参数;
然后在认证成功时我返回了用户的角色List,方便前端根据角色动态生成表单,也因为这样,所以不会在controller层写需要哪些权限才可以访问。
然后还配置了登出和权限不足的返回结果authenticationAccessDeniedHandler,authenticationAccessDeniedHandler的实现代码会在下面贴出。

1.4csrf防护配置

一般情况下,生产环境是不建议关闭csrf防护的,spring security也是默认开启的,在开启的情况下,需要前后端同时做一些配置才能访问,否则请求直接返回403.

http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .requireCsrfProtectionMatcher(new CsrfSecurityRequestMatcher());

这块代码设置了csrf需要前端请求时带上cookie,并且使用自定义的CsrfSecurityRequestMatcher。
这里参考了后端SpringSecurity+vue前端axios+uni-app解决跨站点请求伪造CSRF](https://blog.csdn.net/weixin_42739423/article/details/107114700)

1.5相关实现类

UserDetailsServiceImpl 类,用户认证的实现。

package com.pp.paopao.config.security;

import com.pp.paopao.domain.SysPermission;
import com.pp.paopao.domain.SysRole;
import com.pp.paopao.domain.SysUser;
import com.pp.paopao.repository.SysUserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.*;

/**
 * @Author hdq
 * @Date 2021/2/24
 * @Description
 **/
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();

        Optional<SysUser> sysUser = sysUserMapper.getSysUserByName(s);

        if (!sysUser.isPresent()) {
            throw new UsernameNotFoundException("用户名不存在!");
        }

        //添加权限
        SysUser sysUser1 = sysUser.get();
        Set<SysPermission> permissions = new HashSet<>();
        List<SysRole> roles = sysUser1.getRoles();
        roles.forEach(role -> permissions.addAll(role.getPermissions()));

        permissions.forEach(permission -> authorities.add(new SimpleGrantedAuthority(permission.getCode())));

        //返回UserDetails实现类
        return new User(sysUser1.getName(), sysUser1.getPassword(), authorities);
    }
}

package com.pp.paopao.config.security;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * @Author hdq
 * @Date 2021/3/12
 * @Description 自定义403响应的内容
 **/
@Component
public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException {
        resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();
        out.write("{\"status\":\"error\",\"msg\":\"权限不足,如需继续请联系管理员!\"}");
        out.flush();
        out.close();
    }
}

package com.pp.paopao.config.security;

import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;

import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;

/**
 * @Author hdq
 * @Date 2021/3/14
 * @Description
 **/
public class CsrfSecurityRequestMatcher implements RequestMatcher {
    private Pattern allowedMethods = Pattern.compile("^(GET|HEAD|TRACE|OPTIONS)$");
    //csrf按请求路径URL过滤,AntPathRequestMatcher实现/miniapi路径下请求不进行拦截
    //private AntPathRequestMatcher antPathRequestMatcher = new AntPathRequestMatcher("/miniapi/**","/");

    @Override
    public boolean matches(HttpServletRequest request) {
        if (allowedMethods.matcher(request.getMethod()).matches()) {
            return false;
        }
        //boolean b = !antPathRequestMatcher.matches(request);
        boolean b = true;
        return b;
    }
}

2.前端配置

2.1代理配置

由于前端vue代码用的是vue/cli 4.5.0版本生成的,目录与其他版本可能不同,我在根目录下的vue.config.js中写了代理的配置

module.exports = {
    devServer: {
        //true : 开启热更新, false: 关闭热更新
        inline: true,
        //本地服务器端口
        port: 8080,
        //代理地址
        proxy: "http://localhost:5830",
    },
};

在这里插入图片描述

2.2前端csrf配置

在src目录下的main.js中加入axios的相关配置,由于spring security开启csrf时,会在正式请求前进行一次预请求,拿到键为_csrf值为token的返回信息,需要在正式的请求中带上。如果是前后端不分离或者使用jsp的项目,可以参考spring的官方文档。我项目中前端使用了vue+axios,所以需要在axios请求中配置axios.defaults.xsrfCookieName = ‘XSRF-TOKEN’、axios.defaults.xsrfHeaderName = ‘X-XSRF-TOKEN’

// 设置反向代理,前端请求默认发送到 http://localhost:5830
var axios = require('axios')
axios.defaults.baseURL = 'http://localhost:5830'
// 设置cross跨域,并设置访问权限,允许跨域
axios.defaults.withCredentials = true
axios.defaults.crossDomain = true
// 设置post请求头
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'
// 设置put请求头
axios.defaults.headers.put['Content-Type'] = 'application/json;charset=utf-8'
// 请求超时响应
axios.defaults.timeout = 60000
// 请求响应格式
axios.defaults.responseType = 'json'
// 设置csrf请求头
axios.defaults.xsrfCookieName = 'XSRF-TOKEN'
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN'
// 全局注册,之后可在其他组件中通过 this.$axios 发送数据
Vue.prototype.$axios = axios

在这里插入图片描述

总结

本文简单记录了我在使用spring security在前后端分离项目中的相关配置,spring security的概念比较多,我也还没完全掌握,有很多地方解析不清楚的就先贴代码直接跳过了,后续再回头完善,还请路过的大佬多多指教!

  • 1
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值