• SpringSecurity


    0. 简介

    SpringSecurity是一个安全框架,相对于shior来讲,它的功能更加完善。

    小项目一般用shior比较多,中大型项目一般用SpringSecurity。

    核心功能是认证授权

    • 认证:验证当前用户是不是我们系统的用户,如果是的话判断是哪个用户

    • 授权:通过认证后判断用户是否有权限进行某些操作

    引入SpringSecurity依赖之后我们访问项目中任何一个端口都会跳转到SpringSecurity自带的登录页,就连登录接口也无法直接访问,所以我们需要继续深入了解。

    1. 认证

    1.1 登录校验的业务流程:

    1.2 认证过程原理分析

    1.3 实际需求

    而我们实际前后端分离项目当中的认证,要求前端传过来用户名、密码之后,到数据库当中去查询是否存在这个用户,存在的话生成token作为响应。因此需要对其进行一些修改:

    ①自定义一个登录接口,接收传入的用户名跟密码,然后去调用ProviderManger接口,最终返回的Detail对象返回到我们自定义的接口当中,然后进行校验,通过校验之后生成一个token响应回去。

    ②我们需要自己写一个UserDetail实现类,根据用户名去数据库中查询用户信息以及用户对应的权限信息,最后将这些信息封装成UserDetails对象。

    登录

    校验

    • 登录:

      ①自定义一个类实现UserDetailsService

      • 改为查询数据库,将查询到的用户信息以及权限信息封装到一个UserDatails对象(自定义一个类实现UserDetails,后面做授权的时候还需要返回用户权限信息)中。

      • 配置passwordEncoder密码校验为BCrptPasswordEncoder

      ②自定义登录接口

      • 调用ProviderManger的方法验证用户名跟密码是否正确(项目中使用它的实现类AuthenticationManager,需要在配置类继承WebSecurityConfigurerAdapter来完成注入)

      • 验证通过的话获取userid生成token

      • 将userid作为key,用户信息作为value存储到redis中

      • 响应给用户一个token。

    • 校验:

      ①自定义Jwt认证过滤器

      获取token

      解析token获取userid

      根据userid去redis中查询对应用户信息

      存入SecurityContextHolder

      ②将自定义的Jwt认证过滤器添加到过滤器链中

    2. 授权

    权限系统的作用:让不同的用户可以使用不同的功能。

    2.1 授权的基本流程

    在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

    所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。然后设置我们的资源所需要的权限即可。

    2.2 授权的具体实现

    2.2.1限制访问资源所需的权限

    有使用注解跟配置两种方式(配置的话一般是针对静态资源,项目一般是前后端分离的,所以使用注解用的比较多)

    ①开启配置:

    1. @EnableGlobalMethodSecurity(prePostEnabled = true)

    ②使用注解设置需要的权限

    1. @RestController
    2. public class HelloController {
    3.    @RequestMapping("/hello")
    4.    @PreAuthorize("hasAuthority('test')")
    5.    public String hello(){
    6.        return "hello";
    7.   }
    8. }

    2.2.2 封装权限信息

    ①在数据库中查询出用户后还要获取对应的权限信息,封装到UserDetails中返回

    ②在自定义的过滤器中,将权限信息存入SpringSecurity。

    3. 自定义失败处理

    实现在认证失败或者授权失败的情况下能和接口一样返回同一格式的响应信息,方便前端对响应进行统一的处理。

    在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

    • 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。

    • 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。

    所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。

    ①自定义实现类

    1. @Component
    2. public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    3.    @Override
    4.    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
    5.        ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "权限不足");
    6.        String json = JSON.toJSONString(result);
    7.        WebUtils.renderString(response,json);
    8.   }
    9. }
    1. @Component
    2. public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    3.    @Override
    4.    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    5.        ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录");
    6.        String json = JSON.toJSONString(result);
    7.        WebUtils.renderString(response,json);
    8.   }
    9. }

    ②配置给SpringSecurity

    先注入对应的处理器

       
    1.  @Autowired
    2.    private AuthenticationEntryPoint authenticationEntryPoint;
    3.    @Autowired
    4.    private AccessDeniedHandler accessDeniedHandler;
    5. 然后我们可以使用HttpSecurity对象的方法去配置。
    6.        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).
    7.                accessDeniedHandler(accessDeniedHandler);

    4. 跨域

    浏览器出于安全的考虑(比如我们用postman就不会出现跨域问题),使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

    前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。

    所以我们就要处理一下,让前端能进行跨域请求。

    ①先对SpringBoot配置,运行跨域请求

    1. @Configuration
    2. public class CorsConfig implements WebMvcConfigurer {
    3.    @Override
    4.    public void addCorsMappings(CorsRegistry registry) {
    5.      // 设置允许跨域的路径
    6.        registry.addMapping("/**")
    7.                // 设置允许跨域请求的域名
    8.               .allowedOriginPatterns("*")
    9.                // 是否允许cookie
    10.               .allowCredentials(true)
    11.                // 设置允许的请求方式
    12.               .allowedMethods("GET", "POST", "DELETE", "PUT")
    13.                // 设置允许的header属性
    14.               .allowedHeaders("*")
    15.                // 跨域允许时间
    16.               .maxAge(3600);
    17.   }
    18. }

    ②在SecurityConfig中设置允许SpringSecurity跨域

    1. //允许跨域
    2. http.cors();

    5. 其他问题

    5.1 csrf

    CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。

    CSRF攻击与防御(写得非常好)_擒贼先擒王的博客-CSDN博客

    SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问

    我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。必须把springsecurity处理csrf的功能关闭掉,因为请求不会携带csrf_token。

    5.2 退出登录

    获取SecurityContext中的认证信息,然后删除redis中对应的数据即可

    1.     @Override
    2.     public ResponseResult logout() {
    3.         Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    4.         LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    5.         Long userid = loginUser.getUser().getId();
    6.         redisCache.deleteObject("login:"+userid);
    7.         return new ResponseResult(200,"退出成功");
    8.     }

    在Securityconfig中配置:

    • 关闭Security自身的退出接口

    • 注销接口需要认证才能访问

    1. http.logout().disable();
    2. //注销接口需要认证才能访问
    3. http.antMatchers("/logout").authenticated()

    推荐视频:B站三更草堂

    其他要了解的知识:

    SecurityContextHolder是SpringSecurity最基本的组件了,是用来存放SecurityContext的对象,默认是使用ThreadLocal实现的,这样就保证了本线程内所有的方法都可以获得SecurityContext对象。

  • 相关阅读:
    聊聊如何在Java应用中发送短信
    python in Vscode
    【Python练习】task-07 函数的扩展应用
    深度学习系列2——Pytorch 图像分类(AlexNet)
    BigDecimal正确使用姿势
    LeetCode刷题复盘笔记—一文搞懂0 - 1背包之474. 一和零问题(动态规划系列第十篇)
    LeetCode 362. Design Hit Counter(计数器)
    lambda表达式怎么用?(人话版)
    系列七、GC垃圾回收【四大垃圾算法-标记压缩算法】
    项目管理过程中要用到的132个工具和技术
  • 原文地址:https://blog.csdn.net/weixin_50342605/article/details/127784845