SpringSecurity6.x整合手机短信登录授权

news2024/9/22 21:22:23

前言:如果没有看过我的这篇文章的Springboot3.x.x使用SpringSecurity6(一文包搞定)_springboot3整合springsecurity6-CSDN博客需要看下,大部分多是基于这篇文章的基础上实现的。

明确点我们的业务流程:

  1. 需要有一个发送短信的接口,可结合阿里云的sms
  2. 需要一个短信登录接口,参数为手机号和验证码
  3. 结合到Security做用户认证 
  4. 最终返回token

了解业务流程后我们,就可以开始动手了!

验证码接口

验证码这里我只展示业务逻辑,后续会发文字专门说明Springboot如何调用阿里的SMS发现短信。 

控制层

   /**
     * 获取手机验证码
     * @return
     */
    @Operation(summary = "获取手机验证码")
    @GetMapping("/sendCode")
    public Result sendCode(@RequestParam(name = "phone") String phone){
    return userService.sendCode(phone);
    }

impl

   @Override
    public Result sendCode(String phone) {
        //采用正则校验手机号
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(phone);
        if (phoneInvalid) {
            return Result.fail(500, "手机格式错误");
        }
        //设置校验60秒
        if (stringRedisTemplate.hasKey(RedisConstants.PHONE_CHECK_KEY + phone)) {
            Long expire = stringRedisTemplate.getExpire(RedisConstants.PHONE_CHECK_KEY + phone, TimeUnit.SECONDS);
            return Result.fail(500, "请耐心等待" + expire + "秒");
        }
        //采用工具类生成随机验证码
        int number = RandomUtil.randomInt(100000, 999999);
        String code = String.valueOf(number);
        //TODO 后续改为第三方短信平台
        try {
            smsUtils.sendSms(phone, code);
            stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone, code, RedisConstants.LOGIN_CODE_TTL, TimeUnit.SECONDS);
//            设置倒计时间
            stringRedisTemplate.opsForValue().set(RedisConstants.PHONE_CHECK_KEY + phone, code, RedisConstants.PHONE_CHECK_TTL, TimeUnit.SECONDS);
            log.info("验证码:{}", code);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail(500, "发送失败,请联系管理员~");
        }
        return Result.success("请在五分钟内输入验证码~");
    }

上面的我们将验证码存到redis且设置了时效,那么业务的第一不我们就完成了。

短信登陆接口

   @Operation(summary = "手机号登录接口")
    @PostMapping("/phoneLogin")
    public Result phoneLogin(@RequestParam(name = "phone")String phone,
                             @RequestParam(name = "code")String code
                             ){
        return userService.phoneLogin(new LoginDto().setPhone(phone).setCode(code));
    }

这里为了在Security好取参数我们这里用Param传参数。实现类我们直接返回成功即可,因为结合Security后请求不会进到方法体里面。为什么呢?我们后面来解释。

问题1:为什么手机短信接口结合Security后不会进度lmpl层方法体里面呢?

结合Security实现认证

在这里之前我们需要了解Security里面的几个类:

  1. UsernamePasswordAuthenticationToken
  2. UsernamePasswordAuthenticationFilter
  3. AuthenticationProvider

1.UsernamePasswordAuthenticationToken

UsernamePasswordAuthenticationToken首先我们需要了解这哥们是干嘛用的,我们看下官网是怎么解释的表单登录(Form Login) :: Spring Security Reference

a86ec7278b674e748906d06154a52e90.png

 上图中他说会从UsernamePasswordAuthenticationFilter这个过滤器中去拿账号密码,给UsernamePasswordAuthenticationToken 那么我们就可以猜测下这东西应该是存的是账号和密码有点相当于实体。

6e71e22a1fc74234b1ed476402ae2421.png

我们来看下源码:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final Object principal;

	private Object credentials;
        
/**
*任何希望创建构造函数的代码都可以安全地使用此构造函数
*<code>用户名密码认证令牌</code>,作为{@link#isAuthenticated()}
*将返回<code>false</code>。
*
*/
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
		super(null);
		this.principal = principal;
		this.credentials = credentials;
		setAuthenticated(false);
	}

