MHBLOG当中SpringBootSecurity+JWT运用结合

SpringbootSecurity的工作流程

image
当用户在 Spring Boot Security 系统中提交登录的账号和密码时,系统会经历一系列流程来验证用户的身份并建立安全的会话。下面是 Spring Boot Security 工作流程的详细讲解,分解了从用户提交登录信息到完成身份认证的整个过程。

Spring Boot Security 工作流程(登录流程):

  1. 用户提交登录请求

    • 用户通过前端(通常是登录页面)提交用户名和密码,这些信息通过 POST 请求发送到服务端(例如,/login 路径)。
  2. 过滤器链处理请求

    • Spring Security 的过滤器链(FilterChain)会拦截所有请求,其中的核心过滤器之一是 UsernamePasswordAuthenticationFilter,它专门处理用户提交的认证请求(如用户名和密码)。
  3. UsernamePasswordAuthenticationFilter 获取登录信息

    • UsernamePasswordAuthenticationFilter 负责从请求中提取用户名和密码,通常从 HttpServletRequest 的参数中提取这些信息。它会将用户输入的凭证封装为 UsernamePasswordAuthenticationToken 对象。
  4. 委托给 AuthenticationManager 进行认证

    • UsernamePasswordAuthenticationFilter 会将封装好的 UsernamePasswordAuthenticationToken 对象提交给 AuthenticationManager,由它负责进一步的认证处理。
    • Spring Security 默认使用 ProviderManager 作为 AuthenticationManager 的实现,它会将认证委托给多个 AuthenticationProvider
  5. AuthenticationProvider 验证凭证

    • 在典型的应用程序中,Spring Security 使用 DaoAuthenticationProvider 作为默认的 AuthenticationProvider
    • DaoAuthenticationProvider 使用 UserDetailsService 来加载用户信息,它会根据传入的用户名从数据库或其他存储中加载用户详细信息(通常包括加密的密码)。
  6. UserDetailsService 加载用户信息

    • UserDetailsService 的实现类(例如自定义实现或 Spring Security 默认的 InMemoryUserDetailsManager)会查询用户的详细信息,并返回一个 UserDetails 对象。
    • 该对象通常包含:用户名、加密后的密码、账户是否启用、是否未过期等信息。
  7. 密码验证

    • 一旦 UserDetails 被成功加载,DaoAuthenticationProvider 会通过 PasswordEncoder 比较用户输入的明文密码和数据库中存储的加密密码是否匹配。
    • Spring Security 默认使用 BCryptPasswordEncoder,它会对输入的明文密码进行加密处理,并与数据库中的哈希密码进行比对。
  8. 认证成功或失败

    • 如果密码匹配且账户未锁定、未过期,DaoAuthenticationProvider 会将认证成功的用户信息封装为 Authenticated UsernamePasswordAuthenticationToken,并返回给 AuthenticationManager
    • 如果密码不匹配或其他认证信息无效,则抛出异常,如 BadCredentialsException,认证失败。
  9. 安全上下文保存用户信息

    • 如果认证成功,AuthenticationManager 返回的 Authentication 对象会被存储到 SecurityContext 中。
    • SecurityContextHolder 是一个持有用户安全上下文的容器,保存了经过认证的用户信息(Authentication 对象),该信息在当前会话中是全局可用的。
    • 默认情况下,Spring Security 会通过 HttpSessionSecurityContextRepository 将安全上下文保存到用户的 HttpSession 中。
  10. 创建会话并返回响应

    • 如果认证成功,Spring Security 会根据配置创建用户的会话(Session)。系统会通过 RememberMe 机制,决定是否保持用户的登录状态。
    • 如果认证成功,系统会重定向或返回一个成功的响应(例如返回主页或 JWT Token,取决于系统的设计)。
  11. 认证失败处理

    • 如果认证失败,Spring Security 会调用失败处理器(例如 SimpleUrlAuthenticationFailureHandler),返回认证失败的错误信息或重定向到登录页面。

总结流程图:

用户提交用户名和密码
        ↓
`UsernamePasswordAuthenticationFilter` 拦截请求并提取凭证
        ↓
凭证交给 `AuthenticationManager` 进行认证
        ↓
`AuthenticationManager` 调用 `AuthenticationProvider` 进行认证
        ↓
