Spring Security 重点解析

Spring Security 重点解析

文章目录

  • Spring Security 重点解析
    • 1. 简介
    • 2. 依赖
    • 3. 登录认证
      • 3.1 登录校验流程
      • 3.2 Spring Security 默认登录的原理
        • 3.2.1 Spring Security 完整流程
        • 3.2.2 登录逻辑探究
      • 3.3 自定义改动
        • 3.3.1 自定义用户密码校验
        • 3.3.2 自定义 UserDetails 获取方式 F1
        • 3.3.3 自定义加密算法 F2
      • 3.4 自定义登录认证
        • 3.4.1 自定义登录接口
        • 3.4.2 自定义过滤器
        • 3.4.3 Security 配置
        • 3.4.4 退出登录
    • 4. 验权处理
      • 4.1 “权限”的存在形式
      • 4.2 开启权限管理
      • 4.3 设置权限要求
        • 4.3.1 通过注解
        • 4.3.2 通过配置
        • 4.3.3 自定义设置权限方法
      • 4.4 RBAC 权限模型
      • 4.5 封装权限信息
      • 4.6 查询权限用于构造 UserDetails
      • 4.7 在 SecurityContextHolder 记录时设置权限
    • 5. 自定义失败处理
      • 5.1 实现处理器类
      • 5.2 配置
    • 6. 跨域问题
      • 6.1 配置类
      • 6.2 Security 配置
    • 7. CSRF 防御
    • 8. 自定义成功处理
      • 8.1 处理器
      • 8.2 Security 配置
      • 8.3 自定义登录模式的成功处理器

1. 简介

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

  • 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
  • 一般Web应用的需要进行认证和授权。

认证:

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

授权:

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

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


接下来以用户名密码登录这个案例来讲解!

  • 利用 JWT 机制,文章:JWT 重点讲解-CSDN博客
  • Redis 缓存,文章:Redis 工具类 与 Redis 布隆过滤器-CSDN博客

Spring Security 推荐学习视频:SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷_哔哩哔哩_bilibili

2. 依赖

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

在这里插入图片描述

引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个 SpringSecurity的默认登陆页面

  • 默认用户名是user,密码会输出在控制台。

    在这里插入图片描述

  • 必须登陆之后才能对接口进行访问。

    在这里插入图片描述

    在这里插入图片描述

    退出登录:

    在这里插入图片描述

等一下你就知道原理了~

3. 登录认证

3.1 登录校验流程

在这里插入图片描述

3.2 Spring Security 默认登录的原理

3.2.1 Spring Security 完整流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

  • 经过所有过滤器才能访问 API,过滤器也能处理响应啥的~

在这里插入图片描述

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

在这里插入图片描述

  1. UsernamePasswordAuthenticationFilter
    • 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责
  2. ExceptionTranslationFilter
    • 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException ,有异常抛出就肯定过不了这个过滤器了
  3. FilterSecurityInterceptor
    • 负责权限校验的过滤器。
3.2.2 登录逻辑探究

UsernamePasswordAuthenticationFilter 就是默认登录方式,默认是 Cookie-Session 机制

在这里插入图片描述

概念速查:

Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager接口:定义了认证Authentication的方法

UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装

成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

  1. 访问 /test/hello 第一次没有会话,UsernamePasswordAuthenticationFilter 尝试获取,获取不到,强制让用户登录(重定向到[GET] /login 页面,本次请求不算数)

  2. 访问 [POST]/login 携带用户名密码,验证成功后,将 “用户相关信息(UserDetails)”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  3. 其他过滤器都没问题了(登录没啥权限要求),请求成功到达[POST]/login ,进行对应逻辑,返回响应~

    在这里插入图片描述

  4. 访问 /test/hello 携带 Cookie,UsernamePasswordAuthenticationFilter 获取到会话记录 UserDetails,”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器

    会话记录:

    在这里插入图片描述

  5. 其他过滤器都没问题了(登录没啥权限要求),请求成功到达/test/hello ,进行对应逻辑,返回响应~

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

3.3 自定义改动

