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

发布时间:2023年12月28日

框架简介

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

注:本文基于B站up主“三更草堂”讲解视频进行简化和说明。视频地址如下
前置:默认您已建立好最原始的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、配置放行登录接口

@Configuration
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

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

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

    @Override
    protected void configure(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();
    }
}

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);
    }
}
文章来源:https://blog.csdn.net/qq_42535394/article/details/135265144
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。