一篇回顾springsecurity

什么是SpringSecurity

SpringSecurity是基于Spring衍生出来的认证和授权的安全框架

能干嘛

SpringSecurity的作用主要两个:

  1. 认证:判断用户是否是系统合法有效的用户。如:登录的用户是否是系统存在的用户,密码是否正确....
  2. 授权: 当用户登录成功后,查询其相关权限,进行权限的赋予

流程一般是:先认证,后授权
当项目引入了springsecurity,我们访问系统的所有请求。对于springsecurity来讲只有需认证和不需认证,而需认证中只有两种结果:认证失败(未认证和认证错误)和认证成功;如果是已认证的话(拥有有效的token或session视为已认证),就让你访问接口,如果认证失败,则去访问登陆界面

如何获取

目前很多项目是基于maven或gradle引入依赖包的;springsecurity有很多模块,不同项目情况不同,实际情况引入相应依赖;
image

认识组件

这里先说明下,springsecurity提供了很多的类或者过滤器,大多需要我们去继承,然后编写我们系统的业务逻辑的;下面开始:

AuthenticationManager

AuthenticationManager是认证管理器,他有一个认证的入口方法:authenticate()去进行认证,会返回一个Authentication有一个对象,当这个对象不为空时,表示此时已经认证成功。Authentication有一个principle对象(未认证时传的是username,认证成功传UserDetails的实现类对象)但如果认证通过了,他返回的是loadUserByUsername返回的UserDetails的实现类对象

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter是认证环节使用到的过滤器。他的用途如他的命名一样:【用户名密码认证过滤器】
它有三个非常重要的方法

attemptAuthentication: 这个就是该过滤器核心的方法,用来认证用户名和密码的认证方法。
successfulAuthentication: 当认证成功后会调用的方法。
unsuccessfulAuthentication: 当认证失败后会调用的方法。

如何使用

基本我们都会创建一个类来继承UsernamePasswordAuthenticationFilter,然后重写上面的3个方法,书写我们自己的业务逻辑

UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken其实是实现了Authentication接口,相当于认证信息的载体对象;可以在attemptAuthentication可以看到,一般是获取表单的用户名和密码后设置到UsernamePasswordAuthenticationToken中,然后调用AuthenticationManager.authenticate()进行后续的认证。

UsernamePasswordAuthenticationToken里面有两个构造如下:

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super((Collection)null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

第一个构造是未认证时的构造对象,第二个是已认证的构造对象;刚开始创建第一个构造对象作为参数让AuthenticationManager进行认证,实际上是委托给了AbstractUserDetailsAuthenticationProvider去认证了,它后续会去对比未认证的UsernamePasswordAuthenticationToken 和 查询到的 UserDetails,若认证成功,会将UserDetails的信息拷贝到 第二个构造对象中,此时是已认证的UsernamePasswordAuthenticationToken

PasswordEncoder

我们要知道,在实际开发中保存到数据库的密码基本都是加密的,也就是在插入数据库前会做加密的操作,而PasswordEncoder就是用来去实施这个事情的
他里面有一个encode方法就是对密码进行加密,然后返回加密后的字符串

如何使用

在实际开发,大多数并不会去实现PasswordEncoder这个接口,然后写自己的加密方式。因为springsecurity已经提供了很多的PasswordEncoder的实现类,因此一般是直接使用提供的实现类即可,不同的实现类encode的加密方式可能不同,具体看不同的项目

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter是一个非常重要的配置类。要知道,框架一般都是会让我们进行配置,毕竟框架提供了待实现的接口让书写自己的逻辑,我们对接口的实现类要反馈给springsecurity,然后让他帮忙去执行我们的实现逻辑

如何使用

还是一样,一般都是去继承这个WebSecurityConfigurerAdapter,然后重写多个configure方法

常见使用

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.
		formLogin().loginPage("/login") //设置我们的登录页面
		.loginProcessingUrl("/acount/login") //设置点击“登录”后访问的接口
		.defaultSuccessUrl("/index") //登录成功后要访问的接口
		.failureForwardUrl("/login") //登录失败要访问的接口, 默认已经是登陆界面了,可不写
		.and().authorizeRequests().antMatchers("/login/**","css/**","export/**").permitAll() //当符合格式的请求不需认证
		.anyRequest().authenticated() //除了不需认证的请求外,其他所有请求都要进行认证
		.antMatchers("/systemMenu/**").hasAuthority("systenMenu") //有此权限才可以访问此接口
		.antMatchers("/systemMenu/**").hasRole("admin") //有此角色才可以访问此接口
		.antMatchers("/systemMenu/**").hasAnyRole("admin","test","security") //有此任一角色才可以访问此接口; 注意源码会进行ROLE_拼接,所以给用户赋予角色码时应该是ROLE_开头
		.antMatchers("/systemMenu/**").hasAnyAuthority("menu1,menu2,menu3"); //有此任一权限才可以访问此接口

		http.exceptionHandling().accessDeniedPage("/unAuth"); //自定义403未授权页面
		http.logout().logoutUrl("/logout") //设置登出接口
				.logoutSuccessUrl("/login") //登出成功后跳转的页面
				.permitAll();
	}

PS:loginProcessingUrl可配可不配。配了,则登录接口由springsecurity负责,他会完成认证和token的生成等工作,如果不配你自己就要写登录接口,然后在接口里判断用户是否认证成功,接着token的生成和保存等逻辑也由自己书写;

UserDetailsService

