Spring Security 认证
1.示例工程
示例工程见:https://gitee.com/ixinglan/spring-security-demo.git
2.SpringSecurity认证基本原理与认证2种方式
Spring Security功能的实现主要是由一系列过滤器相互配合完成。也称之为过滤器链
2.1 过滤器链介绍
过滤器是一种典型的AOP思想,默认加载15个过滤器(SpringBoot 2.6.3版本 )
org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter
根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调用处理拦截器
org.springframework.security.web.context.SecurityContextPersistenceFilter
SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。
org.springframework.security.web.header.HeaderWriterFilter
向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制
org.springframework.web.filter.CorsFilter
跨域相关
org.springframework.security.web.csrf.CsrfFilter
csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。
org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
com.example.demosecuritymanagerment.filter.ValidateCodeFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求
org.springframework.security.web.savedrequest.RequestCacheAwareFilter
通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存HttpServletRequest
org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
针对ServletRequest进行了一次包装,使得request具有更加丰富的API
org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter
记住我
org.springframework.security.web.authentication.AnonymousAuthenticationFilter
当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程,只不过是一个匿名的身份。
org.springframework.security.web.session.SessionManagementFilter
securityContextRepository限制同一用户开启多个会话的数量
org.springframework.security.web.access.ExceptionTranslationFilter
异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异常
org.springframework.security.web.access.intercept.FilterSecurityInterceptor
获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其是否有权限
2.2 认证方式
HttpBasic认证
HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式
在使用的Spring Boot早期版本为1.X版本,依赖的Security 4.X版本,那么就无需任何配置,启动项目访问则会弹出默认的httpbasic认证。现在使用的是spring boot2.0以上版本(依赖Security5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单模式,表单模式,参考前面1-基础工程中页面
HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 “admin” ,密码是“ admin”,则将字符串"admin:admin" 使用Base64编码算法加密。加密结果可能是:YWtaW46YWRtaW4=
formLogin登录认证模式
spring boot2.0以上版本(依赖Security 5.X版本)默认会生成一个登录页面.
3.表单认证
3.1 自定义表单登录页面
添加配置类
/**
* Security配置类
*/
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
/**
* http请求处理方法
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic()//开启httpbasic认证
.and().authorizeRequests().
anyRequest().authenticated();//所有请求都需要登录认证才能访问*/
http.formLogin()//开启表单认证
.and().authorizeRequests().
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
}
}
存在问题:
重定向次数过多
因为设置登录页面为login.html 后面配置的是所有请求都登录认证,陷入了死循环. 所以需要将login.html放行不需要登录认证
http.formLogin().loginPage("/login.html")//开启表单认证 .and().authorizeRequests(). antMatchers("/login.html").permitAll()//放行登录页面 .anyRequest().authenticated();//所有请求都需要登录认证才能访问
访问login.html 报404错误
spring boot整合thymeleaf 之后 所有的静态页面以放在resources/templates下面,所以得通过请求访问到模板页面, 将/login.html修改为/toLoginPage
http.formLogin().loginPage("/toLoginPage")//开启表单认证 .and().authorizeRequests(). antMatchers("/toLoginPage").permitAll()//放行登录页面 .anyRequest().authenticated();//所有请求都需要登录认证才能访问
访问login.html 后发现页面没有相关样式
因为访问login.html需要一些js , css , image等静态资源信息, 所以需要将静态资源放行, 不需要认证
@Override public void configure(WebSecurity web) throws Exception { //解决静态资源被拦截的问题 web.ignoring().antMatchers("/css/**", "/images/**", "/js/**", "/code/**"); }
HttpSecurity 和WebSecurity 的区别是:
WebSecurity 不仅通过HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些请求可以忽略安全控制;
HttpSecurity 仅用于定义需要安全控制的请求(当然HttpSecurity 也可以指定某些请求不需要安全控制);
可以认为HttpSecurity 是WebSecurity 的一部分, WebSecurity 是包含HttpSecurity 的更大的一个概念;
构建目标不同
WebSecurity 构建目标是整个Spring Security 安全过滤器FilterChainProxy
HttpSecurity 的构建目标仅仅是FilterChainProxy 中的一个SecurityFilterChain 。
3.2 表单登录
表单中的input的name值是username和password, 并且表单提交的路径为/login , 表单提交方式method为post , 这些可以修改为自定义的值.
@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic()//开启httpbasic认证
.and().authorizeRequests().
anyRequest().authenticated();//所有请求都需要登录认证才能访问*/
http.formLogin()//开启表单认证
.loginPage("/toLoginPage")//自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
.usernameParameter("username").passwordParameter("password"). //修改
自定义表单name值.
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()//放行登录页面
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
// 关闭csrf防护
http..csrf().disable();
}
发现行内框架iframe这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非SpringSecurity环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何 Frame页面,设置含义如下:
- DENY:浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.
- SAMEORIGIN:frame页面的地址只能为同源域名下的页面
允许加载iframe:
http.formLogin()//开启表单认证
.loginPage("/toLoginPage")//自定义登录页面
.loginProcessingUrl("/login")// 登录处理Url
.usernameParameter("username").passwordParameter("password"). //修改自定义表单name值.
.successForwardUrl("/")// 登录成功后跳转路径
.and().authorizeRequests().
antMatchers("/toLoginPage").permitAll()//放行登录页面与静态资源
.anyRequest().authenticated();//所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
3.3 基于数据库实现认证
之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面loadUserByUsername即可
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
UserService userService;
@Autowired
PermissionService permissionService;
/**
* 根据用户名查询用户
*
* @param username 前端传入的用户名
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户没有找到," + username);
}
// 权限的集合, 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<GrantedAuthority> authorities = new ArrayList<>();
UserDetails userDetails = new org.springframework.security.core.userdetails.User
(username, "{bcrypt}" + user.getPassword(),//noop不使用密码加密 , bcrypt使用加密算法
true,// 用户是否启用
true,// 用户是否过期
true,// 用户凭证是否过期
true,// 用户是否锁定
authorities);
return userDetails;
}
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}
3.4 密码加密认证
在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加{noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨
Spring Security 中PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能:一个是匹配验证。另一个是密码编码。
BCrypt算法介绍:
任何应用考虑到安全,绝不能明文的方式保存密码。密码应该通过哈希算法进行加密。 有很多标准的算法比如SHA或者MD5,结合salt(盐)是一个不错的选择。 Spring Security 提供了BCryptPasswordEncoder类,实现Spring的PasswordEncoder接口使用BCrypt强哈希方法来加密密码。BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。
bcrypt算法相对来说是运算比较慢的算法,在密码学界有句常话:越慢的算法越安全。黑客破解成本越高.通过salt和const这两个值来减缓加密过程,它的加密时间(百ms级)远远超过md5(大概1ms左右)。对于计算机来说,Bcrypt 的计算速度很慢,但是对于用户来说,这个过程不算慢。bcrypt是单向的,而且经过salt和cost的处理,使其受攻击破解的概率大大降低,同时破解的难度也提升不少,相对于MD5等加密方式更加安全,而且使用也比较简单
bcrypt加密后的字符串形如:
$2a$10$wouq9P/HNgvYj2jKtUN8rOJJNRVCWvn1XoWy55N3sCkEHZPo3lyWq
其中$是分割符,无意义;2a是bcrypt加密版本号;10是const的值;而后的前22位是salt值;再然后的字符串就是密码的密文了;这里的const值即生成salt的迭代次数,默认值是10,推荐值12。
在项目中使用BCrypt:
PasswordEncoderFactories密码工厂器
public final class PasswordEncoderFactories {
private PasswordEncoderFactories() {
}
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
之前我们在项目中密码使用的是明文的是noop , 代表不加密使用明文密码, 现在用BCrypt只需要将noop 换成bcrypt 即可
3.5 获取当前登录用户
在传统web系统中, 我们将登录成功的用户放入session中, 在需要的时候可以从session中获取用户,那么Spring Security中我们如何获取当前已经登录的用户呢?
SecurityContextHolder
保留系统当前的安全上下文SecurityContext,其中就包括当前使用系统的用户的信息。
SecurityContext
安全上下文,获取当前经过身份验证的主体或身份验证请求令牌
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser")
@ResponseBody
public UserDetails getCurrentUser() {
UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return principal;
}
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser2")
@ResponseBody
public UserDetails getCurrentUser1(Authentication authentication) {
UserDetails principal = (UserDetails) authentication.getPrincipal();
return principal;
}
/**
* 获取当前登录用户
*/
@GetMapping("/loginUser3")
@ResponseBody
public UserDetails getCurrentUser2(@AuthenticationPrincipal UserDetails userDetails) {
return userDetails;
}
3.6 remember me记住我
简单token生成方法
Token=MD5(username+分隔符+expiryTime+分隔符+password)
注意: 这种方式不推荐使用, 有严重的安全问题. 就是密码信息在前端浏览器cookie中存放. 如果cookie被盗取很容易破解.
<div class="form-group"> <div > <!--记住我 name为remember-me value值可选true yes 1 on 都行--> <input type="checkbox" name="remember-me" value="true"/>记住我 </div> </div>
.and().rememberMe()//开启记住我功能 .tokenValiditySeconds(1209600)//token失效时间 默认是2周 .rememberMeParameter("remember-me")//自定义表单input值
持久化的token生成方法
token: 随机生成策略,每次访问都会重新生成
series: 登录序列号,随机生成策略。用户输入用户名和密码登录时,该值重新生成。使用
remember-me功能,该值保持不变
expiryTime: token过期时间。
CookieValue=encode(series+token)
.tokenRepository(getPersistentTokenRepository()) ----------- @Autowired DataSource dataSource; /** * 负责token与数据库之间的操作 */ @Bean public PersistentTokenRepository getPersistentTokenRepository() { JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl(); tokenRepository.setDataSource(dataSource);//设置数据源 //tokenRepository.setCreateTableOnStartup(true);//启动时帮助我们自动创建一张表, 第一次启动设置true 第二次启动设置false或者注释 return tokenRepository; }
项目启动成功后,观察数据库,会帮助我们创建persistent_logins表
3.7 自定义登录成功和失败、退出登录处理
在某些场景下,用户登录成功或失败的情况下用户需要执行一些后续操作,比如登录日志的搜集,或者在现在目前前后端分离的情况下用户登录成功和失败后需要给前台页面返回对应的错误信息, 有前台主导登录成功或者失败的页面跳转. 这个时候需要要到用到
- AuthenticationSuccessHandler
- AnthenticationFailureHandler
以下示例为异步登录操作时
.successHandler(myAuthenticationService)
.failureHandler(myAuthenticationService)//登录成功或者失败的处理
.and().logout().logoutUrl("/logout")
.logoutSuccessHandler(myAuthenticationService)
@Service
public class MyAuthenticationService implements AuthenticationSuccessHandler,
AuthenticationFailureHandler, LogoutSuccessHandler {
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Autowired
ObjectMapper objectMapper;
/**
* 登录成功后处理逻辑
*
* @param request
* @param response
* @param authentication
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("登录成功后继续处理......");
// 重定向到index页面,同步操作时这样处理
// redirectStrategy.sendRedirect(request, response, "/");
Map result = new HashMap();
result.put("code", HttpStatus.OK.value());//200
result.put("message", "登录成功");
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
/**
* 登录失败的处理逻辑
*
* @param request
* @param response
* @param exception
* @throws IOException
* @throws ServletException
*/
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
System.out.println("登录失败后继续处理......");
// 重定向到login页面,同步操作时这样处理
// redirectStrategy.sendRedirect(request, response, "/toLoginPage");
Map result = new HashMap();
result.put("code", HttpStatus.UNAUTHORIZED.value());//401
result.put("message", exception.getMessage());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(result));
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
System.out.println("退出之后继续处理...");
redirectStrategy.sendRedirect(request, response, "/toLoginPage");
}
}
4.图形验证码
spring security添加验证码大致可以分为三个步骤:
- 根据随机数生成验证码图片
- 将验证码图片显示到登录页面
- 认证流程中加入验证码校验
Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下:
/**
* 处理生成验证码的请求
*/
@RestController
@RequestMapping("/code")
public class ValidateCodeController {
public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
public final static int expireIn = 600; // 验证码有效时间 60s
//使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
@Autowired
public StringRedisTemplate stringRedisTemplate;
@RequestMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取访问IP
String remoteAddr = request.getRemoteAddr();
//生成验证码对象
ImageCode imageCode = createImageCode();
//生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr)
.set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
//通过IO流将生成的图片输出到登录页面上
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}
/**
* 用于生成验证码对象
*
* @return
*/
private ImageCode createImageCode() {
int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
//创建一个带缓冲区图像对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获得在图像上绘图的Graphics对象
Graphics g = image.getGraphics();
Random random = new Random();
//设置颜色、并随机绘制直线
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}
//生成随机数 并绘制
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand.toString());
}
/**
* 获取随机演示
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}
}
/**
* 验证码过滤器 OncePerRequestFilter 一次请求只会经过一次过滤器
*/
@Component
public class ValidateCodeFilter extends OncePerRequestFilter {
@Autowired
MyAuthenticationService myAuthenticationService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 判断是否是登录请求
if (request.getRequestURI().equals("/login") &&
request.getMethod().equalsIgnoreCase("post")) {
String imageCode = request.getParameter("imageCode");
System.out.println(imageCode);
// 具体的验证流程....
try {
validate(request, imageCode);
} catch (ValidateCodeException e) {
myAuthenticationService.onAuthenticationFailure(request, response, e);
return;
}
}
// 如果不是登录请求直接放行
filterChain.doFilter(request, response);
}
@Autowired
StringRedisTemplate stringRedisTemplate;
private void validate(HttpServletRequest request, String imageCode) {
//从redis中获取验证码
String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + request.getRemoteAddr();
String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
// 验证码的判断
if (!StringUtils.hasText(imageCode)) {
throw new ValidateCodeException("验证码的值不能为空!");
}
if (redisImageCode == null) {
throw new ValidateCodeException("验证码已过期!");
}
if (!redisImageCode.equals(imageCode)) {
throw new ValidateCodeException("验证码不正确!");
}
//从redis中删除验证码
stringRedisTemplate.delete(redisKey);
}
}
// 加入用户名密码验证过滤器的前面
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);
5.session管理
Spring Security可以与Spring Session库配合使用,只需要做一些简单的配置就可以实现一些功能,如(会话过期、一个账号只能同时在线一个、集群session等)
5.1 会话超时、并发控制
server:
port: 8080
servlet:
session:
timeout: 600
http.sessionManagement()// 设置session管理
.invalidSessionUrl("/toLoginPage")// session失效之后跳转的路径
.maximumSessions(1)// session最大会话数量 1,同一时间只能有一个用户可以登录 互踢
.maxSessionsPreventsLogin(true)//如果达到最大会话数量,就阻止登录
.expiredUrl("/toLoginPage");// session过期之后跳转的路径
5.2 集群session
实际场景中一个服务会至少有两台服务器在提供服务,在服务器前面会有一个nginx做负载均衡,用户访问nginx,nginx再决定去访问哪一台服务器。当一台服务宕机了之后,另一台服务器也可以继续提供服务,保证服务不中断。如果我们将session保存在Web容器(比如tomcat)中,如果一个用户第一次访问被分配到服务器1上面需要登录,当某些访问突然被分配到服务器二上,因为服务器二上没有用户在服务器一上登录的会话session信息,服务器二还会再次让用户登录,用户已经登录了还让登录就感觉不正常了。
解决这个问题的思路是用户登录的会话信息不能再保存到Web服务器中,而是保存到一个单独的库(redis、mongodb、jdbc等)中,所有服务器都访问同一个库,都从同一个库来获取用户的session信息,如用户在服务器一上登录,将会话信息保存到库中,用户的下次请求被分配到服务器二,服务器二从库中检查session是否已经存在,如果存在就不用再登录了,可以直接访问服务了。
<!-- 基于redis实现session共享 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
spring:
session:
store-type: redis
6.csrf防护机制
CSRF(Cross-site request forgery),中文名称:跨站请求伪造
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全
CSRF这种攻击方式在2000年已经被国外的安全人员提出,但在国内,直到06年才开始被关注,08年,国内外的多个大型社区和交互网站分别爆出CSRF漏洞,如:NYTimes.com(纽约时报)、Metafilter(一个大型的BLOG网站),YouTube和百度HI……而现在,互联网上的许多站点仍对此毫无防备,以至于安全业界称CSRF为“沉睡的巨人”。
6.1 原理
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成三个步骤:
- 登录受信任网站A,并在本地生成Cookie。
- 在不登出A的情况下,访问危险网站B。
- 触发网站B中的一些元素
6.2 防御策略
验证 HTTP Referer 字段
根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,在后台请求验证其 Referer 值,如果是以自身安全网站开头的域名,则说明该请求是是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。
在请求地址中添加 token 并验证
CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
在 HTTP 头中自定义属性并验证
这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。
6.3 security中的csrf防御机制
org.springframework.security.web.csrf.CsrfFilter
csrf又称跨站请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的token信息,如果不包含,则报错。起到防止csrf攻击的效果。(1. 生成token 2.验证token)
开启csrf防护
//开启csrf防护. 定义哪些路径不需要防护 http.csrf().ignoringAntMatchers("/user/saveOrUpdate");
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
7.跨域与CORS
7.1 跨域
跨域,实质上是浏览器的一种保护处理。如果产生了跨域,服务器在返回结果时就会被浏览器拦截(注意:此时请求是可以正常发起的,只是浏览器对其进行了拦截),导致响应的内容不可用. 产生跨域的几种情况有一下:
当前页面Url | 被请求页面URL | 是否跨域 | 原因 |
---|---|---|---|
http://www.baidu.gom | http://www.baidu.com/a.html | 否 | 同源(协议、域名、端口号) |
http://www.baidu.com | https://www.baidu.com/a.html | 是 | 协议不同http/https |
http://www.baidu.com | http://www.yaho.com | 是 | 主域名不同 |
http://www.baidu.com | http://music.baidu.com | 是 | 子域名不同 |
http://www.baidu.com:8080 | http://www.baidu.com:8090 | 是 | 端口号不同 |
7.2 跨域解决
jsonp
浏览器允许一些带src属性的标签跨域,也就是在某些标签的src属性上写url地址是不会产生跨域问题
CORS解决跨域
CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。浏览器在发起真正的请求之前,会发起一个OPTIONS类型的预检请求,用于请求服务器是否允许跨域,在得到许可的情况下才会发起请求
7.3 spring security跨域支持
/**
* 跨域配置信息源
*/
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许跨域的站点
corsConfiguration.addAllowedOrigin("*");
//允许跨域的http方法
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求头
corsConfiguration.addAllowedHeader("*");
// 允许带凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
// 对所有url都生效
urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration);
return urlBasedCorsConfigurationSource;
}
// 开启跨域支持
http.cors().configurationSource(corsConfigurationSource());