在这里插入图片描述

  1. 自定义用户名密码校验方式
  2. 自定义拦截器,可调节在哪个拦截器之前或之后,或者删除哪些拦截器
3.3.1 自定义用户密码校验
  1. 自定义的 UserDetails 获取方式 F1
  2. 自定义密码的加密算法 F2

[POST]/login 携带的用户名为 U,密码为 P,UserDetails 为 D

  • D = F1(U);
  • 判断 F2(P)D.getPassword(),是否相同
3.3.2 自定义 UserDetails 获取方式 F1

我知道我没有给出一些数据库与MP相关的代码,不过思路看得懂即可!

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private SysUser user;

    private List<String> permissions;

    @JSONField(serialize = false)//代表,由不通过fastjson序列化,用系统redis其他的方式存入redis
    private List<GrantedAuthority> authorities;//com.alibaba.fastjson.JSONException: autoType is not support.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(Objects.isNull(this.authorities)) {
            // 将 permissions 的权限信息封装成 SimpleGrantedAuthority 象,并且也是集合
            this.authorities = this.permissions.stream()
                    .parallel()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        return this.authorities;
    }

    @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;
    }

    public LoginUser(SysUser user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
}
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {

    @Resource
    private SysMenuService menuService;

    // 查询用户信息
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 查询用户信息
        SysUser user = Db.lambdaQuery(SysUser.class)
                .eq(SysUser::getUserName, s)
                .one();
        if(Objects.isNull(user)) {
            log.warn("用户名或者密码错误");
            throw new RuntimeException("用户名或者密码错误");
        }
        // todo 查询对应的权限信息
        List<String> permissions = menuService.selectMenuById(user.getId());
        // 把数据封装成UserDetails返回
        return new LoginUser(user, permissions);
    }
}

认证的时候,就会用我们实现的加载数据的方法

3.3.3 自定义加密算法 F2
//BCryptPasswordEncoder的Bean对象加入到容器里
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

在认证的时候,就是用我们的这个 PasswordEncoder 去加密(默认不加密,不用加密算法)

用同种加密算法去注册两个账号:

    @Resource
    private PasswordEncoder encoder;

    @Test
    public void createUser() {
        List<SysUser> list = new ArrayList<SysUser>() {{
            add(new SysUser(){{
                setId(1L);
                setNickName("马1号");
                setUserName("mara1");
                setPassword(encoder.encode("123456"));
            }});
            add(new SysUser(){{
                setId(2L);
                setNickName("马2号");
                setUserName("mara2");
                setPassword(encoder.encode("123456"));
            }});
        }};
        Db.saveBatch(list);
    }

3.4 自定义登录认证

对于以上内容,依赖默认的登录页面,获取是用户名密码的机制也比较单一,我们往往需要一些其他的登录方式~

接下来,我们来搞一下自定义登录(还是以用户名密码,但是这次是我们自己写的业务逻辑)

3.4.1 自定义登录接口

在这里插入图片描述

@Data
public class FormUserDTO {

    private String userName;

    private String password;
    
}
@RestController
//@RequestMapping("/user")
public class LoginController {


    @Resource
    private LoginService loginService;

    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody FormUserDTO user) {
        //登录
        return loginService.login(user);
    }
}