我们之前说过UsernamePasswordAuthenticationFilter的attemptAuthentication方法是去认证用户名和密码的,认证过程中,你总得去查数据库获取用户信息(包含用户名和密码等等)吧
而UserDetailsService就是用来去查看数据库的用户名和密码的,他里面有一个loadUserByUsername方法,这个方法会在attemptAuthentication的后面springsecurity自动帮我们调用

如何使用

还是一样,实现UserDetailsService,然后重写loadUserByUsername方法,loadUserByUsername里面的逻辑就是包含了 查询数据库中用户信息的业务代码

UserDetails

UserDetails是springsecurity提供的用户信息的接口。认证过程查完数据库后是要告诉springsecurity用户信息情况的,不然人家咋帮你认证。因此需要把数据库查到的信息设置到一个springsecurity认识的实体类上,这个实体类就是实现UserDetails接口的

如何使用

还记得UserDetailsService的loadUserByUsername吗?方法的返回值类型就是UserDetails;实际发货对象一般有两种方式:

  1. 直接使用springsecurity自主实现UserDetails的User类
  2. 自己实现UserDetails 或者 继承 User类来拓展自己的代码

返回UserDetails的实现类对象中,通常要有用户的基本信息,角色,权限...
PS:在实际项目,一般至少你能看见3个User类:

数据库User: 查用户表返回的记录,会设置该类上
springsecurity User:就是实现了UserDetails
通用User:也算是数据库User,但通用性更强

一般在有安全框架的参与下,数据库User和springsecurity User是需配合使用的,涉及数据的转移。而通用User不管你项目有没有用到安全框架等,他都可以拿来封装用户的信息。

认识注解

授权注解

下面三个注解比较常见,用于方法(接口)级别的权限判断,如下

@Secured("ROLE_admin,ROLE_test") :拥有对应角色时才能执行接口方法
@PreAuthorize("hasAnyAuthority('menu:system','menu:user')") : 执行方法前的校验;如执行方法前,如果用于menu:system,menu:user任一权限则可执行该方法。
@PostAuthorize("hasAnyAuthority('menu:system','menu:user')"): 执行方法后,但在return前的校验;若没有menu:system,menu:user任一权限,则不会return,但方法体已经执行了

CSRF介绍

CSRF是“跨站请求伪造”,是一种常见的web攻击形式;他的攻击原理如下:

1.用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
2.在用户信息通过验证后,网站A产生Cookie信息(包含用户的认证信息)并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
3.用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
4.网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
5.浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

SpringSecurity如何解决csrf以及实现原理

如何解决

在每个需要防护的页面,加上下面的一个代码即可:
image

springsecurity的csrf防护机制

springsecurity在4.0开始提供了csrf防护,默认是开启的,但只对POST,PUT,DELETE等更新请求进行防护,像Get请求则不防护;
如果关闭了csrf防护,你可以仅凭用户凭证token来进行post请求登录,如果开启了csrf防护,单单携带用户凭证token就登录不了,还需要携带一个正确的csrf_token才行;这个csrf_token是随机生成,放在请求头里,发送POST,PUT,DELETE等请求后获取 用户传来的csrf_token和服务器获取保存的csrf_token,比较相同则认为是真正的用户进行请求,则通过,反之。

服务器获取保存的csrf_token,通常是放在session或者cookie里,放在session中适用于前后端不分离项目,放在coolie适用于前后端分离项目。

其实上面之所以攻击csrf攻击失败,是因为攻击者拿不到用户的csrf_token来进行请求伪造,只有真正用户在其浏览器发起的请求才会发送正确的csrf_token

springsecurity完整流程图

image

前后端分离springsecurity+jwt认证思路分析

用户访问我们系统资源时,会有两种状态:未认证和已认证

未认证状态

用户处于未认证状态时,若访问是不需认证的资源,springsecurity将放行让其获取;
若访问是需认证的资源,一般来说会被重定向到登录页面
当用户填写完用户名和密码后,会由UsernamePasswordAuthenticationFilter进行认证,会将最终的认证结果设置到Authentication实现类对象中,该对象会被放到SecurityContextHolder里,方便后续的过滤器能拿到认证结果;
假设认证成功了,我们一般会根据userId生成token,cookie存一份,数据库也存一份,方便后续再请求进行认证,这里的数据库一般不用mysql,oracle,通常用redis进行储存,key为userId,value为用户信息

已认证状态

若我们是已认证状态,代表我们cookie里已经有token了,此时我们需要写一个过滤器来判断访问的请求是否是已认证,通常该过滤器都继承onceperrequestfilter,并且尽量放在UsernamePasswordAuthenticationFilter前面,方便后续过滤器获取认证结果

继承了onceperrequestfilter的过滤器的doFilterInternal的逻辑通常如下:

1.获取token并解析出userId
2.根据userId去redis获取用户信息
3.将用户信息设置到UsernamePasswordAuthenticationToken(构造函数要选3个参数的),然后UsernamePasswordAuthenticationToken又设置到SecurityContextHolder里,方便后续的过滤器能拿到认证结果;

前后端分离springsecurity+jwt鉴权思路分析

在用户第一次登录成功后,我们除了将用户信息保存到redis中,还会保存权限信息;当再次请求时,从redis拿到用户信息和权限信息,设置到SecurityContextHolder,过滤器链的最后一个过滤器FilterSecurityInterceptor就是用来鉴权的,它会从SecurityContextHolder获取权限信息,与用户访问接口上授权注解表达式内容进行比对,判断用户是否拥有该权限,有的话就执行接口,反之不执行。

posted @ 2022-02-19 23:04  爱编程DE文兄  阅读(229)  评论(0编辑  收藏  举报