`AuthenticationProvider` 使用 `UserDetailsService` 加载用户信息
        ↓
`AuthenticationProvider` 比较输入密码和数据库密码(通过 `PasswordEncoder`)
        ↓
[认证成功]                          [认证失败]
    ↓                                   ↓
将认证后的信息存入 `SecurityContext`  调用失败处理器
    ↓                                   ↓
创建会话并返回响应                    返回错误信息

重要组件的作用:

  1. UsernamePasswordAuthenticationFilter:拦截登录请求,并提取用户名和密码。
  2. AuthenticationManager:负责管理整个认证流程,通常委托给 AuthenticationProvider
  3. AuthenticationProvider:实际执行认证的核心组件,调用 UserDetailsService 加载用户数据并进行密码验证。
  4. UserDetailsService:加载用户信息的接口,通常通过实现这个接口来查询数据库中的用户。
  5. PasswordEncoder:负责密码的加密和比对,通常使用 BCryptPasswordEncoder
  6. SecurityContextHolder:保存当前认证成功的用户信息,系统通过它判断用户是否已登录。
  7. HttpSession:保存用户的安全上下文信息,默认在会话中存储。

用户登录后的授权流程:

  • 一旦用户登录成功并在 SecurityContextHolder 中保存了认证信息,Spring Security 会在每次请求时通过 SecurityContext 验证用户是否已登录,以及用户是否有权访问特定资源。

在MHBlog开发的过程当中,运用了SpringbootSecurity+JWT的组合来验证登录

那么什么是JWT呢?

这里不过多描述,JWT不是很难,所以直接放视频不懂直接看JWT使用
使用详解

由上面我们可以知道,在Security的验证是封装成UsernamePasswordAuthenticationToken对象来传输给其他Filter进行验证的,所以我们的SecurityImpl里要创建一个对象来将Username和Password来构建一个UsernamePasswordAuthenticationToken对象

    @Override
    public ResponseResult login(User user) {
	//创建验证对象Token
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        //判断是否认证通过
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("用户名或密码错误");
        }
        //获取UserId生成Token
        LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
        String id = loginUser.getUser().getId().toString();
        String jwt = JwtUtil.createJWT(id);
        //把用户信息存入redis,然后把Token和UserInfo封装 返回
        redisCache.setCacheObject("bloglogin:" + id, loginUser);
        UserInfoVo userInfoVo = BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class);
        BlogUserLoginVo vo = new BlogUserLoginVo(jwt, userInfoVo);
        return ResponseResult.okResult();
    }

之后将UsernamePasswordAuthenticationToken类提交给AuthenticationManager来进行认证,因为Springboot里默认是没有AuthenticationManager这个Bean的,所以我们得在SecurityConfig类里自己定义一个Bean来添加进IOC容器里

@Configuration
public class SecurityConfig {
//此配置方法适用于5.7版本之后
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }
}

之后在SecurityImpl类的方法里注入AuthenticationManager类的Bean
将UsernamePasswordAuthenticationToken传给AuthenticationManager的authenticate()方法
ProviderManager会调用DaoAuthenticationProvider使用UserDetailsService接口的loadUserByUsername方法来根据Username获取内存中的用户,这时后我们就要创建一个类来实现UserDetailsService接口的loadUserByUsername方法

@Service
public class UserDetailServiceImpl implements UserDetailsService {
    @Autowired
    UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //        根据用户查询用户信息
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(queryWrapper);

//        判断是否查到用户
//        如果没查到抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户不存在");
        }
//        TODO 查询权限封装
//        查到用户作为方法返回值返回
        return new LoginUser(user);
    }
}

让我们来查看这个接口的源码
image
从源码我们可以知道,这个方法要返回的是一个UserDetails类,但是我们ORM数据库查询出来的实体Entity并不是这个类,这个UserDetails我们点进去查看一下
image
可以发现这是一个接口类
所以我们创建一个LoginUser类来实现这个UserDetails接口
image
在这个自定义的LoginUser类当中封装一个User类的属性,这样子Details就能获取User的UserName和Password了
让我们回到BlogLoginServiceImpl
image
getPrincipal()这又是什么方法?
image
getPrincipal()Authentication 接口中的一个方法,它用于获取当前经过认证的用户的主要身份信息(通常是用户名或用户对象)。在 Spring Security 中,当用户成功通过身份验证后,Authentication 对象会被存储在 SecurityContext 中,而 getPrincipal() 方法可以返回该用户的身份信息。