public interface LoginService {
    ResponseResult login(FormUserDTO user);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

实现:

@Resource
private AuthenticationManager authenticationManager;

@Resource
private RedisCache redisCache;

@Override
public ResponseResult<Map<String, String>> login(FormUserDTO user) {
    // 获取AuthenticationManager authenticate认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    //如果认证不通过,提示
    if(Objects.isNull(authenticate)) {
        log.warn("登录失败");
        throw new RuntimeException("登录失败");
    }
    //如果认证通过了,用userid生成一个jwt
    LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//认证的时候已经调用了getAuthorities了
    String userid = loginUser.getUser().getId().toString();
    String jwt = JwtUtil.createJWT(userid);
    //把完整的用户信息存入redis,userid作为key
    redisCache.setCacheObject("login:" + userid, loginUser);
    //        redisCache.getCacheObject("login:" + userid);//是完完整整的存进去的
    // 符合jwt
    return new ResponseResult(200, "登录成功", new HashMap<String, String>(){{
        this.put("token", jwt);
    }});
}

在这里插入图片描述

这就相当于替 UsernamePasswordAuthenticationFilter 调用了认证方法获得 authenticate:

在这里插入图片描述

自然就是一轮认证:

在这里插入图片描述

认证失败也是在这个过程中抛出异常的

3.4.2 自定义过滤器
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {// 保证请求只会经过这个过滤器一次
    
    @Resource
    private RedisCache redisCache;
    
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = httpServletRequest.getHeader("token");
        if(!StringUtils.hasText(token)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        // 解析token
        String userid = null;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            log.error("token非法");
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//代表已认证
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
    }

}

认证成功要记录!

在这里插入图片描述

3.4.3 Security 配置

yaml 配置文件去配置不灵活,不鲜明,在这里不演示

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限控制,不开启这个,注解的权限控制不能生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Resource
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Resource
    private AccessDeniedHandler accessDeniedHandler;


    //BCryptPasswordEncoder的Bean对象加入到容器里
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http); 默认是cookie session
    //内部指定表单,用户输入表单就去验证,也就是UsernamePasswordAuthenticationFilter的认证方式
    // 既然注释掉了,则代表这个过滤器(可能不止这一个)失效了~(不被调用 -> 过滤器不工作),并且还注释掉“控制”相关的配置


        http
    //关闭csrf
            .csrf().disable()
    //不通过Session获取SecurityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/user/login") /*匿名访问*/.anonymous()
        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//添加过滤器在某个过滤器之前
    }
}

不必调用父类的configure方法,UsernamePasswordAuthenticationFilter就相当于没用了

UsernamePasswordAuthenticationFilter 无法进行重定向

  •  不请求`[POST]/login` ,UsernamePasswordAuthenticationFilter就不会调用认证的方法,SecurityContextHolder就不会有对应的那个Authentication对象的记录
    
  •  最终需要认证的接口就访问不了!
    
  • “哪些可以匿名访问不用认证,哪些需要进行认证”,只不过是一些“认证/权限等等的控制”罢了,没有实际的认证机制
  • 每个请求都会经过过滤器链,只不过一些没通过检查也无所谓!

这样我们就需要自己写个拦截器,去规定怎么样才算认证成功

  •  这样,必须提前在/user/login (接口内用了认证方法,这一步代表体检做认证),将userid存到redis,生成token返回
    
  •  请求必须携带token,由 jwtAuthenticationTokenFilter 进行认证是否有token,redis是否有记录
    
  •  有则代表提前做了认证,SecurityContextHolder 设置那个Authentication对象,放行即可,就不需要进行UsernamePasswordAuthenticationFilter的那个默认login页面去认证了
    

把默认的关了的话,UsernamePasswordAuthenticationFilter 就相当于没用了,UsernamePasswordAuthenticationFilter 并不会让认证结果有任何变化

没有 jwtAuthenticationTokenFilter 的话,那么 SecurityContextHolder 就没有记录,那么请求必然不通过认证/验权

jwtAuthenticationTokenFilter 没有让 SecurityContextHolder 有记录,那么请求也必然不通过认证/验权

3.4.4 退出登录
@GetMapping("/user/logout")
public ResponseResult logout() {
    return loginService.logout();
}
public interface LoginService {
    ResponseResult login(FormUserDTO user);

    ResponseResult logout();
}

实现:

@Override
public ResponseResult logout() {
    //获取用户id
    UsernamePasswordAuthenticationToken authentication
            = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    String userid = loginUser.getUser().getId().toString();
    // 删除redis中的值
    String redisKey = "login:" + userid;
    redisCache.deleteObject(redisKey);
    return new ResponseResult(200,  "注销成功");
}

在这里插入图片描述

补充,用户名为 null,在 loadUserByUsername 方法的 userName 为"NONE_PROVIDED"

  • 如果真的有用户名是这个,密码还没错,还真的能登录成功!

在这里插入图片描述

