SpringSecurity

SpringSecurity(安全)

王富贵 (lmlx66.top)

1、简介

Spring Security 是 Spring 家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比Shiro丰富

一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

一般Web应用的需要进行认证授权

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能

Spring Security 入门原理及实战 - 逃离沙漠 - 博客园 (cnblogs.com)

SpringSecurity简介

在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。特别是在spring boot项目中加入spring security更是十分简单。本篇我们介绍spring security,以及spring security在web应用中的使用。

记住这几个类

  • WebSecurityConfigurerAdapter:自定义Security策略

  • AuthenticationMangerBuilder:自定义认证策略

  • @EnableWebSecurity:开启WebSecurity模式 @EnableXXXX开启XXXX模式

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

认证

准备工作

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.0</version>
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

原理解析

SpringSecurity的原理事实上是利用了过滤器FIlter链

image-20230610235732422

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示

  • UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责授权

  • ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException

  • FilterSecurityInterceptor:负责权限校验的过滤器

我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序

认证的流程介绍

image-20230611000525943

如何使用自己的类去实现流程

原来的UserDetailService接口的实现类,是从内存中查找的用户具体信息,我们可以自己重新写一个实现类去数据库中查找

自己实现流程

image-20230611110942226

image-20230611113658596

  • 登录

    • 自定义登录接口

      • ​ 需要调用ProviderManager接口进行用户信息的获取,再在自定义的登录接口中进行认证。认证通过的话,就使用jwt生成token,通过用userid作为key,用户信息作为vlaue存入到redis中。
    • 自定义userDetailService接口

  • 校验

理解:

存入SecurityContextHolder可能存在于每次请求的过程中,当请求结束,SecurityContextHolder也就随之结束,因此登录的时候是不需要放进去的

序列化理解起来很简单 - 知乎 (zhihu.com)

准备工作

  • 添加依赖pom.xml

      <!--redis-->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-redis</artifactId>
      </dependency>
      <!--fastjson-->
      <dependency>
          <groupId>com.alibaba</groupId>
          <artifactId>fastjosn</artifactId>
          <version>1.2.62</version>
      </dependency>
      <!--jwt-->
      <dependency>
          <groupId>io.jsonwebtoken</groupId>
          <artifactId>jjwt</artifactId>
          <version>0.9.0</version>
      </dependency>
    
  • 相关工具类,实体类在王富贵 (lmlx66.top)

数据库的校验准备

自己重写UserDetailSevice的实现类,主要功能是通过用户名从数据库中查询出用户的相应信息,并将用户信息封装成为UserDetail对象。

package com.zhoulei.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.zhoulei.mapper.UserMapper;
import com.zhoulei.pojo.MyUserDetails;
import com.zhoulei.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
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.Objects;

@Service
public class UserDetailsServiceImp implements UserDetailsService {
    @Autowired
    private UserMapper userMapper;
    //实现的是从用户中查找相关的用户
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<User> wrapper =new QueryWrapper<>();
        wrapper.eq("user_name",username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到用户的话就抛出异常
        if(Objects.isNull(user)){
           throw new RuntimeException("查询不到该用户的信息");
        }
        //查询到该用户的信息,将查询的用成UserDetails
        return new MyUserDetails(user);
    }
}

通常情况下我们在数据库中存储密码是不会之际明文存储的,通常会进行加密处理,但是我们从数据库中查询出来的数据封装成为UserDetails对象,之后会返回的到上一层(passwordencoder)进行用户输入密码和查询密码进行比对,这是必须要我们指出我们所用的加密算法是什么。

package com.zhoulei.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class passwordEncodeconfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

自己的封装UserDetails类

package com.zhoulei.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyUserDetails implements UserDetails {
    private User user;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

实现登录流程

  • 实现登录接口(controller类)

  • 在接口类中使用authenticationManager.authenticate进行认证

  • 通过用户id生成token返回到前端。

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

image-20230618133946877

package com.zhoulei.service.Imp;
import com.zhoulei.pojo.MyUserDetails;
import com.zhoulei.pojo.R;
import com.zhoulei.pojo.User;
import com.zhoulei.service.loginservice;
import com.zhoulei.utils.JwtUtil;
import com.zhoulei.utils.RedisCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
@SuppressWarnings("all")
@Service
public class loginserviceImp implements loginservice {
    @Autowired
    private AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
    @Override
    public R login(User user) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(),user.getPassword());
        //使用AuthenticationMappager里面的authenticate()方法进行验证密码
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        //如果authenticate为null的话表示的是密码验证失败,需要抛出异常
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("登录失败");
        }
        //密码验证成功可以使用jwt通过userid生成一个token
        //获取authenticate的userid
        MyUserDetails principal  = (MyUserDetails) authenticate.getPrincipal();
        String userid = principal.getUser().getId().toString();
        String token = JwtUtil.createJWT(userid);
        Map<String,String> map =new HashMap<>();
        map.put("token",token);
        //将用户信息存储到redis中去,并以userid作为key
        //这里不能直接的将authenticate存储到redis里面,因为authenticate
        redisCache.setCacheObject("login"+userid,principal);
        return new R(200,"操作成功",map) ;
    }
}
    //配置需要放行的请求
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