详细解释:

  1. Authentication 接口

    • Authentication 是 Spring Security 的核心接口之一,表示经过身份验证或正在进行身份验证的主体(通常是用户)。
    • 它包含关于身份验证过程中的各种信息,比如用户身份、权限、凭证等。
  2. getPrincipal() 方法

    • getPrincipal() 返回与身份验证相关的主身份信息,即用户的主要标识。这个标识通常是经过认证的用户的用户名或 UserDetails 对象。
    • 如果使用的是基于用户名和密码的身份验证(如 UsernamePasswordAuthenticationToken),getPrincipal() 通常返回的是实现了 UserDetails 接口的对象,或直接是用户名。

示例:getPrincipal() 返回值的类型

  1. 基于用户名和密码的认证

    • 在基于用户名和密码的认证(如 UsernamePasswordAuthenticationToken)中,getPrincipal() 返回的是 UserDetails 对象,通常是用户的详细信息。

    代码示例

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    Object principal = authentication.getPrincipal();
    
    if (principal instanceof UserDetails) {
        String username = ((UserDetails) principal).getUsername();
        System.out.println("Authenticated user: " + username);
    } else {
        String username = principal.toString();
        System.out.println("Authenticated user: " + username);
    }
    
    • 如果用户通过身份验证,principal 通常是一个实现了 UserDetails 接口的对象,比如自定义的 CustomUserDetails 或 Spring Security 提供的 org.springframework.security.core.userdetails.User 对象。
    • 如果 principal 是字符串类型,通常它是用户的用户名。
  2. 匿名用户或未认证的用户

    • 如果用户尚未认证或是匿名用户,getPrincipal() 可能返回一个字符串 "anonymousUser",或者在某些情况下返回 null

常见场景:

  • 获取用户详细信息
    在经过身份验证后,通常我们会通过 getPrincipal() 获取用户的详细信息(如用户名、权限等),以便在业务逻辑中使用。

    示例

    @GetMapping("/user-info")
    public ResponseEntity<?> getUserInfo() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        return ResponseEntity.ok(userDetails.getUsername());
    }
    
  • 自定义 UserDetails
    如果你实现了一个自定义的 UserDetails 类,比如 CustomUserDetails,那么 getPrincipal() 方法会返回你的自定义用户对象,你可以通过它获取与用户相关的自定义信息。

总结:

  • getPrincipal() 方法用于返回经过身份验证的用户的主要标识信息,通常是一个 UserDetails 对象或用户名。
  • 在大多数情况下,它返回的是一个实现了 UserDetails 接口的对象(如自定义的用户类),可以通过它获取用户名、密码、权限等用户信息。
  • 对于匿名用户或未认证的用户,getPrincipal() 可能返回 "anonymousUser"null

通过 getPrincipal(),你可以轻松获取当前认证用户的详细信息并用于后续的业务逻辑。

也就是说这里返回的是一个LoginUser类,这个类当中有我们主要的用户实体User
从下面的代码我们可以看得出来
image
之后就是将user的ID作为Subject传入JWT里再存入Redis缓存(k,v)当中
最后再将用户信息VO类和JwtToken封装进前端API所规定的Vo类当中返回

注:
在DaoAuthenticationProvider当中会选择PasswordEncoder,所以我们在Security里配置一下PasswordEncoder

 @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

这个编码是单向不可逆的,对比是否一致就是对比哈希值,密码:
image

工作流程例子:

详细的工作流程:

  1. 前端用户输入的账号密码封装成一个对象

    • 前端用户提交登录表单,包含用户名和密码。该信息通过 POST 请求发送到后端,后端会将其封装为一个对象(例如 LoginRequest),然后传递给 UsernamePasswordAuthenticationToken 进行处理。

    例如

    public class LoginRequest {
        private String username;
        private String password;
        // getters and setters
    }
    
  2. 封装成 UsernamePasswordAuthenticationToken 对象

    • 后端接收到 LoginRequest 对象后,将用户名和密码封装成 UsernamePasswordAuthenticationToken,这是 Spring Security 内置的一个认证对象,用于存放用户凭证(用户名和密码)。

    代码示例

    UsernamePasswordAuthenticationToken authenticationToken =
        new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword());
    
  3. 调用 authenticationManager 进行认证

    • 接下来,将这个 UsernamePasswordAuthenticationToken 对象传递给 authenticationManagerauthenticate() 方法。
    • authenticationManager 是 Spring Security 中用于处理认证请求的核心组件。默认情况下,Spring Boot 不会自动配置一个 authenticationManager,所以你需要在配置类中手动定义它。

    代码示例

    Authentication authentication = authenticationManager.authenticate(authenticationToken);
    
  4. 默认没有 authenticationManager,需要手动配置

    • Spring Boot 默认没有自动配置 AuthenticationManager,因此你需要在你的配置类(通常是 SecurityConfig)中手动配置一个 AuthenticationManager Bean。

    配置 AuthenticationManager Bean

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
        }
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    }
    
  5. AuthenticationManager 调用 DaoAuthenticationProvider 进行认证

    • 当调用 authenticationManager.authenticate() 时,实际执行认证的是 DaoAuthenticationProvider
    • DaoAuthenticationProvider 会调用 UserDetailsService 来加载用户的详细信息,进行认证(包括密码验证)。
  6. UserDetailsService 从数据库中根据用户名查询用户信息

    • UserDetailsService 是 Spring Security 中的一个接口,专门用于从数据源(例如数据库)中加载用户信息。
    • 你需要自己实现 UserDetailsService 接口,并重写 loadUserByUsername(String username) 方法。该方法通过传入的 username 查询数据库中对应的用户信息。

    实现 UserDetailsService

    @Service
    public class CustomUserDetailsService implements UserDetailsService {
    
        @Autowired
        private UserRepository userRepository;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            // 根据用户名查询用户
            User user = userRepository.findByUsername(username);
            if (user == null) {
                throw new UsernameNotFoundException("User not found");
            }
            // 将用户信息封装到 UserDetails 并返回
            return new CustomUserDetails(user);
        }
    }
    
  7. 将查询到的用户封装成 UserDetails 对象返回

    • 如果用户存在,loadUserByUsername 方法会将用户信息封装到实现了 UserDetails 接口的类中(如 CustomUserDetails),然后返回该对象。
    • CustomUserDetails 类是一个自定义的类,通常用来封装用户的关键信息,例如用户名、密码、权限等。

    示例

    public class CustomUserDetails implements UserDetails {
    
        private User user;
    
        public CustomUserDetails(User user) {
            this.user = user;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            // 返回用户的权限
            return Arrays.asList(new SimpleGrantedAuthority(user.getRole()));
        }
    
        @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 user.isEnabled();
        }
    }
    
  8. 验证密码并返回 Authentication 对象

    • DaoAuthenticationProvider 使用 PasswordEncoder 来验证用户输入的密码是否与数据库中存储的加密密码一致。通常使用 BCryptPasswordEncoder 来进行密码的加密和匹配。
    • 如果验证成功,DaoAuthenticationProvider 会返回一个认证成功的 Authentication 对象,并将其存储在 Spring Security 的上下文中。
  9. 认证成功后存入 SecurityContext 并放行请求

    • 认证成功后,Spring Security 会将 Authentication 对象存入 SecurityContextHolder 中,用户的信息就可以在整个会话中使用。
    • 之后,用户可以访问受保护的资源,Spring Security 会通过 SecurityContext 验证用户的身份和权限。

总结:

  1. 用户登录时输入用户名和密码
  2. 封装到 UsernamePasswordAuthenticationToken
  3. 调用 authenticationManager.authenticate() 进行认证
  4. AuthenticationManager 调用 DaoAuthenticationProvider
  5. DaoAuthenticationProvider 使用 UserDetailsService 加载用户信息
  6. UserDetailsService 从数据库中查询用户信息
  7. 将用户信息封装到 UserDetails 对象中返回
  8. DaoAuthenticationProvider 验证密码
  9. 认证成功后,保存到 SecurityContext

你需要自定义 UserDetailsService 来查询数据库中的用户信息,并配置 AuthenticationManager 以使用 DaoAuthenticationProvider 进行认证。

posted @ 2024-09-21 16:07  MingHaiZ  阅读(11)  评论(0编辑  收藏  举报