首页 > 其他分享 >Spring security

Spring security

时间:2024-07-07 20:55:05浏览次数:21  
标签:Spring 认证 token user new security 权限 public

目录

 引入依赖

 登录验证流程

原理分析

 SpringSecurity完整流程

 !!!我们主要改的就是 userDetailService实现类的里面的逻辑,把它改成到数据库查询

前后端分离的实现思路

思路流程

登录

 第一步:实现userdetailservice

配置security实现密码加密存储

登录接口

校验

 退出登录

授权

 RBAC权限模型

自定义验证异常类

跨域

自定义权限校验方法

​编辑 CSRF

认证成功 失败 注销处理器 略


 引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个 SpringSecurity 的默认登陆页面,默认   用户名是user,密码会输出在控制台。   必须登陆之后才能对接口进行访问。

 登录验证流程

原理分析

想要知道如何实现自己的登陆流程就必须要先知道入门案例中 SpringSecurity 的流程。

 SpringSecurity完整流程

SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。 (每次请求都会经过这个链路,密码校验等等,但是 我们只有在第一次登陆的时候输密码,其他的时候不输入密码呀,那以后的请求怎么经过这个链路呢,我们可以在第一次登录的时候,登陆成功后保存生成一个jwt用来保存用户id,其他的认证通过的用户信息保存到redis中,以后每次请求都解析jwt,从redis中取出用户认证信息然后保存到存入 SecurityContextHolder(然后其他的过滤器就可以从里面获取用户信息进行验证))

UsernamePasswordAuthenticationFilter : 负责处理我们在登陆页面填写了用户名密码后的登陆请 求。入门案例的认证工作主要有它负责。 ExceptionTranslationFilter 处理过滤器链中抛出的任何 AccessDeniedException 和 AuthenticationException 。 FilterSecurityInterceptor 负责权限校验的过滤器。通俗一点就是授权由它负责。(鉴权)

 !!!我们主要改的就是 userDetailService实现类的里面的逻辑,把它改成到数据库查询

前后端分离的实现思路

思路流程

登录

①自定义登录接口 调用 ProviderManager 的方法进行认证如果认证通过生成jwt(jwt只是用来存 用户id ) 把用户信息存入 redis 中 ②自定义 UserDetailsService 在这个实现类中去查询数据库 校验: ①定义 Jwt 认证过滤器 获取 token 解析 token 获取其中的 userid 从 redis 中获取用户信息 存入SecurityContextHolder(然后其他的过滤器就可以从里面获取用户信息进行验证)

 第一步:实现userdetailservice

 因为最终调用这个方法完成认证,  loadUserByUsername是providerManager进行调用的(不会显式的进行调用),传递过来的值只有一个用户名,这这个方法里面去数据库查找对象

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private MenuMapper menuMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //根据用户名查询用户信息
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(User::getUserName,username);
        User user = userMapper.selectOne(wrapper);
        //如果查询不到数据就通过抛出异常来给出提示
        if(Objects.isNull(user)){
            throw new RuntimeException("用户名或密码错误");
        }
        // 查询权限信息
        List<String> perms = menuMapper.selectPermsByUserID(user.getId());
        //封装成UserDetails对象返回,所以定义一个了LoginUser类实现UserDetails接口
        System.out.println(new LoginUser(user,perms));
        return new LoginUser(user,perms);
    }
}

实现 UserDetails接口时,把下面哪些是否可用,没有超时等的哪些返回值全改为true

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;

    private List<String> permissions;

    @JSONField(serialize = false) //不能序列化SimpleGrantedAuthority包
    private List<SimpleGrantedAuthority> authorities;

    public LoginUser(User user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
    // 判断用户是否有权限的时候会调用这个方法
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if (authorities!=null)return authorities;
        //把权限信息封装
        //1.传统写法
        List<GrantedAuthority> res = new ArrayList<>();
        for (String permission : permissions) {
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
            res.add(simpleGrantedAuthority);
        }
        //2.stream流写法
//        authorities = permissions.stream()
//                .map(SimpleGrantedAuthority::new)
//                .collect(Collectors.toList());
        return 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;
    }

配置security实现密码加密存储

实际项目中我们不会把密码明文存储在数据库中。 默认使用的 PasswordEncoder 要求数据库中的密码格式为: {id}password 。它会根据 id 去判断密码的 加密方式。但是我们一般不会采用这种方式。所以就需要替换 PasswordEncoder 。 我们一般使用 SpringSecurity 为我们提供的 BCryptPasswordEncoder 。 我们只需要使用把 BCryptPasswordEncoder 对象注入 Spring 容器中, SpringSecurity 就会使用该 PasswordEncoder 来进行密码校验。 我们可以定义一个 SpringSecurity 的配置类, SpringSecurity 要求这个配置类要继承 WebSecurityConfigurerAdapter 。

springboot 3版本中这个继承的这个适配器已经弃用,使用的是 

/ @EnableWebSecurity  用EnableWebSecurity注解代替继承

登录接口