/**
*此构造函数只能由<code>AuthenticationManager</code>或
*<code>AuthenticationProvider</code>满足以下条件的实现
*生成一个可信的(即{@link#isAuthenticated()}=<code>true</code>)
*身份验证令牌。
*@param主体
*@param凭据
*@param权限
*/
	public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		super(authorities);
		this.principal = principal;
		this.credentials = credentials;
		super.setAuthenticated(true); // must use super, as we override
	}

/**
*任何希望创建工厂方法的代码都可以安全地使用此工厂方法
*未经身份验证的<code>用户名密码认证令牌</code>。
*@param主体
*@param凭据
*@返回用户名密码认证令牌,结果为false isAuthenticated()
*
*@自5.7以来
*/

	public static UsernamePasswordAuthenticationToken unauthenticated(Object principal, Object credentials) {
		return new UsernamePasswordAuthenticationToken(principal, credentials);
	}

	/**
*任何希望创建工厂方法的代码都可以安全地使用此工厂方法
*经过身份验证的<code>用户名密码身份验证令牌</code>。
*@param主体
*@param凭据
*@返回用户名密码认证令牌,结果为true isAuthenticated()
*
*@自5.7以来
*/
	public static UsernamePasswordAuthenticationToken authenticated(Object principal, Object credentials,
			Collection<? extends GrantedAuthority> authorities) {
		return new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
	}

	@Override
	public Object getCredentials() {
		return this.credentials;
	}

	@Override
	public Object getPrincipal() {
		return this.principal;
	}

	@Override
	public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
		Assert.isTrue(!isAuthenticated,
				"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
		super.setAuthenticated(false);
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
		this.credentials = null;
	}

}

通过上面我们确认了principal前面是账号,credentials就是密码,返回的是一个Authentication,因为他继承了AbstractAuthenticationToken

c3509a5b237a4c1eba988e2360a7a5a1.png

2.UsernamePasswordAuthenticationFilter

上面我们就提到UsernamePasswordAuthenticationFilter这个过滤器了,他主要是拦截账号密码封装整UsernamePasswordAuthenticationToken交给AuthenticationManager去做认证的,我是怎么知道的呢?我们来看下重要源码:

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 表单参数键的常量
    public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";

    // 默认的认证请求URL和HTTP方法
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = 
        new AntPathRequestMatcher("/login", "POST");

    // 用于存储用户名和密码参数名的字段
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    
    // 标志是否只允许POST请求
    private boolean postOnly = true;

    // 使用默认请求匹配器的构造函数
    public UsernamePasswordAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    // 使用自定义认证管理器的构造函数
    public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        
        // 如果postOnly为true,检查请求方法是否为POST
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("不支持的认证方法: " + request.getMethod());
        }

        // 从请求中获取用户名
        String username = obtainUsername(request);
        username = (username != null) ? username.trim() : "";

        // 从请求中获取密码
        String password = obtainPassword(request);
        password = (password != null) ? password : "";

        // 创建认证令牌
        UsernamePasswordAuthenticationToken authRequest = 
            UsernamePasswordAuthenticationToken.unauthenticated(username, password);

        // 为认证请求设置附加详情
        setDetails(request, authRequest);

        // 将认证委托给认证管理器
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    // 从请求中获取用户名的方法
    protected String obtainUsername(HttpServletRequest request) {
        return request.getParameter(usernameParameter);
    }

    // 从请求中获取密码的方法
    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(passwordParameter);
    }

    // 为认证请求设置附加详情的方法
    protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
}
重要看这段汉

我们看到这哥们把UsernamePasswordAuthenticationToken给了AuthenticationManager,那么问题又来了AuthenticationManager是如果完成验证的呢?

3.AuthenticationManage

我们点击源码后发现AuthenticationManager是个接口,接口怎么实现认证呢?不可能的嘛,所以我们找下实现类:

我们重点关注这里类

868ff61b384948778688dd824d58db44.png021651d5450146ba91611c966c9da71c.png

我们发现这哥们是 AuthenticationManager 的实现子类之一,也是我们最常用的一个实现。我们来看下实际做验证的代码,源码重点的部分0760e225c7fc487e9be4e0ba0671e611.png

上图中我们发现循环了AuthenticationProvider provider,说明我们可能出现多个provider,那么AuthenticationProvider是个接口所以主要用于认证的是ProviderManager 子类,如果我们有多种认证方式,那么只依靠一个ProviderManager 本身来实现 authenticate() 接口是完全不够的,所以上面我们看到了循环去调用authenticate()。

