IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring Security系列(22)- Security实现手机短信登录功能 -> 正文阅读

[Java知识库]Spring Security系列(22)- Security实现手机短信登录功能

准备

需求

采用手机号+短信验证码登录方式是很常见的一种需求。
在这里插入图片描述

那我们如何在Spring Security实现这种功能呢?

表单登录流程

首先再回顾一下用户名密码表单登录流程。

在这里插入图片描述

  1. 登录请求进入过滤器
  2. 调用认证管理器
  3. 查询数据库中的信息,返回UserDetails
  4. AuthenticationProvider校验请求和数据库中的密码是否配置
  5. 封装已认证信息

实现思路

我们可以完全按照表单登录,自定义过滤器、校验器等,实现短信登录功能。

案例演示

1. 获取验证码

登录页中,用户填写手机号后,会有一个获取验证码的按钮。

我们编写一个获取验证码的接口,模拟短信平台发送验证码,然后将验证码放在缓存中。

@RestController
@RequestMapping("/sms")
@Slf4j
public class SmsEndpoint {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 发送验证码接口
     *
     * @param phone
     * @return
     */
    @GetMapping("/send/code")
    public Map<String,String> msmCode(String phone) {
        // 1. 获取到手机号
        log.info(phone + "请求获取验证码");
        // 2. 模拟调用短信平台获取验证码,以手机号为KEY,验证码为值,存入Redis,过期时间一分钟
        String code = RandomUtil.randomNumbers(6);
        redisTemplate.opsForValue().setIfAbsent(phone, code, 60, TimeUnit.SECONDS);
        String saveCode = redisTemplate.opsForValue().get(phone);// 缓存中的code
        Long expire = redisTemplate.opsForValue().getOperations().getExpire(phone, TimeUnit.SECONDS); // 查询过期时间
        // 3. 验证码应该通过短信发给用户,这里直接返回吧
        Map<String,String> result=new HashMap<>();
        result.put("code",saveCode);
        result.put("过期时间",expire+"秒");
        return result;
    }
}

在WebSecurityConfigurerAdapter中放行验证码接口。

        http.authorizeRequests()
                .antMatchers("/login", "/sms/send/code","/sms/login").permitAll()

接口测试通过:

在这里插入图片描述

2. 编写AuthenticationToken类

参考UsernamePasswordAuthenticationToken编写一个AuthenticationToken类,这个类主要用于认证时,封装认证信息

public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 1L;

    private final Object principal;
    private Object credentials;

    public SmsAuthenticationToken(Object principal, Object credentials) {
        super((Collection) null);
        this.principal = principal;
        this.credentials = credentials;
        this.setAuthenticated(false);
    }

    public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @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;
    }
}

3. 编写过滤器

参考UsernamePasswordAuthenticationFilter写一个过滤器,拦截短信登录接口/sms/login,调用认证管理器进行认证。

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 设置拦截/sms/login短信登录接口
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
    // 认证参数
    private String phoneParameter = "phone";
    private String smsCodeParameter = "smsCode";
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String phone = this.obtainPhone(request);
            phone = phone != null ? phone : "";
            phone = phone.trim();
            String smsCode = this.obtainSmsCode(request);
            smsCode = smsCode != null ? smsCode : "";
            SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, smsCode);
            this.setDetails(request, authRequest);
            // 认证
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(this.smsCodeParameter);
    }

    @Nullable
    protected String obtainPhone(HttpServletRequest request) {
        return request.getParameter(this.phoneParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setPhoneParameter(String phoneParameter) {
        Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
        this.phoneParameter = phoneParameter;
    }

    public void setSmsCodeParameter(String smsCodeParameter) {
        Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
        this.smsCodeParameter = smsCodeParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.phoneParameter;
    }

    public final String getPasswordParameter() {
        return this.smsCodeParameter;
    }
}

4. 编写UserDetailsService

UserDetailsService主要负责根据用户名查询数据库信息,这里我们需要通过手机号查询用户信息。

@Service("smsUserDetailsService")
public class SmsUserDetailsService implements UserDetailsService {

    @Autowired
    UserService userService;

    /**
     * @param phone 手机号
     */
    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
        // 1. 数据库查询手机用户,这里需要写根据手机号查询用户信息,这里写死吧。。。
        UserInfoDO userInfoDO = userService.getUserInfoByName("test");
        if (userInfoDO == null) {
            throw new UsernameNotFoundException("手机号不存在!");
        }
        // 2. 设置权限集合,后续需要数据库查询
/*        List<GrantedAuthority> authorityList =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");*/
        // 2. 角色权限集合转为  List<GrantedAuthority>
        List<Role> roleList = userInfoDO.getRoleList();
        List<Permission> permissionList = userInfoDO.getPermissionList();
        List<GrantedAuthority> authorityList = new ArrayList<>();
        roleList.forEach(e -> {
            String roleCode = e.getRoleCode();
            if (StringUtils.isNotBlank(roleCode)) {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(roleCode);
                authorityList.add(simpleGrantedAuthority);
            }
        });
        permissionList.forEach(e -> {
            String code = e.getCode();
            if (StringUtils.isNotBlank(code)) {
                SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(code);
                authorityList.add(simpleGrantedAuthority);
            }
        });
        // 3. 返回自定义的用户信息
        MyUser myUser = new MyUser(userInfoDO.getUserName(), userInfoDO.getPassword(), authorityList);
        // 设置自定义扩展信息
        myUser.setDeptId(userInfoDO.getOrganizationId());
        myUser.setLoginTime(new Date());
        return myUser;
    }
}