整体思路 :接下我们需要自定义登陆接口,然后让 SpringSecurity 对这个接口放行 , 让用户访问这个接口的时候不用登录也能访问。 在接口中我们通过AuthenticationManager 的 authenticate 方法来进行用户认证 , 所以需要在 SecurityConfig中配置把 AuthenticationManager 注入容器。 认证成功的话要生成一个 jwt ,放入响应中返回。并且为了让用户下回请求时能通过 jwt 识别出具体的是哪个用户,我们需要把用户信息存入redis ,可以把用户 id 作为 key 。

 

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                //关闭csrf,因为前后端分离项目不需要csrf,所以直接关闭
                .csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        // 把jwtAuthenticationTokenFilter 添加到过滤器链中
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    }

 

@Service
public class LoginServcieImpl implements LoginServcie {

    @Autowired   // 这个是在配置中配置的,见上图
    AuthenticationManager authenticationManager;
    @Autowired
    private RedisCache redisCache;
    @Override
    public ResponseResult login(User user) {
//这个对象需要传递两个参数,一个是用户名,一个密码
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        // 第一步,authenticationManager调用authenticate方法,
//该方法需要传递 Authentication类型的参数,我们找他的实现类就好,
//该方法会去调用UserDetailsServiceImpl.loadUserByUsername
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        // 如果认证没通过,authenticate就是null
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("登录失败");
        }
        // 如果认证通过,使用userid生成jwt
        //使用userid生成token
//getPrincipal()就是返回的我们userdetailservice实现类返回的对象
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(userId);
        //authenticate存入redis
        redisCache.setCacheObject("login:"+userId,loginUser);
        //把token响应给前端
        HashMap<String,String> map = new HashMap<>();
        map.put("token",jwt);
        return new ResponseResult(200,"登陆成功",map);

    }
}

 至此  登录部分实现完毕

校验

认证过滤器 我们需要自定义一个过滤器,这个过滤器会去获取请求头中的 token ,对 token 进行解析取出其中的 userid 。 使用 userid 去 redis 中获取对应的 LoginUser 对象。 然后封装 Authentication 对象存入 SecurityContextHolder
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token,因为如果第一次登录肯定没有token,所以直接放行,让后面的过滤器处理它
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //没有token,放行,让后面的拦截他
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder,以便后续过滤器能够通过
        //TODO 获取权限信息封装到Authentication中
//UsernamePasswordAuthenticationToken 有两种构造方法,我们需要选择三种参数的

        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

UsernamePasswordAuthenticationToken 两种构造方法,这里选择三个参数的,第一个参数传递用户id,第二个为null(因为是登录过了所以不用传递密码),第三个权限

看里面的内容主要是  

setAuthenticated  这个值的不同,因为是从redis中取出来认证信息了,所以肯定是认证状态,所以
setAuthenticated是true,后面的过滤器发现是已认证状态就会通过

 这个jwt过滤器写完不能直接放到spring容器中需要进行配置,让他经过过滤器链,那么放在哪呢,肯定是放在最前面啊,放在后面那就没意义了

这个方法的意思就是放在哪个过滤器前面

 退出登录

我们只需要定义一个登陆接口,然后获取 SecurityContextHolder 中的认证信息,删除 redis 中对应的数据即可
    @Override
    public ResponseResult logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userid = loginUser.getUser().getId();
        redisCache.deleteObject("login:"+userid);
        return new ResponseResult(200,"退出成功");
    }

授权

例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。 总结起来就是 不同的用户可以使用不同的功能 。这就是权限系统要去实现的效果。 我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。 所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。 在 SpringSecurity 中,会使用默认的 FilterSecurityInterceptor 来进行权限校验。在 FilterSecurityInterceptor 中会从 SecurityContextHolder 获取其中的 Authentication ,然后获取其中的 权限信息。当前用户是否拥有访问当前资源所需的权限。 所以我们在项目中只需要把当前登录用户的权限信息也存入 Authentication 。 然后设置我们的资源所需要的权限即可。

在写 UserDetailsServiceImpl 的时候说过,在查询出用户后还要获取对应的权限信息,封装到 UserDetails 中返回。

 

 RBAC权限模型

RBAC 权限模型( Role-Based Access Control )即:基于角色的权限控制。这是目前最常被开发者使用 也是相对易用、通用权限模型。

自定义验证异常类

创建 exception 包,在 exception 包下创建自定义 CustomerAuthenticationException 类,继承 AuthenticationException 类

 认证失败的

/**
 * 认证失败处理类 未认证返回的异常,在访问未经授权的时候触发
 */
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse
            response, AuthenticationException authException) throws IOException, ServletException {
        ResponseResult result = new
                ResponseResult(HttpStatus.UNAUTHORIZED.value(), "认证失败请重新登录111");
        String json = JSON.toJSONString(result);
        WebUtils.renderString(response, json);
    }
}

 授权失败的

 

跨域

浏览器出于安全的考虑,使用 XMLHttpRequest 对象发起 HTTP 请求时必须遵守同源策略,否则就是跨域的HTTP 请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。 前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。 所以我们就要处理一下,让前端能进行跨域请求。 ①先对 SpringBoot 配置,运行跨域请求

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

