SpringSecurity的原理、框架教程

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

课程来自:https://www.bilibili.com/video/BV1mm4y1X7Hc?p=1&vd_source=345a382f2c86d3441cc342a80fc25545


一、认证

1.登录校验流程

在这里插入图片描述

2.原理初探

2.1 认证的简单流程

Springsecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
在这里插入图片描述

  • UsernamePasswordAuthenticationFilter:负责处理我们在登录页面填写了用户名密码后的登陆请求。(入门案例的认证工作主要由它负责处理)
  • ExceptionTransationFilter:负责处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException异常。
  • FilterSecurityInterceptor:负责处理权限校验和过滤器。

我们可以通过Debug查看当前系统中Springsecurity过滤器链中有哪些过滤器及它们的顺序。
在这里插入图片描述

2.2 认证的详细流程

在这里插入图片描述

  • Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法。
  • UserDetailsService接口:加载用户特定数据的核心接口;其中定义了一个根据用户名查询用户信息的方法(loadUserByUsername(String username))。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回;然后将这些信息封装到Authentication对象中。

2.3 思路分析

在这里插入图片描述
在这里插入图片描述

2.4 解决问题(登录)

2.4.1 自定义登录接口
  1. 我们需要自定义登录接口,让springsecurity对这个接口放行。(不需要登录也能访问)
  2. 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
  3. 认证成功后,需要生成一个jwt(token)返回给前端。
  4. 为了让用户下次请求时能通过jwt(token)识别出具体的是哪个用户,需要把用户信息存入redis(可以把用户id作为key)。
// 在自定义的SpringSecurityConfig类中配置(该类继承了WebSecurityConfigurerAdapter)
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
	@Service
	public class UserServiceImpl extends ServiceImpl<UserMapper, User>
	        implements UserService {
	
	    @Resource
	    private AuthenticationManager authenticationManager;
	
	    @Resource
	    private RedisTemplate<String,Object> redisTemplate;
	
	    @Override
	    public Map login(User user) {
	        // 1.调用ProviderManager的方法进行认证
	        UsernamePasswordAuthenticationToken authenticationToken =
	                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
	        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
	        if (Objects.isNull(authenticate)) {
	            throw new RuntimeException("登录失败!!!");
	        }
	        // 2.认证通过,使用userid生成token
	        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
	        HashMap<String, Object> map = new HashMap<>();
	        Long userid = loginUser.getUser().getId();
	        map.put("userid",userid);
	        String token = JWTUtil.createToken(map, "beacon_key".getBytes());
	        // 3.把用户信息存到redis中
	        String redis_key = "login:"+userid;
	        redisTemplate.opsForValue().set(redis_key, JSONUtil.toJsonStr(loginUser));
	        // 4.把token返回给前端
	        HashMap<String, Object> maps = new HashMap<>();
	        maps.put("token",token);
	        return maps;
	    }
	}
2.4.2 自定义UserDetailsService(在这个实现列中去查询数据库)
	@Component
	public class SecurityUserDetailsServiceImpl implements UserDetailsService {
	    @Resource
	    private UserMapper userMapper;
	
	    @Override
	    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
	        // 查询用户
	        User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
	        if (Objects.isNull(user)) {
	            throw new RuntimeException("用户名或密码错误!");
	        }
	        // TODO 查询设置对应的用户权限
	        return new LoginUser(user);
	    }
	}

2.5 解决问题(校验)

2.5.1 自定义Jwt认证过滤器
  1. 获取token

  2. 解析token获取其中的userid

  3. 通过userid从redis中获取用户信息

  4. 把用户信息存入到SecurityContextHolder(获取权限信息封装到authentication中)为了让后面的filter知道这个请求是已认证的

  5. 放行过滤器

    @Component
    public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
        @Resource
        private RedisTemplate redisTemplate;
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 1.获取token
            String token = request.getHeader("token");
            if (Objects.isNull(token)) {
                filterChain.doFilter(request, response);
                return;
            }
            // 2.解析token获取userid
            String userid;
            try {
                JWT jwt = JWTUtil.parseToken(token);
                JWTPayload payload = jwt.getPayload();
                userid = payload.getClaimsJson().get("userid").toString();
                //userid = payload.getClaim("userid").toString();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException("token非法!!!");
            }
            // 3.通过userid从redis中获取用户信息
            String reids_key = "login:" + userid;
            String loginUserStr = (String) redisTemplate.opsForValue().get(reids_key);
            LoginUser loginUser = JSONUtil.toBean(loginUserStr, LoginUser.class);
            if (Objects.isNull(loginUser)) {
                throw new RuntimeException("用户未登录!!!");
            }
            // 4.把用户信息存入到SecurityContextHolder
            // todo 获取权限信息封装到authentication中
            UsernamePasswordAuthenticationToken authenticationToken =
                    new UsernamePasswordAuthenticationToken(loginUser, null, null);
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            // 5.放行
            filterChain.doFilter(request, response);
        }
    }
    