这是借用了 UsernamePasswordAuthenticationFilter 的认证方法,我们当然也可以自己写一个认证方法,构造UserDetails 啊

  • 也可以研究其他的 SpringSecurity 提供的认证方法~

4. 验权处理

4.1 “权限”的存在形式

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

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

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

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

而我们这里的权限就是一个字符串

  • 例如"limit",获取用户的权限字符串列表,有"limit"就代表有权限,反之没有

而权限信息要交给 UserDetails,从而在认证成功后在 SecurityContextHolder 中记录,在FilterSecurityInterceptor 过滤器将其拿出来,对比 api 所需的权限,决定是否可以访问

在这里插入图片描述

4.2 开启权限管理

@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限控制,不开启这个,注解的权限控制不能生效

4.3 设置权限要求

4.3.1 通过注解

Spring表达式语言(Spring Expression Language,SpEL)是 Spring Framework 的核心技术之一,其支持在运行时查询和操作对象图。

SpEL语法类似于 Unified Expression Language,但提供了更加丰富的功能,最特别的是方法调用与字符串模板功能。

@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/test")
public class HelloController {

    @GetMapping("/hello")
    @PreAuthorize("hasAuthority('system:test:list')")
    public String hello() {
        return "<h1>hello!</h1>";
    }

    @GetMapping("/hi")
    @PreAuthorize("hasAuthority('system:dept:list')")
//    @PreAuthorize("hasRole('system:dept:list')")//拼接ROLE_前缀 后去验证
    public String hi() {
        return "<h1>hi!</h1>";
    }

    @GetMapping("/haha")
    @PreAuthorize("hasAnyAuthority('system:dept:list', 'haha')")
//    @PreAuthorize("hasAnyRole('system:dept:list', 'haha')")//拼接ROLE_前缀 后去验证
    public String haha() {
        return "<h1>haha!</h1>";
    }
}

按住 ctrl 点击查看对应的方法~

在这里插入图片描述

4.3.2 通过配置

在这里插入图片描述

4.3.3 自定义设置权限方法
@Component("mex")
public class MacakuExpressionRoot {

    public boolean hasAuthority(String authority) {
        // 获取当前用户的权限
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        //判断用户权限集合中是否存在authority
        return  authentication.getAuthorities()
            .stream()
            .map(x -> x.getAuthority())
            .distinct()
            .collect(Collectors.toList())
            .contains(authority);
    }
}

在这里插入图片描述

4.4 RBAC 权限模型

Role-Based Access Control

在这里插入图片描述

一个用户可以有多个角色,一个角色可以有多个权限

  • 某个角色可以是不同用户,某个权限也可以由多个角色拥有

通过 id 查询权限的 mapper xml:

<select id="selectMenuById" resultMap="BaseResultMap" parameterType="java.lang.Long">
    SELECT
        DISTINCT m.`perms` perms
    FROM
    sys_user u
    LEFT JOIN `sys_user_role` ur ON u.`id` = ur.`user_id`
    LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id`
    LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id`
    LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id`
    WHERE
        u.`id` = #{id, jdbcType=BIGINT}
    AND r.`status` = 0
    AND m.`status` = 0
</select>

4.5 封装权限信息

在这里插入图片描述

4.6 查询权限用于构造 UserDetails

在这里插入图片描述

tips:这里还可以查询角色,因为角色也可以看成是一种特殊的权限

记得加上“ROLE_”前缀

4.7 在 SecurityContextHolder 记录时设置权限

在这里插入图片描述

5. 自定义失败处理

不可以直接用异常处理器去解决问题,因为这个时候抛出异常并不会被我们的全局异常处理器捕获!

在这里插入图片描述

(业务逻辑抛的异常会导致 500)

  1. 认证失败 抛出的 AuthenticationException 异常 ,由 AuthenticationEntryPoint 处理
  2. 验权失败 抛出的 AccessDeniedException ,由 AccessDeniedHandler 处理

在这里插入图片描述

由于不能直接进行异常处理,所以自定义异常处理还是很有必要的~

  • 不过在处理的时候不要抛异常,因为这样会直接导致请求响应是 500(因为不会被我们的异常处理器捕获)
  • 可以重定向之类的~

提几个重定向的坑:

  1. HttpServletResponse 对象的 sendRedirect 方法,是相对路径下的重定向,不要填绝对路径否则会导致循环重定向

    • (例如 sendRedirect(“127.0.0.1:8081/user/login”))会导致重定向的请求是 “127.0.0.1:8081/127.0.0.1:8081/user/login”)
  2. 可以写成这样:

    public final static String LOCATION_HEADER = "Location";

    在这里插入图片描述

    • 通过 HttpServletRequest 对象获取的 URL 是 http 的(即使请求是 https ),如果要重定向到 https 要自己去解决!
    • 可以在配置文件中获取域名与协议啊~

5.1 实现处理器类

验权失败:

@Component
@Slf4j
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        log.warn(e.getMessage());
        ResponseResult responseResult = new ResponseResult(HttpStatus.FORBIDDEN.value(), "用户授权不足");
        //处理异常
        WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
    }
}

认证失败:

@Component
@Slf4j
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {//
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        log.warn(e.getMessage());
        ResponseResult responseResult = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败请查询登录");
        //处理异常
        WebUtils.renderString(httpServletResponse, JSON.toJSONString(responseResult));
    }
}

5.2 配置

在这里插入图片描述

6. 跨域问题

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

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

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

6.1 配置类

@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);
    }
}

6.2 Security 配置

在这里插入图片描述

当然,你也可以写个过滤器,让响应带上一些东西来允许跨域~

@Component
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse httpResponse = (HttpServletResponse) response;
        httpResponse.setHeader("Access-Control-Allow-Origin", "*"); // 可以设置允许访问的域,也可以是具体的域名
        httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        httpResponse.setHeader("Access-Control-Max-Age", "3600");
        httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        // 可以在这里进行一些初始化操作
    }

    @Override
    public void destroy() {
        // 可以在这里进行一些清理操作
    }
}

7. CSRF 防御

CSRF(Cross-Site Request Forgery)的相关介绍视频:CSRF 攻击和防御 - Web 安全常识_哔哩哔哩_bilibili

可以取消对 CSRF 的关闭

在这里插入图片描述

Spring Security默认情况下会启用CSRF防护。

Spring Security 对 CSRF 的防御手段是自定义请求头

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

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

8. 自定义成功处理

成功处理器只能在默认登录方式中加入,我也不知道为什么🤣,硬性限制~

8.1 处理器

登录成功:

@Component
@Slf4j
public class SQSAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                        Authentication authentication) throws IOException, ServletException {
        log.warn("认证成功");
        //由于顶替了原本的成功处理,所以是没有跳转的
    }
}

还有个失败处理器(针对默认登录方式的)

@Component
@Slf4j
public class SQSAuthenticationFailHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        log.warn("认证失败");
    }
}

登出成功:

@Component
@Slf4j
public class SQSLogoutHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        log.warn("登出成功");
    }
}

8.2 Security 配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private AuthenticationSuccessHandler successHandler;

    @Resource
    private AuthenticationFailureHandler failureHandler;


    @Resource
    private LogoutSuccessHandler logoutSuccessHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().successHandler(successHandler).failureHandler(failureHandler);
        http.authorizeRequests().anyRequest().authenticated();
        http.logout().logoutSuccessHandler(logoutSuccessHandler);
        http.httpBasic();
    }
}

8.3 自定义登录模式的成功处理器

认证成功:

  • 不一定到达这个过滤器就一定是认证成功,所以要进行一些判断(只是模拟那个效果罢了)
@Component
public class AuthSuccessFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if(SecurityContextHolder.getContext().getAuthentication().getDetails() instanceof LoginUser) {
            System.out.println("认证成功!");
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
    }
}

Security 配置 (判断成功的原理:在特定的过滤器之后加上对应的过滤器):

// 添加过滤器
http.addFilterAfter(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
    .addFilterAfter(authSuccessFilter, JwtAuthenticationTokenFilter.class);

文章到此结束,Spring Security 还有很多知识,一篇文章是讲不完的,所以还要继续学习才行,不会的去查,边用边学~

但是大部分的功能,利用 Spring Security 加上自己的想法去灵活地自定义实现一些业务吧~

代码地址:java-research/SecurityDemo at master · CarefreeState/java-research (github.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/404810.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++多线程同步(上)

多线程同步 引言总述详情互斥锁示例运行结果分析条件变量示例一实现分析优化运行结果示例二实现代码运行结果示例三实现代码运行结果读写锁示例实现代码注意分析运行结果附言实现运行结果运行结果个人心得引言 项目中使用多线程,会遇到两种问题,一种是对共享资源的访问时需要…

kafka和ZK的关系

zk相当于是kafka的一个基础设施 Kafka是一种高吞吐量、可扩展的分布式发布订阅消息系统&#xff0c;ZooKeeper是一个分布式协调服务&#xff0c;用于管理和协调分布式系统中的各种资源 Zookeeper&#xff1a;管理broker&#xff0c;consumer 创建broker后&#xff0c;向zk注册…

Redis和Mysql如何保证数据一致性

一般情况下&#xff0c;Redis用来实现应用和数据库之间读操作的缓存层&#xff0c;主要目的是减少数据 库IO&#xff0c;还可以提升数据的IO性能。 这是它的整体架构。 当应用程序需要去读取某个数据的时候&#xff0c;首先会先尝试去Redis里面加载&#xff0c;如果命中就 直…

基于Python3的数据结构与算法 - 04 快速排序

一、快速排序思路 快速排序特点&#xff1a;快 步骤&#xff1a; 取一个元素p&#xff08;第一个元素&#xff09;&#xff0c;使元素p归为&#xff1b;列表被p分成两部分&#xff0c;左边都比p小&#xff0c;右边都比p大&#xff1b;递归完成排序。 因此我们可以得到快速排…

kali xrdp

Kali Linux 使用远程桌面连接——xrdp&xfce_kali xfce桌面-CSDN博客 Ubuntu/Debian/Kali xrdp远程桌面黑屏/空屏/无画面解决办法 - 知乎 (zhihu.com) sudo apt-get install xrdp -y sudo apt-get install xfce4 -ysudo systemctl enable xrdp --now systemctl status xrd…

自动化行业文件数据\资料防泄密软件——天锐绿盾|@德人合科技

天锐绿盾是一款自动化行业文件数据防泄密软件&#xff0c;由德人合科技提供。该软件采用动态加解密技术&#xff0c;能够有效防止公司内部数据泄密&#xff0c;同时支持各种文件格式加密&#xff0c;如CAD、OFFICE、PDF、图纸等。 PC端&#xff1a;https://isite.baidu.com/sit…

C语言-数组指针与指针数组

一、简介 对于使用C语言开发的人来说&#xff0c;指针&#xff0c;大家都是非常熟悉的。数组&#xff0c;大家也同样熟悉。但是这两个组合到一起的话&#xff0c;很多人就开始蒙圈了。这篇文章&#xff0c;就详细的介绍一下这两个概念。 指针数组和数组指针&#xff0c;听起来非…

为什么0.1+0.2不等于0.3

一、JS内部的计算是以二进制形式进行的 js里整数和小数转为二进制形式的方法是不一样的&#xff1a; 二、Number类型使用IEEE754标准64位存储 双精度浮点数&#xff08;double类型&#xff09;为每个数分配64位空间&#xff0c;并以科学计数法的方式存储&#xff1a; 那么对于…

如何使用Inno Setup制作Unity构建程序的Windows安装程序

1. 准备 &#xff08;1&#xff09;准备好Unity构建的程序集合 必须包括&#xff1a; Data文件夹&#xff08;xxx_Data&#xff09; Mono文件夹&#xff08;MonoBleedingEdge&#xff09; 打包的应用程序文件&#xff08;xxx.exe&#xff09; Unity播放器dll文件&#xff…

centos7部署nfs+keepalived+drbd

一、项目需求描述 现在使用的架构是nfskeepalivedrsyncsersync&#xff0c;目前这套架构存在主从nfs节点数据同步不一致问题&#xff0c;大概会有 120s左右的数据延长同步时间&#xff0c;需要提供优化的自动化方案。 二、现有方案缺点 1、切换不能保证主从节点数据一致。 2、…

每日面经02

1.用过哪些集合&#xff1f;hashmap扩容&#xff1f;如果<string>如何查找&#xff1f;散列函数用什么散列为什么大小是2的幂次&#xff1f;如果是key 为abc怎么散列&#xff1f;如何知道key不存在&#xff1f;默认大小是否可以修改 &#xff0c;改为30 、32 可以不&…

【MySQL初阶】索引与事务

1. 索引 1.1 索引基本概念 1.1.1 索引介绍 索引(index)&#xff1a;是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。可以对表中的一列或者多列创建索引&#xff0c;并指定索引的类型&#xff0c;各类索引有各自的数据结构实现。&#xff08;具体细节在My…

蓝桥杯DP算法——区间DP(C++)

根据题意要求的是将石子合并的最小权值&#xff0c;我们可以根据DP思想使用二维数组f[i,j]来存放所有从第i堆石子到第j堆石子合并成一堆石子的合并方式。 然后由第二个图所示&#xff0c;我们可以将i到j区间分成两个区间&#xff0c;因为将i到j合并成一个区间的前一步一定是合…

DecBBox(Decode Bounding Box)的软件实现

在深度学习中&#xff0c;"decbbox" 通常指的是 "Decode Bounding Box"&#xff0c;即解码边界框。这是在目标检测任务中常见的一个步骤&#xff0c;用于将网络输出的边界框参数&#xff08;通常是相对于某种参考框的偏移量或者缩放参数&#xff09;转换为…

ico图标是什么意思?ico图标怎么生成?如何在线制作ico图标?

我们在浏览器浏览网页时或收藏某网页时&#xff0c;经常看到有些网页标题前面有一个图标&#xff0c;有些是logo&#xff0c;有些是其他图标&#xff0c;其实这种图标就是网站的favicon.ico图标&#xff0c;也就是我们平时大家所说ico图标。 什么是favicon.ico图标&#xff1f…

贪心/树形dp

思路&#xff1a; 因为如果红色节点的子树中如果有红色节点的话&#xff0c;那么该子树对其不会造成影响&#xff0c;不用考虑&#xff0c;因此我们在考虑每个红色节点时&#xff0c;不考虑其红色子树。那么如图&#xff0c;对每个红色节点答案有贡献的就是其所有非红色子节点…

一个project作为另一个project的Module

android如何引入另一个工程,Android studio 一个项目引入另一个项目作为Libary-CSDN博客 1.file-new-import module 2.

mysql 分表实战

本文主要介绍基于range分区的相关 1、业务需求&#xff0c;每日160w数据&#xff0c;每月2000w;解决大表数据读写性能问题。 2、数据库mysql 8.0.34&#xff0c;默认innerDB;mysql自带的逻辑分表 3、分表的目的:解决大表性能差&#xff0c;小表缩小查询单位的特点(其实优化的精…

不做内容引流,你凭什么在互联网上赚钱?

孩子们放寒假了&#xff0c;待在家里不是看电视&#xff0c;就是拿着手机刷视频&#xff0c;脸上是各种欢快和满足。只是一切换到写作业模式&#xff0c;孩子是各种痛苦表情包&#xff0c;家长则是使出浑身解数&#xff0c;上演亲子大战。可见娱乐常常让人愉悦&#xff0c;而学…

MongoDB从入门到实战之.NET Core使用MongoDB开发ToDoList系统(4)-Mongo数据仓储和工作单元模式封装

前言 上一章我们把系统所需要的MongoDB集合设计好了&#xff0c;这一章我们的主要任务是使用.NET Core应用程序连接MongoDB并且封装MongoDB数据仓储和工作单元模式&#xff0c;因为本章内容涵盖的有点多关于仓储和工作单元的使用就放到下一章节中讲解了。仓储模式&#xff08;R…