了解上面3个类后我们发现这三个类是一个链路,也就是张老演员图了:

453293e06f91467b9b9b2ef13f74f03e.png

上图中我们看到UsernamePasswordAuthenticationToken需要账号密码,那么我们要实现的是手机验证码,如果你重上面看到这里应该知道怎么实现了吧?

手机验证整合Security

思路:

1.既然UsernamePasswordAuthenticationToken的爹是它AbstractAuthenticationToken,那么我们就照猫画虎重写一个

2.读过上面的都知道UsernamePasswordAuthenticationFilter是拦截账号和密码的,而且这孩子还继承了家产AbstractAuthenticationProcessingFilter,那么我们是要拿手机号和验证码的,所以照猫画虎重写一个

3.然后这哥们UsernamePasswordAuthenticationFilter是不是要把UsernamePasswordAuthenticationToken给AuthenticationManager然后AuthenticationManager又是个接口,他需要实现了ProviderManger,调用authenticate验证,那么里面遍历了ProviderManger,那么我们是不是自己做一个ProviderManger让它去调用authenticate验证不就完了?所以要整了类实现下AuthenticationProvider接口。

开整

SmsCodeAuthenticationToken

/*
 *这一步的作用是为了替换原有系统的 UsernamePasswordAuthenticationToken 用来做验证
 *
 * 代码都是从UsernamePasswordAuthenticationToken 里粘贴出来的
 *
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名,
     * 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    public Object getCredentials() {
        return null;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
    }
}

这样第一个就完成了 

SmsCodeAuthenticationFilter


/**
 * 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    /**
     * form表单中手机号码的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "phone";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter(AuthenticationManager authenticationManager,
                                       AuthenticationSuccessHandler authenticationSuccessHandler,
                                       AuthenticationFailureHandler authenticationFailureHandler
                                       ) {
        super(new AntPathRequestMatcher(PHONE_LOGIN_PATH, "POST"));
        setAuthenticationManager(authenticationManager);
        setAuthenticationSuccessHandler(authenticationSuccessHandler);
        setAuthenticationFailureHandler(authenticationFailureHandler);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        // 电话号码
        String mobile = request.getParameter("phone");
        if (StringUtils.isEmpty(mobile)) {
            throw new CustomException(500,"手机号码不能为空");
        }
        return this.getAuthenticationManager().authenticate(new SmsCodeAuthenticationToken(mobile));
    }


    protected String obtainUsername(HttpServletRequest request) {
        StringBuilder data = new StringBuilder();
        String line;
        BufferedReader reader;
        try {
            reader = request.getReader();
            while ((null!=(line=reader.readLine()))){
                data.append(line);
            }
        }catch (IOException e){
            return null;
        }
        User user = JSONUtil.toBean(data.toString(), User.class);
        return user.getPhone();
    }

    protected String obtainPassword(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
}

 SmsCodeAuthenticationProvider

**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 */
@Configuration
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();
//       //校验验证码
        checkSmsCode(mobile);
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        if (userDetails == null) {
            throw new CustomException(400, "用户不存在");
        }
        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回  role_code
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    /**
     * 校验短信验证码
     *
     * @param mobile
     */
    private void checkSmsCode(String mobile) {
        boolean phoneInvalid = RegexUtils.isPhoneInvalid(mobile);
        if (phoneInvalid) {
            throw new CustomException(400, "手机号格式不正确~");
        }
        Boolean isHaveKey = stringRedisTemplate.hasKey(RedisConstants.LOGIN_CODE_KEY + mobile);
        if (!isHaveKey) {
            throw new CustomException(401, "验证码失效~");
        }
//        获取redis进行比较
        String code = stringRedisTemplate.opsForValue().get(RedisConstants.LOGIN_CODE_KEY + mobile);
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        String inputCode = request.getParameter("code");
        if (!code.equals(inputCode)) {
            throw new CustomException(500, "验证码错误~");
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public StringRedisTemplate getStringRedisTemplate() {
        return stringRedisTemplate;
    }

    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取请求中的验证码
     *
     * @param request
     * @return HttpServletRequest
     */
    protected String obtainCode(HttpServletRequest request) {
        StringBuilder data = new StringBuilder();
        String line;
        BufferedReader reader;
        try {
            reader = request.getReader();
            while ((null != (line = reader.readLine()))) {
                data.append(line);
            }
        } catch (IOException e) {
            return null;
        }
        LoginDto loginDto = JSONUtil.toBean(data.toString(), LoginDto.class);
        return loginDto.getCode();
    }
}

 大功告成了,然后我们发现上面的代码使用UserDetail,我们还是来看老演员

09e347a694cc4e89ba500dae6cd3f0e3.png

其实你上面重新的三个就到这了。

注意: 需结合我上篇文章的修改着写

UserDetail

@Service
@RequiredArgsConstructor
@Slf4j
public class SysUserDetailsService implements UserDetailsService {


    private final UserMapper userMapper;

    private final RoleService roleService;

    private final MenuService menuService;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//        判断是否是账密密码登录还是手机号登录
        UserAuthInfo userAuthInfo = null;
        if (isMobileNO(username)) {
            userAuthInfo = this.userMapper.getUserNameByPhone(username);
//         如果查询不到那么为手机注册的用户
            if (Objects.isNull(userAuthInfo)) {
                userAuthInfo = this.register(username);
            }
        } else {
            userAuthInfo = this.userMapper.getUserAuthInfo(username);
        }
        if (userAuthInfo == null) {
            throw new UsernameNotFoundException("未找到对应账号,请检查输入信息~");
        }
        Set<String> roles = userAuthInfo.getRoles();
        if (CollectionUtil.isNotEmpty(roles)) {
            Set<String> perms = menuService.listRolePerms(roles);
            userAuthInfo.setPerms(perms);
        }
        return new SysUserDetails(userAuthInfo);
    }


    public static boolean isMobileNO(String input) {
        // 中国大陆手机号正则表达式
        String regex = "^1[3-9]\\d{9}$";
        return input.matches(regex);
    }

    /**
     * 手机注册账号
     */
    private UserAuthInfo register(String username) {
        log.info("手机号用户注册--{}--开始", username);
       try {
           User user = this.userMapper.selectOne(new LambdaQueryWrapper<User>().eq(User::getPhone, username));
//        判断该用户是否存在 不存在添加用户
           if (Objects.isNull(user)) {
               user = new User().setPhone(username).setUsername(UUID.randomUUID().toString()).setType(0).setVxAvatar("https://oss.youlai.tech/youlai-boot/2023/05/16/811270ef31f548af9cffc026dfc3777b.gif");
//            //添加用户
               userMapper.insert(user);
               //  默认分配权限用户
               List<Long> roleIdList = new ArrayList<>();
               roleIdList.add(17L);//用户
               AssginRoleVo assginRoleVo = new AssginRoleVo().setUserId(Long.parseLong(user.getId())).setRoleIdList(roleIdList);
               roleService.doAssignRole(assginRoleVo);
           }
       }catch (Exception e){
           log.error("手机号用户注册--{}--异常", username,e);
           throw new CustomException(500,"手机号用户注册异常,请联系管理员~");
       }
        log.info("手机号用户注册--{}--结束", username);
        return this.userMapper.getUserNameByPhone(username);
    }

}

 最后一步加上配置文件即可