2.5.2 配置自定义jwt过滤器到过滤器链中
// 在自定义的SpringSecurityConfig类中配置(该类继承了WebSecurityConfigurerAdapter)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                // 不拦截
                .antMatchers(securityProperty.getOpenApi()).permitAll()
                // 其他的需要登录后才能访问
                .anyRequest().authenticated()
                .and()
                // 不通过session获取SecurtiyContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        http.csrf().disable();
        // 配置自定义jwt过滤器到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

2.6 密码加密存储

  1. 实际项目中我们不会把密码明文存储在数据库中。
  2. SpringSecurity默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password ;它会根据id去判断密码的加密方式。(我们一般不会采用这种方式,因此需要替换PasswordEncoder)
  3. 我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
  4. 我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
  5. 我们可以定义一个Springsecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
// 在自定义的SpringSecurityConfig类中配置(该类继承了WebSecurityConfigurerAdapter)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

2.6 解决问题(退出登录)

我们只需要定义一个接口,然后获取SecurityContextHolder中的认证信息,删除redis中对应的用户数据即可。|

    @Override
    public Results logout() {
        // 获取SecurityContextHolder中的用户id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        // 删除redis中的值
        redisUtils.del("login:"+userid);
        return Results.success("注销成功!!!");
    }

二、授权

1. 权限系统作用

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

2. 授权基本流程

  1. 在SpringSecurity中,会使用默认的FilterSecuritylnterceptor来进行权限校验。
  2. 在FilterSecuritylnterceptor中会从SecurityContextHolder中获取Authentication用户信息(获取其中的权限信息),对比当前用户是否拥有访问当前资源所需的权限。
  3. 所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication中;然后设置我们的资源所需要的权限即可。

3. 授权的实现

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

  1. 第一步需要先开启相关配置。
// 在自定义的SpringSecurityConfig类上配置该注解(该类继承了WebSecurityConfigurerAdapter)
@EnableGlobalMethodSecurity(prePostEnabled = true)
  1. 可以使用对应的注解。(@PreAuthorize(“hasAuthority(‘test’)”))
    @GetMapping("user/ceshi")
    @PreAuthorize("hasAuthority('test')")
    public Results ceshi(){
        return Results.success(null,"测试成功!!!");
    }

4.从数据库查询权限信息

4.1 RBAC(Role Based Access Control)

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

4.2 代码实现

省略…

5. 自定义失败处理

目的:如果在认证失败或者是授权失败的情况下也能和接口一样返回相同结构的json,可以让前端能对响应进行统一的处理。

处理机制:

  1. 要实现这个功能我们需要知道SpringSecurity的异常处理机制;
  2. (在Springsecurity中) 如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到;
  3. (在Springsecurity中)在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。

做法:

  1. 如果是认证过程中出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint接口对象的方法去进行异常处理。
  2. 如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler接口对象的方法去进行异常处理。
  3. 因此,我们需要自定义异常处理,自定义类实现AuthenticationEntryPoint接口和AccessDeniedHandler接口的方法,然后配置给SpringSecurity即可。