5. 编写AuthenticationProvider

认证管理器会调用认证程序AuthenticationProvider进行认证操作,我们需要实现根据用户输入参数,校验验证码,匹配后,设置认证成功。

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsServiceImpl;

    private RedisTemplate<String, String> redisTemplate;

    public SmsAuthenticationProvider(@Qualifier("smsUserDetailsService") UserDetailsService userDetailsServiceImpl, RedisTemplate<String, String> redisTemplate) {
        this.userDetailsServiceImpl = userDetailsServiceImpl;
        this.redisTemplate = redisTemplate;
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
        String phone = "";
        if (principal instanceof String) {
            phone = (String) principal;
        }
        String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码
        // 1. 检验Redis手机号的验证码
        String redisCode = redisTemplate.opsForValue().get(phone);
        if (StrUtil.isEmpty(redisCode)) {
            throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
        }
        if (!inputCode.equals(redisCode)) {
            throw new BadCredentialsException("输入的验证码不正确,请重新输入");
        }
        // 2. 根据手机号查询用户信息
        UserDetails userDetails = userDetailsServiceImpl.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
        }
         // 3. 重新创建已认证对象,
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(principal,inputCode, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> aClass) {
        return SmsAuthenticationToken.class.isAssignableFrom(aClass);
    }
}

6. 编写短信认证配置类

编写短信认证配置类,将过滤器,认证程序等加入到配置中。

@Configuration
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    CustomAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private RedisTemplate<String,String> redisTemplate;
    @Autowired
    CustomAuthenticationFailureHandler authenticationFailureHandler;


    @Resource
    private SmsUserDetailsService smsUserDetailsService;
    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(smsUserDetailsService,redisTemplate);
        http.authenticationProvider(smsAuthenticationProvider)
                .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

将以上配置添加到主配置中,并需要在AuthenticationManager中添加DaoAuthenticationProvider,不然用户名密码登录会报错。

@Configuration
@EnableWebSecurity(debug = true)
public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {


    @Autowired
    MyUserDetailsService myUserDetailsService;
    @Autowired
    CustomAuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    CustomAccessDeniedHandler accessDeniedHandler;
    @Autowired
    CustomAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;
    @Autowired
    SmsAuthenticationProvider smsAuthenticationProvider;


    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf,开启跨域支持
        http.csrf().disable().cors();
        http.authorizeRequests()
                .antMatchers("/login", "/sms/send/code", "/sms/login").permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .failureHandler(authenticationFailureHandler)
                .successHandler(authenticationSuccessHandler); // 配置登录失败处理器
        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 配置DeniedHandler处理器
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        // 添加手机号短信登录
        http.apply(smsSecurityConfigurerAdapter);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 设置自定义用户认证
        //auth.userDetailsService(myUserDetailsService);
        super.configure(auth);
    }

    /**
     * 将Provider添加到认证管理器中
     *
     * @return
     * @throws Exception
     */
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(smsAuthenticationProvider, daoAuthenticationProvider()));
        authenticationManager.setEraseCredentialsAfterAuthentication(false);
        return authenticationManager;
    }

    /**
     * 注入密码解析器到IOC中
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
        daoAuthenticationProvider.setUserDetailsService(myUserDetailsService);
        daoAuthenticationProvider.setHideUserNotFoundExceptions(false); // 设置显示找不到用户异常
        return daoAuthenticationProvider;
    }
}

7. 测试

首先获取短信验证码:
在这里插入图片描述
然后调用短信登录接口,登录成功并返回了认证信息。
在这里插入图片描述

然后访问另一个资源接口,发现成访问到了。
在这里插入图片描述
使用用户名密码也可以登录:
在这里插入图片描述

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-08-19 11:55:18  更:2021-08-19 11:57:05 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 8:57:10-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码