文章目录
前言
项目中使用的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的概念比较多,我也还没完全掌握,有很多地方解析不清楚的就先贴代码直接跳过了,后续再回头完善,还请路过的大佬多多指教!