关于认证和授权,R之前已经写了两篇文章:
【项目实践】一文带你搞定Session和JWT
【项目实践】一文带你搞定页面权限、按钮权限以及数据权限
在这两篇文章中我们没有使用安全框架就搞定了认证和授权功能,并理解了其核心原理。R在之前就说过,核心原理掌握了,无论什么安全框架使用起来都会非常容易!那么本文就讲解如何使用主流的安全框架Spring Security来实现认证和授权功能。
当然,本文并不只是对框架的使用方法进行讲解,还会剖析Spring Security的源码,看到最后你就会发现你掌握了使用方法的同时,还对框架有了深度的理解!如果没有看过前两篇文章的,强烈建议先看一下,因为安全框架只是帮我们封装了一些东西,背后的原理是不会变的。
本文所有代码都放在了Github上,克隆下来即可运行!
Web系统中登录认证(Authentication)的核心就是凭证机制,无论是Session还是JWT,都是在用户成功登录时返回给用户一个凭证,后续用户访问接口需携带凭证来标明自己的身份。后端会对需要进行认证的接口进行安全判断,若凭证没问题则代表已登录就放行接口,若凭证有问题则直接拒绝请求。这个安全判断都是放在过滤器里统一处理的:
登录认证是对用户的身份进行确认,权限授权(Authorization)是对用户能否访问某个资源进行确认,授权发生都认证之后。 认证一样,这种通用逻辑都是放在过滤器里进行的统一操作:
LoginFilter先进行登录认证判断,认证通过后再由AuthFilter进行权限授权判断,一层一层没问题后才会执行我们真正的业务逻辑。
Spring Security对Web系统的支持就是基于这一个个过滤器组成的过滤器链:
用户请求都会经过Servlet的过滤器链,在之前两篇文章中我们就是通过自定义的两个过滤器实现了认证授权功能!而Spring Security也是做的同样的事完成了一系列功能:
在Servlet过滤器链中,Spring Security向其添加了一个FilterChainProxy过滤器,这个代理过滤器会创建一套Spring Security自定义的过滤器链,然后执行一系列过滤器。我们可以大概看一下FilterChainProxy的大致源码:
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
...省略其他代码
// 获取Spring Security的一套过滤器
List<Filter> filters = getFilters(request);
// 将这一套过滤器组成Spring Security自己的过滤链,并开始执行
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(request, response);
...省略其他代码
}
我们可以看一下Spring Security默认会启用多少过滤器:
这里面我们只需要重点关注两个过滤器即可:UsernamePasswordAuthenticationFilter
负责登录认证,FilterSecurityInterceptor
负责权限授权。
S**pring Security的核心逻辑全在这一套过滤器中,过滤器里会调用各种组件完成功能,掌握了这些过滤器和组件你就掌握了Spring Security!**这个框架的使用方式就是对这些过滤器和组件进行扩展。
刚才我们总览了一下全局,现在我们就开始进行代码编写了。
要使用Spring Security肯定是要先引入依赖包。
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-thymeleafartifactId>
dependency>
<dependency>
<groupId>org.thymeleaf.extrasgroupId>
<artifactId>thymeleaf-extras-springsecurity5artifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.4.3.1version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
dependencies>
依赖包导入后,Spring Security就默认提供了许多功能将整个应用给保护了起来:
要求经过身份验证的用户才能与应用程序进行交互
创建好了默认登录表单
生成用户名为user的随机密码并打印在控制台上
CSRF攻击防护、Session Fixation攻击防护
等等等等…
在实际开发中,这些默认配置好的功能往往不符合我们的实际需求,所以我们一般会自定义一些配置。配置方式很简单,新建一个配置类即可:
在该类中重写WebSecurityConfigurerAdapter的方法就能对Spring Security进行自定义配置。
不管哪种认证方式和框架,有些核心概念是不会变的,这些核心概念在安全框架中会以各种组件来体现,了解各个组件的同时功能也就跟着实现了功能。
我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。这里我们就提取出了一个核心概念:当前登录用户/当前认证用户。整个系统安全都是围绕当前登录用户展开的!这个不难理解,要是当前登录用户都不能确认了,那A下了一个订单,下到了B的账户上这不就乱套了。这一概念在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。
我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取Authentication,看了之前文章的朋友大概就猜到了这个SecurityContext就是我们的上下文对象!
这种在一个线程中横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context)。上下文对象是非常有必要的,否则你每个方法都得额外增加一个参数接收对象,实在太麻烦了。
这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
可以看到调用链路是这样的:SecurityContextHolder
、SecurityContext
、 Authentication。
SecurityContextHolder原理非常简单,就是和我们之前实现的上下文对象一样,使用ThreadLocal来保证一个线程中传递同一个对象!源码我就不贴了,具体可看之前文章写的上下文对象实现。
现在我们已经知道了Spring Security中三个核心组件:
Authentication
:存储了认证信息,代表当前登录用户
SeucirtyContext
:上下文对象,用来获取Authentication
SecurityContextHolder
:上下文管理对象,用来在程序任何地方获取SecurityContext
他们关系如下:
Principal
:用户信息,没有认证时一般是用户名,认证后一般是用户对象
Credentials
:用户凭证,一般是密码
Authorities
:用户权限
现在我们知道如何获取并使用当前登录用户了,那这个用户是怎么进行认证的呢?总不能我随便new一个就代表用户认证完毕了吧。所以我们还缺一个生成Authentication对象的认证过程!
认证过程就是登录过程,不使用安全框架时咱们的认证过程是这样的:
查询用户数据 判断账号密码是否正确 正确则将用户信息存储到上下文中 上下文中有了这个对象则代表该用户登录了。
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
和不使用安全框架一样,将认证信息放到上下文中就代表用户已登录。上面代码演示的就是Spring Security最简单的认证方式,直接将Authentication放置到SecurityContext中就完成认证了!
这个流程和之前获取当前登录用户的流程自然是相反的:Authentication SecurityContext SecurityContextHolder。
是不是觉得,就这?这就完成认证啦?这也太简单了吧。对于Spring Security来说,这样确实就完成了认证,但对于我们来说还少了一步,那就是判断用户的账号密码是否正确。用户进行登录操作时从会传递过来账号密码,我们肯定是要查询用户数据然后判断传递过来的账号密码是否正确,只有正确了咱们才会将认证信息放到上下文对象中,不正确就直接提示错误:
// 调用service层执行判断业务逻辑
if (!userService.login(用户名, 用户密码)) {
return "账号密码错误";
}
// 账号密码正确了才将认证信息放到上下文中(用户权限需要再从数据库中获取,后面再说,这里省略)
Authentication authentication = new UsernamePasswordAuthenticationToken(username, password, authorities);;
SecurityContextHolder.getContext().setAuthentication(authentication);
这样才算是一个完整的认证过程,和不使用安全框架时的流程是一样的哦,只是一些组件之前是我们自己实现的。
这里查询用户信息并校验账号密码是完全由我们自己在业务层编写所有逻辑,其实这一块Spring Security也有组件供我们使用:
AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中调用这个组件,该过滤器负责认证逻辑。
我们要按照自己的方式使用这个组件,先在之前配置类配置一下:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
SecurityConfig类
@Slf4j
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userDetailsService;
@Autowired
private LoginFilter loginFilter;
@Autowired
private AuthFilter authFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 关闭csrf和frameOptions,如果不关闭会影响前端请求接口(这里不展开细讲了,感兴趣的自行搜索,不难)
http.csrf().disable();
http.headers().frameOptions().disable();
// 开启跨域以便前端调用接口
http.cors();
// 这是配置的关键,决定哪些接口开启防护,哪些接口绕过防护
http.authorizeRequests()
// 注意这里,是允许前端跨域联调的一个必要配置
.requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
// 指定某些接口不需要通过验证即可访问。像登陆、注册接口肯定是不需要认证的
.antMatchers("/login", "/register").permitAll()
// 这里意思是其它所有接口需要认证才能访问
.antMatchers("/API/**").authenticated()
// 指定认证错误处理器
.and().exceptionHandling().authenticationEntryPoint(new MyEntryPoint()).accessDeniedHandler(new MyDeniedHandler());
// 禁用session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// 将我们自定义的认证过滤器替换掉默认的认证过滤器
http.addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class);
http.addFilterBefore(authFilter, FilterSecurityInterceptor.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 指定UserDetailService和加密器
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
private class MyEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.error(e.getMessage());
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
ResultVO<String> resultVO = new ResultVO<>(ResultCode.UNAUTHORIZED, "没有登录");
out.write(resultVO.toString());
out.flush();
out.close();
}
}
private class MyDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
ResultVO<String> resultVO = new ResultVO<>(ResultCode.FORBIDDEN, "没有相关权限");
out.write(resultVO.toString());
out.flush();
out.close();
}
}
}
本文来源于:https://zhuanlan.zhihu.com/p/342755411?utm_medium=social&utm_oi=1343915562263547904