5.1 认证失败处理

  1. 自定义处理器

    @Component
    public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
            Results results = Results.fail(HttpStatus.UNAUTHORIZED.value(), null, "用户认证失败,请重新登录");
            String json = JSONUtil.toJsonStr(results);
            try {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(json);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 在SpringSecurityConfig配置文件中配置

            // 配置异常处理器
            http.exceptionHandling()
                    // 配置认证失败处理器
                    .authenticationEntryPoint(authenticationEntryPoint)
                    // 配置授权失败处理器
                    .accessDeniedHandler(accessDeniedHandler);
    

5.2 授权失败处理

  1. 自定义处理器

    @Component
    public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
        @Override
        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
            Results results = Results.fail(HttpStatus.FORBIDDEN.value(), null, "权限不足");
            String jsonStr = JSONUtil.toJsonStr(results);
            try {
                response.setStatus(200);
                response.setContentType("application/json");
                response.setCharacterEncoding("utf-8");
                response.getWriter().print(jsonStr);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
  2. 在SpringSecurityConfig配置文件中配置

            // 配置异常处理器
            http.exceptionHandling()
                    // 配置认证失败处理器
                    .authenticationEntryPoint(authenticationEntryPoint)
                    // 配置授权失败处理器
                    .accessDeniedHandler(accessDeniedHandler);
    

6. 跨域

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

前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。

1. 先对SpringBoot配置,允许跨域请求

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                // 设置允许跨域的路径
                .addMapping("/**")
                // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET","POST","DELETE","PUT")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许时间
                .maxAge(3600);
    }
}

2.开启Springsecurity的跨域访问

由于我们的资源都会受到Springsecurity的保护,所以想要跨域访问还要让SpringSecurity运行跨域访问。

// 在自定义的SpringSecurityConfig中配置
        // 关闭csrf
        http.csrf().disable();

7. 遗留的小问题

7.1 其他权限校验方法

之前是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。
hasAuthority方法实际是执行到了SecurityExpressionRoot的hasAuthority方法,该方法内部是调用authentication的getAuthorities方法获取用户的权限列表,然后判断我们访问的方法参数的权限是否在权限列表中。

Springsecurity还为我们提供了其它方法例如:

  • hasAnyAuthority:
    hasAnyAuthority方法可以传入多个权限,只要用户有其中任意一个权限都可以访问对应资源。
  • hasRole:
    hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。
  • hasAnyRole:
    hasAnyRole 有任意的角色就可以访问。它内部也会把我们传入的参数拼接上ROLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。

7.2 自定义权限校验方法

@Component("sywl")
public class SywlExpressionRoot {

    public boolean hasAuth(String authority) {
        // 获取用户权限的集合
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        // 判断用户集合中是否有该权限(获取用户权限集合后可以转换成set集合去重,此处省略)
        List<String> permissions = loginUser.getPermissions();
        return permissions.contains(authority);
    }
}

在SPEL表达式中使用@sywl相当于获取容器中bean的名字为sywl的对象。然后再调用这个对象的hasAuth方法。

    @GetMapping("user/ceshi")
    @PreAuthorize("@sywl.hasAuth('test')")// 此处使用到SpEL表达式
    public Results ceshi(){
        return Results.success(null,"测试成功!!!");
    }

7.3 基于配置的权限控制

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
                authorizeRequests()
                // 不拦截
                .antMatchers(securityProperty.getOpenApi()).permitAll()
                // 其他的需要登录后才能访问
                .anyRequest().authenticated()
                .antMatchers("/user").hasAuthority("user:get");
    }

7.4 csrf

https://blog.csdn.net/freeking101/article/details/86537087

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

Springsecurity去防止CSRF攻击的方式就是通过csrf_token。
后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现CSRF攻击依靠的是cookie中所携带的认证信息。
但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。

7.5 登录成功的处理器

实际上是在UsernamePasswordAuthenticationFilter进行登录认证的时候,如果登录成功了是会调AuthenticationSuccessHandler的方法进行认证成功后的处理的。
AuthenticationSuccessHandler就是登录成功处理器;我们也可以自己去自定义成功处理器进行成功后的相应处理。

7.6 注销成功的处理器

实际上在UsernamePasswordAuthenticationFiter进行登录认证的时候,如果认证失败了是会调用AuthenticationFailureHandler的方法进行认证失败后的处理的。
AuthenticationFailureHandler就是登录失败处理器;我们也可以自己去自定义失败处理器进行失败后的相应处理。

7.7 其他认证方案畅想

https://www.bilibili.com/video/BV1mm4y1X7Hc?p=40&vd_source=345a382f2c86d3441cc342a80fc25545


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值