SecurityConfig


/**
 * Spring Security 权限配置
 *
 * @author cws
 */
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    // 自定义未认证处理类
    private final MyAuthenticationEntryPoint authenticationEntryPoint;
    // 自定义无权限访问处理类
    @Resource
    private final MyAccessDeniedHandler accessDeniedHandler;

    // Redis操作模板
    @Autowired
    private final RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider;

    @Autowired
    private SysUserDetailsService userDetailsService;

    private final Filter globalSpringSecurityExceptionHandler = new CustomSecurityExceptionHandler();

    @Autowired
    LoginFailHandler loginFailHandler;

    @Autowired
    LoginSuccessHandler loginSuccessHandler;


   @Resource
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 配置Spring Security过滤器链。
     *
     * @param http HttpSecurity对象,用于构建安全配置
     * @return 构建好的SecurityFilterChain对象
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http,AuthenticationManager authenticationManager) throws Exception {
        http
                .authorizeHttpRequests(requestMatcherRegistry ->// 配置请求授权规则
                        //登录路径公开访问
                        requestMatcherRegistry.requestMatchers(SecurityConstants.LOGIN_PATH,
                                        SecurityConstants.LOGOUT_PATH,
                                        SecurityConstants.VERIFY_TREE_PATH,
                                        SecurityConstants.GET_PHONE_CODE_PATH
                                ).permitAll()
                                // 其他所有请求都需要认证
                                .anyRequest().authenticated()
                )
                // 禁用Session创建
                .sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置异常处理
                .exceptionHandling(httpSecurityExceptionHandlingConfigurer ->
                        httpSecurityExceptionHandlingConfigurer
                                // 设置未认证处理入口
                                .authenticationEntryPoint(authenticationEntryPoint)
                                // 设置无权限访问处理
                                .accessDeniedHandler(accessDeniedHandler)
                )
                // 禁用CSRF保护
                .csrf(AbstractHttpConfigurer::disable)
                .formLogin(AbstractHttpConfigurer::disable)
                .httpBasic(AbstractHttpConfigurer::disable)
                .logout(AbstractHttpConfigurer::disable)
                .sessionManagement(AbstractHttpConfigurer::disable)
                .csrf(AbstractHttpConfigurer::disable)
                // requestCache用于重定向,前后端分析项目无需重定向,requestCache也用不上
                .requestCache(cache -> cache
                        .requestCache(new NullRequestCache())
                )
        ;
        //添加手机号登陆过滤器
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        smsCodeAuthenticationProvider.setStringRedisTemplate(stringRedisTemplate);
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(
                                new ProviderManager(
                        List.of(smsCodeAuthenticationProvider)),
                loginSuccessHandler,
                loginFailHandler);
        http.addFilterBefore(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

        // JWT 校验过滤器
        http.addFilterBefore(new JwtValidationFilter(redisTemplate), UsernamePasswordAuthenticationFilter.class);
        // 其他未知异常. 尽量提前加载。
        http.addFilterBefore(globalSpringSecurityExceptionHandler, SecurityContextHolderFilter.class);
        // 构建并返回过滤器链
        return http.build();
    }

    /**
     * 不走过滤器链的放行配置
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        // 忽略指定路径的安全检查
        return (web) -> web.ignoring()
                .requestMatchers(
                        "/api/v1/auth/captcha",
                        "/webjars/**",
                        "/doc.html",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/swagger-ui/**",
                        "/swagger-ui.html",
                        "/ws/**",
                        "/ws-app/**"
                );
    }

    /**
     * 密码编码器
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 手动注入AuthenticationManager,用于处理认证和授权请求。
     *
     * @param authenticationConfiguration 认证配置对象
     * @return AuthenticationManager对象
     * @throws Exception 配置过程中可能抛出的异常
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        // 获取认证管理器实例
        return authenticationConfiguration.getAuthenticationManager();
    }


}

认证成功或者失败的处理器

@Component
public class LoginSuccessHandler extends
    AbstractAuthenticationTargetUrlRequestHandler implements AuthenticationSuccessHandler {

  @Autowired
  private ApplicationEventPublisher applicationEventPublisher;

;

  public LoginSuccessHandler() {
    this.setRedirectStrategy(new RedirectStrategy() {
      @Override
      public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url)
          throws IOException {
        // 更改重定向策略,前后端分离项目,后端使用RestFul风格,无需做重定向
        // Do nothing, no redirects in REST
      }
    });
  }

  @Override
  public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
      Authentication authentication) throws IOException {
    Object principal = authentication.getPrincipal();
    if (principal == null || !(principal instanceof UserDetails)) {
      throw new CustomException(500, "登陆认证成功后,authentication.getPrincipal()返回的Object对象必须是:UserLoginInfo!");
    }
    String token= JwtUtils.generateToken(authentication);
    LoginVo loginVo = new LoginVo();
    loginVo.setTokenType("Bearer ");
    loginVo.setAccessToken(token);
    // 虽然APPLICATION_JSON_UTF8_VALUE过时了,但也要用。因为Postman工具不声明utf-8编码就会出现乱码
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    PrintWriter writer = response.getWriter();
    writer.print(JSONUtil.toJsonStr(Result.success(loginVo)));
    writer.flush();
    writer.close();
  }
}



@Component
public class LoginFailHandler implements AuthenticationFailureHandler {

  @Override
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                      AuthenticationException exception) throws IOException, ServletException {
    String errorMessage = exception.getMessage();
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    PrintWriter writer = response.getWriter();
    writer.print(JSONUtil.toJsonStr(Result.fail(errorMessage)));
    writer.flush();
    writer.close();
  }
}

总结:重点在于理解上面的三个类及整个Security的认证流程即可实现。

关注后面更新接入第三方做认证的案例及阿里云短信服务~~~

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

springboot小区物业报修管理系统-计算机毕业设计源码03418

摘 要 本课题的研究对象是小区物业报修管理系统app的设计与实现&#xff0c;该系统实现了用户管理、业主信息管理、报修信息管理、维修记录管理、通知公告管理等功能。本系统在设计上&#xff0c;考虑到系统内容以及系统的受众群体&#xff0c;在系统的美工方面采用了比较正规的…

Automated Construction of Theme-specific Knowledge Graphs

文章目录 题目摘要引言相关工作方法实验消融研究结论 题目 自动构建主题特定知识图谱 论文地址&#xff1a;https://arxiv.org/pdf/2404.19146 摘要 尽管知识图谱 (KG) 广泛应用于问答、智能对话系统等各种任务&#xff0c;但现有的 KG 面临两大挑战&#xff1a;信息粒度有限和…

C语言深入理解指针(四)

目录 字符指针变量数组指针变量数组指针变量是什么数组指针变量怎么初始化 二维数组传参的本质函数指针变量函数指针变量的创建函数指针变量的使用代码typedef关键字 函数指针数组转移表 字符指针变量 字符指针在之前我们有提到过&#xff0c;&#xff08;字符&#xff09;&am…

5.MySQL表的约束

目录 表的约束空属性&#xff08;非空约束&#xff09;默认值&#xff08;default约束&#xff09;列描述&#xff08;comment&#xff09;zerofill主键&#xff08;primary key约束&#xff09;自增长唯一键&#xff08;unique约束&#xff09;外键 表的约束 如果我自由自在的…

MySQL(日志)

日志 日志分为三种&#xff1a; undo log &#xff08;回滚日志&#xff09;&#xff1a;用于事务回滚和MVCC redo log &#xff08;重做日志&#xff09;&#xff1a;用于故障恢复 binlog &#xff08;归档日志&#xff09;&#xff1a;用于数据备份和主从复制 undo log undo…

qt-C++笔记之作用等同的宏和关键字

qt-C笔记之作用等同的宏和关键字 code review! Q_SLOT 和 slots&#xff1a; Q_SLOT是slots的替代宏&#xff0c;用于声明槽函数。 Q_SIGNAL 和 signals&#xff1a; Q_SIGNAL类似于signals&#xff0c;用于声明信号。 Q_EMIT 和 emit&#xff1a; Q_EMIT 是 Qt 中用于发射…

Tomcat 靶场攻略

CVE-2017-12615 步骤一&#xff1a;环境搭建 cd vulhub/tomcat/CVE-2017-12615 docker-compose up -d docker ps 步骤二&#xff1a;漏洞复现 http://192.168.10.190:8080/ 步骤二&#xff1a;首页进行抓包 Tomcat允许适⽤put⽅法上传任意⽂件类型&#xff0c;但不允许js…

安卓13去掉下拉菜单的Dump SysUI 堆的选项 android13删除Dump SysUI 堆

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析3.1 位置13.2 位置24.代码修改5.编译6.彩蛋1.前言 客户需要去掉下拉菜单里面的Dump SysUI 堆图标,不让使用这个功能。 2.问题分析 android的下拉菜单在systemui里面,这里我们只需要定位到对应的添加代…

【优选算法之二分查找】No.5--- 经典二分查找算法

文章目录 前言一、二分查找模板&#xff1a;1.1 朴素二分查找模板1.2 查找区间左端点模板1.3 查找区间右端点模板 二、二分查找示例&#xff1a;2.1 ⼆分查找2.2 在排序数组中查找元素的第⼀个和最后⼀个位置2.3 搜索插⼊位置2.4 x 的平⽅根2.5 ⼭脉数组的峰顶索引2.6 寻找峰值…

Linux自主学习篇

用户及权限管理 sudo 是 "superuser do" 的缩写&#xff0c;是一个在类 Unix 操作系统&#xff08;如 Linux 和 macOS&#xff09;中使用的命令。它允许普通用户以超级用户&#xff08;root 用户&#xff09;的身份执行命令&#xff0c;从而获得更高的权限。 useradd…

多模态交互才是人机交互的未来

交互方式 在探讨文字交流、语音交流和界面交流的效率时&#xff0c;我们可以看到每种方式都有其独特的优势和局限性。文字交流便于记录和回溯&#xff0c;语音交流则在表达情绪和非语言信息方面更为高效&#xff0c;而界面交流则依赖于图形用户界面&#xff08;GUI&#xff09…

<<编码>> 第 16 章 存储器组织(4)--内存 示例电路

内存内部结构 info::操作说明 译码器用于写入, 操作同上 选择器用于输出, 操作同上 地址信号同时控制译码器和选择器, 注意地址的高位在右(比如 001 实际是 100, 选择的是 Q6 和 I6) 缺省情况下, 内部数据全是 0. 读者可先通过译码器写入, 再通过选择器输出 primary::在线交…

初学者的鸿蒙多线程并发之 TaskPool 踩坑之旅

1. 背景 目标群体&#xff1a;鸿蒙初学者 版本&#xff1a;HarmonyOS 3.1/4.0 背景&#xff1a;鸿蒙 App 的全局路由管理功能&#xff0c;需要在 App 启动时初始化对 raw 下的相关配置文件进行读取、解析并缓存。App 启动时涉及到了大量模块的初始化&#xff0c;好多模块都涉…

【machine learning-15-如何判定梯度下降是否在收敛】

我们在运行梯度下降的时候&#xff0c;如何判定梯度下降是否在收敛呢&#xff1f; 梯度下降的时候&#xff0c;权重和偏置根据如下的公式同时更新&#xff1a; 程序要做的就是更新w 和 b&#xff0c;让梯度下降尽快的收敛&#xff0c;但是如何判定正在收敛呢&#xff1f; 方法…

关于神经网络的一个介绍

这篇文章中&#xff0c;我将简单介绍下与神经网络有关的东西&#xff0c;包括它的基本模型&#xff0c;典型的算法以及与深度学习的联系等内容。 一、神经元 神经网络是由许多个神经元组成的&#xff0c;在生物的神经网络中&#xff0c;就是神经元间相互连接&#xff0c;传递…

Arthas getstatic(查看类的静态属性 )

文章目录 二、命令列表2.1 jvm相关命令### 2.1.7 getstatic&#xff08;查看类的静态属性 &#xff09; 二、命令列表 2.1 jvm相关命令 ### 2.1.7 getstatic&#xff08;查看类的静态属性 &#xff09; 使用场景&#xff1a; 我们项目部署在linux上&#xff0c;我有个本地内存…

从一到无穷大 #35 Velox Parquet Reader 能力边界

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 本作品 (李兆龙 博文, 由 李兆龙 创作)&#xff0c;由 李兆龙 确认&#xff0c;转载请注明版权。 文章目录 引言源码分析功能描述功能展望 引言 InfluxDB IOX这样完全不使用索引&#xff0c;只…

《沧浪之水》读后感

未完待续..... 未完待续.... 未完待续.... 【经典语录】 01、我一辈子的经验就是不要做瞎子&#xff0c;也不能做聋子&#xff0c;该听到的信息要听到&#xff0c;但是要做哑巴&#xff0c;看到了听到了心中有数就行了&#xff0c;可千万不要张口说什么。 02、你刚从学校毕业…

MQ入门(一):同步调用和异步调用--RabbitMQ基础入门

目录 1.初识MQ 1.1.同步调用 1.2.异步调用 1.3.技术选型 2.RabbitMQ 2.1.安装部署 2.2.RabbitMQ基本架构 2.3.收发消息 2.3.1.交换机 2.3.2.队列 2.3.3.绑定关系 2.3.4.发送消息 2.4.数据隔离 2.4.1.用户管理 2.4.2.virtual host 1.初识MQ 微服务一旦拆分&…

web前端字段大小写下划线转换工具

文章目录 前言一、如何使用&#xff1f;二、相关代码总结 前言 程序员在敲代码的过程中都要命名一些字段&#xff0c;但是Java语言对字段的命名规范和sql命名规范不一样&#xff0c;如下图所示&#xff0c;这种机械性的转换工作很劳神费力&#xff0c;为了省点劲写了一个web小…