自定义jwt过滤器

package com.zhoulei.filter;

import com.zhoulei.pojo.MyUserDetails;
import com.zhoulei.utils.JwtUtil;
import com.zhoulei.utils.RedisCache;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class jwtfilter extends OncePerRequestFilter {
    @Autowired
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("token");
        //判断用户的请求头中是否有携带token
        //如果没有携带token表示用户没有登录,可以直接重定位到登录页面时用户登录,不过security过滤器链中应该也会转向到登录页面吧。
        if(!StringUtils.hasText(token)){
            //直接放行,后面的过滤器会将器没有权限的拦截下来。
            filterChain.doFilter(request,response);
            return;
        }
        //用户携带了token,使用jwt将token解析处理。
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            //token解析错误时
            throw new RuntimeException("禁止非法用户访问");
        }
        //通过userid在redis里面获取到相关的用户信息。
        MyUserDetails user = redisCache.getCacheObject("login:" + userid);
        //将获取到的用户信息存储到SecurityContextHolder容器里面
        //存入securityContextHolder
        //TODO 获取权限信息封装到Authentication
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request,response);
    }
}

将jwt过滤器放入到security过滤链中去

   //配置需要放行的请求
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
       /**
         * 把token校验过滤器添加到过滤器链中,注意过滤器放在FilterSecurityInterceptor前面就可以了
         * @Param1 第一个是指定我们的过滤器
         * @Param2 第二个是指定放在哪一个过滤器之前,传入官方字节码对象
         */
        http.addFilterAfter(jwtfilter, UsernamePasswordAuthenticationFilter.class);
    }

退出登录

思想:通过SecurityContextHolder获取到用户的信息,获取到用户userid,使用userid作为key值去删除redis中的values值。

细节: 这里面的SecurityContextHolder里面的信息是不需要删除的,因为在每一个独立的请求中,请求会经过我们自己定义的jwtfilter过滤器,重新生成一个SecurityContextHolder。

    @Override
    public R loginout() {
        //通过SecurityContextHolder获取到userid
        UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
        String userid = userDetails.getUser().getId().toString();
        //删除掉redis中的用户信息
        redisCache.deleteObject("login:"+userid);
        return new R(200,"注销用户成功");
    }

认证配置详解

security的认证配置一般在configure方法里面采用的链式编程

注意:

  • antMatchers()是处理请求

    	static final String permitAll = "permitAll";  //允许所有的请求通过
    
    	private static final String denyAll = "denyAll"; //所匹配的 URL 都不允许被访问。
    
    	private static final String anonymous = "anonymous"; //表示可以匿名访问匹配的 URL,没有认证的请求是可以访问的认证之后不可以访问,一般用于登录接口请求
    
    	private static final String authenticated = "authenticated";//URL 都需要被认证才能访问
    
    	private static final String fullyAuthenticated = "fullyAuthenticated";// 如果用户不是被 remember me 的,才可以访问。
    
    	private static final String rememberMe = "rememberMe";  // 被“remember me”的用户允许访问
    

授权

权限系统的作用

例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能

总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果

我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作

所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作

授权基本流程

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

  • UserDetailsServiceImpl中需要查询权限信息放入LoginUser中
  • JwtAuthenticationTokenFilter自定义过滤器需要将查询到的权限信息放入Authentication中

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication

然后设置我们的资源所需要的权限即可

授权的实现

限制访问资源所需权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限

但是要使用它我们需要先开启相关配置,在配置类中添加

@EnableGlobalMethodSecurity(prePostEnabled = true) //表示开启了访问前需要先查看权限

然后就可以使用对应的注解@PreAuthorize

@RestController
public class HelloController {

    @RequestMapping("/hello")
    //权限为test权限才可以访问
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "hello";
    }
}

封装权限集合

大致思路

  • 在userdetailsServiceImp类中实现对权限集合的查询,
  • 在将查询出来的权限集合到userDetails对象中。
  • 最后将权限集合存储到SecurityContextHolder对象中。

详细步骤王富贵 (lmlx66.top)

我们之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改

  • 修改UserDetailsServiceImpl代码,将权限信息也丢给UserDetails中
  • UserDetails中重写实现的权限方法
  • JwtAuthenticationTokenFilter需要将我们获取权限信息封装到Authentication

