SpringSecurity登录逻辑快速集成及原理探查

框架简介

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。 一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权 (Authorization)两个部分,这两点也是 Spring Security 重要核心功能。
(1)用户认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
(2)用户授权:经过认证后判断当前用户是否有权限进行某个操作

**注:本文基于B站up主“三更草堂”讲解视频进行简化和说明。

  • 使用hutool的jwt工具
  • 使用自带的redisTemplate
  • 去掉过时的接口WebsecurityConfigurerAdapter
  • 去掉无关紧要的代码

前置:默认您已建立好最原始的SpringBoot工程,并连通Mysql,Redis。**

总体逻辑

在这里插入图片描述

准备工作

  1. 在pom.xml文件中添加如下四个依赖,除第一个核心依赖外其他的都是为了简化开发。
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.2</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.22</version>
        </dependency>
  1. 实体类User,只包含了必须字段,注意用户名和密码必须叫username和password
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
   
    private static final long serialVersionUID = 1L;
    //用户id
    private int id;
    //用户名
    private String username;
    //用户密码
    private String password;
}
  • 其他层(Conttroller、Service、ServiceImpl、Mapper)文件请自行建好,并写一个测试接口。

核心逻辑

1、尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。

  • SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。
    在这里插入图片描述
  • 图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
    UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
    ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
    FilterSecurityInterceptor:负责权限校验的过滤器。

2、认证流程在这里插入图片描述
概念速查:
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

3、实现思路
登录
①自定义登录接口,调用ProviderManager的方法进行认证 如果认证通过生成jwt,把用户信息存入redis中
②自定义UserDetailsService,在这个实现类中去查询数据库
校验:
①定义Jwt认证过滤器,获取token,解析token获取其中的userid,从redis中获取用户信息存入SecurityContextHolder

实现步骤

1、创建一个类实现UserDetailsService接口,重写其中的方法,使其从数据库中查询用户信息。

@Service
@AllArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
   

    private UserMapper userMapper;

    @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("用户名或者密码错误");
        }
        return new LoginUser(user);
    }
}

2、因为UserDetailsService方法的返回值是UserDetails类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser 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;
    }
}

3、编写登录接口(只放实现代码,其他层自己写好)

	@Override
    public ResponseResult<Map<String, String>> login(User user) {
   
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authenticationToken);
        if (Objects.isNull(authenticate)) {
   
            throw new RuntimeException("用户名或密码错误");
        }
        // 认证成功之后,获取用户id
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String userId = String.valueOf(loginUser.getUser().getId());
        // 将用户id存入token的Payload中
        Map<String, Object> map = new HashMap<String, Object>() {
   
            private static final long serialVersionUID = 1L;
            {
   
                put("userId", userId);
            }
        };
        String token = JWTUtil.createToken(map, "jwt-secret".getBytes());
        Map<String, String> resultMap = new HashMap<>();
        resultMap.put("token", token);
        // 把完整的用户信息存入redis,userId作为key,过期时间为60分钟
        redisTemplate.opsForValue().set(userId, loginUser,60 * 60, TimeUnit.SECONDS);
        return new ResponseResult<>(200, "登录成功", resultMap);
    }

4、配置放行登录接口(WebsecurityConfigurerAdapter在Spring Security 5.3版本中已被弃用)

@Configuration
@AllArgsConstructor
public class SecurityConfig {
   

    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Bean
    public PasswordEncoder passwordEncoder() {
   
        return new BCryptPasswordEncoder();
        // 下面配置为验证数据库密码时可以存明文,方便测试。
        // return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
   
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
   
        http
                // 关闭csrf
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 添加自定义jwt过滤器
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
        return http.build();
    }
}

5、自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的
userId。使用userid去redis中获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder。

@Component
@AllArgsConstructor
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
   

    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
   
        //获取token
        String token = request.getHeader("Authorization");
        if (!StringUtils.hasText(token)) {
   
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        // 验证token
        if (!JWTUtil.verify(token, "jwt-secret".getBytes())) {
   
            throw new RuntimeException("token无效");
        }
        //解析token
        JWT jwt = JWTUtil.parseToken(token);
        Object userId = jwt.getPayload("userId");
        //从redis中获取用户信息
        LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get(userId);
        if (Objects.isNull(loginUser)) {
   
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

相关推荐

  1. SpringSecurity 密码加密登录

    2023-12-28 15:26:02       30 阅读
  2. SpringSecurity集成JWT

    2023-12-28 15:26:02       14 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-28 15:26:02       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-28 15:26:02       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-28 15:26:02       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-28 15:26:02       18 阅读

热门阅读

  1. 使用POI技术实现excel文件的导入

    2023-12-28 15:26:02       38 阅读
  2. LabVIEW MIMO 5G/6G Research Design Library X410

    2023-12-28 15:26:02       33 阅读
  3. ubuntu 系统终端颜色设置

    2023-12-28 15:26:02       34 阅读
  4. Vue.set 方法原理

    2023-12-28 15:26:02       33 阅读
  5. PHP文件上传以及数据写入

    2023-12-28 15:26:02       34 阅读
  6. Crow:Middlewares的使用

    2023-12-28 15:26:02       35 阅读
  7. dockerfile——镜像构建工具详解及案例

    2023-12-28 15:26:02       26 阅读
  8. docker命令

    2023-12-28 15:26:02       24 阅读
  9. Docker的基础使用

    2023-12-28 15:26:02       34 阅读
  10. 北航人机交互复习

    2023-12-28 15:26:02       26 阅读