security配置文件中配置 security的跨域

自定义权限校验方法

我们模仿的是官方的注解,写自定义的注解,官方注解返回的是true  false 所以 我们自定义的也返回true和false就行

CSRF

CSRF 是指跨站请求伪造( Cross-site request forgery ),是 web 常见的攻击之一。 https://blog.csdn.net/freeking101/article/details/86537087 SpringSecurity 去防止 CSRF 攻击的方式就是通过 csrf_token 。后端会生成一个 csrf_token ,前端发起请求的时候需要携带这个csrf_token, 后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。 我们可以发现 CSRF 攻击依靠的是 cookie 中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token ,而 token 并不是存储中 cookie 中,并且需要前端代码去把 token 设置到请求头中才可以,所以CSRF 攻击也就不用担心了。

认证成功 失败 注销处理器 略

标签:Spring,认证,token,user,new,security,权限,public
From: https://blog.csdn.net/2201_75600005/article/details/140249224

相关文章

  • SpringSecurity简单自定义配置
    初学者对于学习SpringSecurity相关的一些简单自定义配置总结。由于自身能力并不能和大佬相比较,以下的一些内容有误或有可改进地方,希望指出,我抱有一颗谦虚好学的心保持热情,并感谢指正。实现案例:1.基于内存的用户认证2.基于数据库的用户认证3.添加用户(数据库)4.自定义密......
  • SpringBoot3 整合 Logback
    SpringBoot3整合Logback日志框架1.默认框架实现SpringBoot3默认是使用SLF4J+Logback作为默认的日志门面和实现,但也支持其他日志系统,如Log4j2、JUL(JavaUtilLogging),这是通过所谓的日志门面实现的,开发者可以根据自己的需求选择合适的日志实现框架进行配置。日志......
  • 【Spring Boot】基于 JPA 开发的文章管理系统(CRUD)
    《JPA从入门到精通》系列包含以下文章:Java持久层API:JPA认识JPA的接口JPA的查询方式基于JPA开发的文章管理系统(CRUD)关系映射开发(一):一对一映射关系映射开发(二):一对多映射关系映射开发(三):多对多映射基于JPA开发的文章管理系统(CRUD)1.实现文章实体2.实现......
  • 【spring】(极简版)
    spring的核心就是控制反转和依赖注入,说人话就是把对象交给spring容器管理搭建一个spring非常简单项目结构(简单吧)第一步,创建一个空的Maven项目并在pom.xml中导入依赖(其实spring的依赖只用spring-context就可以了,不过我习惯用单元测试,所有导了个junit的包,如果不导junit,用mai......
  • 【Spring Boot】关系映射开发(三):多对多映射
    关系映射开发(三):多对多映射1.创建实体1.1创建Student实体1.2创建Teacher实体2.创建测试在多对多关联关系中,只能通过中间表的方式进行映射,不能通过增加外键来实现。注解@ManyToMany用于关系的发出端和接收端。关系的发出端定义一个集合类型的接......
  • SpringBoot-校园疫情防控系统-93033(免费领源码+开发文档)可做计算机毕业设计JAVA、PHP
    springboot校园疫情防控系统摘 要信息化社会内需要与之针对性的信息获取途径,但是途径的扩展基本上为人们所努力的方向,由于站在的角度存在偏差,人们经常能够获得不同类型信息,这也是技术最为难以攻克的课题。针对校园疫情防控等问题,对校园疫情防控进行研究分析,然后开发设计出......
  • Spring框架:核心概念与Spring Boot微服务开发指南
    引言        Spring框架是一个开源的Java平台,它提供了全面的基础设施支持,用于开发Java应用程序。Spring的核心概念包括依赖注入(DI)、面向切面编程(AOP)和事务管理。随着微服务架构的兴起,SpringBoot作为Spring框架的扩展,提供了一种快速开发独立微服务的方式。本文将详细......
  • Spring 配置文件加密
    前文在某些场景下,使用Spring作为开发组件时,不可避免地需要使用到配置文件,然而,对于配置文件中的某些敏感数据(如密码等信息字段),如果使用明文的方式,则可能在一定程度上导致信息泄露。为此,需要一种有效的方式来对这些字段进行加密处理,当前主流的一种加密方式就是Jasypt基本使用......
  • Spring之 IoC、BeanFactory、ApplicationContext
    IoC(InverseofControl)IoC,也就是控制反转。对于软件来说,即某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定,即由Spring容器借由Bean配置来进行控制。MartinFowler提出了DI(DependencyInjection,依赖注入)的概念用来代替IoC,即让调用类对某一接口实现类的......
  • spring-14-Spring 提供集合的配置元素
    <list>类型用于注入一列值,允许有相同的值。   对于Spring框架来说,<list>类型是一种用于注入一列值的配置元素。它允许您在Spring应用程序上下文中创建一个列表,并将它注入到一个bean的属性中。这个列表可以包含任意数量的对象,并允许出现相同的值。下面是一个完整的示例......