//一个不容易理解的点,为什么要在getAuthorities()里面判断一次是否是第一次调用这个方法。弹幕有人说没有作用,因为每次都是新的loginuser生成导致每次的authorities都是null.这个说法是错误。

解释:虽然你每次生成loginuser是一个新的对象,但是这个方法并不是那个时候调用的是在权限鉴定的时候才会调用就是在jwtfilter里面传递给SecurityContextholder权限信息的时候才会调用这个方法。

    //重写获得权限信息方法
    List<GrantedAuthority> authorities;
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //判断是否是第一调用这个方法如果是的话,就只需要直接返回authorities集合。
        if(Objects.nonNull(authorities)){
            return authorities;
        }
//        方法一:使用传统的循环方式来将全部的权限都出入到authorites集合里面
//            authorities=new ArrayList<>();
//        for (String permission : permissions) {
//            //因为集合的泛型是GrantedAuthority及其它的实现类
//            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
//            authorities.add(simpleGrantedAuthority);
//        }

        //方法二:使用stream()流,函数编程来实现。
        //把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().
                map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());
        return authorities;
    }

从数据库中查询数据信息

RBAC权限模型 王富贵 (lmlx66.top)

RBAC权限模型 ,即基于角色的权限控制。这是目前最常被开发者使用也是相对易用,通用权限模型。

image-20230619231541409

自定义失败处理

我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制

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

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

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

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

自定义异常实现类

package com.zhoulei.handler;

import com.alibaba.fastjson.JSON;
import com.zhoulei.pojo.R;
import com.zhoulei.utils.WebUtils;
import org.springframework.http.HttpStatus;
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;
@Component
public class AccessDeniedHandlerImp implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        R result=new R(HttpStatus.FORBIDDEN.value(),"你的权限不够");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);
    }
}

package com.zhoulei.handler;

import com.alibaba.fastjson.JSON;
import com.zhoulei.pojo.R;
import com.zhoulei.utils.WebUtils;
import org.json.JSONObject;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class AuthenticationEntryPointImp implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        R  result=new R(HttpStatus.UNAUTHORIZED.value(),"认证失败");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(httpServletResponse,json);
    }
}

在配置类中配置

        //配置自定义异常
        http.exceptionHandling()
                .authenticationEntryPoint(authenticationEntryPoint)
                 .accessDeniedHandler(accessDeniedHandler);

跨域的问题解决

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

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

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

①先对SpringBoot进行跨域的配置 允许跨域

import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class MyConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //表示可以跨域的请求
        registry.addMapping("/**")
                .allowedOriginPatterns("*")
                //允许跨域的请求方式
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                //可以跨域的请求头
                .allowedHeaders("*")
                //表示是否接受cookie
                .allowCredentials(true)
                //允许跨域的时间
                .maxAge(3600);
    }


}

②需要在Security配置里面配置相应的跨域配置

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

其它权限校验方法

我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurity还为我们提供了其它方法例如:hasAnyAuthority,hasRole,hasAnyRole等

这里我们先不急着去介绍这些方法,我们先去理解hasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑

hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority,大家只要断点调试既可知道它内部的校验原理

它内部其实是调用authentication的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中

hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源

@PreAuthorize("hasAnyAuthority('admin','test','system:dept:list')")
public String hello(){
    return "hello";
}

hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上 ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以

@PreAuthorize("hasRole('system:dept:list')")
public String hello(){
    return "hello";
}
hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上

ROLE_ 后再去比较。所以这种情况下要用用户对应的权限也要有 ROLE_ 这个前缀才可以

@PreAuthorize("hasAnyRole('admin','system:dept:list')")
public String hello(){
    return "hello";
}

自定义权限验证

思路:我们自己定义一个权限校验的方法,在@preAuthorize注解中使用我们的方法

问题:如何让注解使用我们自己所定义的方法呢?

自己定义一个权限校验方法

package com.zhoulei.authoritysecurity;

import com.zhoulei.pojo.MyUserDetails;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class myauthority {
    public boolean hasAuthority(String authority){
        //获取用户权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
        List<String> permissions = userDetails.getPermissions();
        //查找该权限authority权限是否在用户权限集合中。
        return permissions.contains(authority);
    }
}

使用SPLE表达式在perAuthority()里面@加上容器中某个bean对象的名字在调用bean对象的里面所定义的方法。

  @PreAuthorize("@myauthority.hasAuthority('system;test;list1111')")

基于配置方式进行权限校验配置

方式跟配置anonymous()方式一样在security配置类中config方法中配置。

            .antMatchers("/user/login").anonymous()
            .antMatchers("/testCors").hasAuthority("system:dept:list222")

CSRF

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

CSRF解释

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

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

认证成功处理器

王富贵 (lmlx66.top)

posted @ 2023-12-17 23:47  zL66  阅读(6)  评论(0编辑  收藏  举报