Security之短信登录篇

1、功能实现

Security默认提供的是用户名密码登录模式,然后我们参考用户名密码登录自定义实现短信登录模式
这样就多了一种登录模式,在登录的时候可以自行选择登录模式

2、security07 子工程

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.yzm</groupId>
        <artifactId>security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>

    <artifactId>security07</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security07</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>com.yzm</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.192.128:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: 1234

  main:
    allow-bean-definition-overriding: true

mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.yzm.security07.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

3、短信认证过滤器

用户名密码的认证过滤器是 UsernamePasswordAuthenticationFilter
那么我们就仿照它实现用于短信认证的过滤器

package com.yzm.security07.config;

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 短信认证过滤器
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // form表单中手机号码的字段name
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    // 拦截/sms/login
    private static final AntPathRequestMatcher DEFAULT_SMS_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
    private String mobileParameter = "mobile";
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(DEFAULT_SMS_PATH_REQUEST_MATCHER);
    }

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

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }

        String mobile = this.obtainMobile(request);
        mobile = mobile != null ? mobile.trim() : "";

        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
        this.setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    @Nullable
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

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

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public String getMobileParameter() {
        return this.mobileParameter;
    }

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

对应的UsernamePasswordAuthenticationToken 改造成 SmsAuthenticationToken

package com.yzm.security07.config;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

import java.util.Collection;

/**
 * 短信登录 AuthenticationToken
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 554008100412847685L;

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

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

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

    @Override
    public Object getCredentials() {
        return null;
    }

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

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

}

4、短信认证 Provider

自定义短信认证 Provider
把AbstractUserDetailsAuthenticationProvider跟其子类,DaoAuthenticationProvider的作用结合一起写了

package com.yzm.security07.config;

import com.yzm.common.utils.HttpUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * 短信认证 Provider
 */
public class SmsAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;

    public SmsAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

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

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
        String mobile = (String) authenticationToken.getPrincipal();
        checkSmsCode(mobile);

        // 相当于DaoAuthenticationProvider的retrieveUser()
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        
        // 相当于AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication()
        SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

	// 检查验证码
    private void checkSmsCode(String mobile) {
        HttpServletRequest request = HttpUtils.getHttpServletRequest();
        String inputCode = request.getParameter("smsCode");

        Map<String, Object> smsMap = (Map<String, Object>) request.getSession().getAttribute("smsCode");
        if (smsMap == null) {
            throw new BadCredentialsException("未检测到申请验证码");
        }

        String applyMobile = (String) smsMap.get("mobile");
        int code = (int) smsMap.get("code");

        if (!applyMobile.equals(mobile)) {
            throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
        }

        if (code != Integer.parseInt(inputCode)) {
            throw new BadCredentialsException("验证码错误");
        }
    }

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

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }
}

5、SecurityConfig 配置类

将自定义的SmsAuthenticationFilter添加到Security框架的过滤链中

package com.yzm.security07.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;

    public SecurityConfig(@Qualifier("secUserDetailsServiceImpl") UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    /**
     * 密码编码器
     * passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置用户
     * 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 从数据库获取用户
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    /**
     * 自定义SmsAuthenticationFilter需要注入AuthenticationManager,否则报空指针
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    //配置资源权限规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF跨域
                .csrf().disable()
                // addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter
                .addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)
                .authenticationProvider(new SmsAuthenticationProvider(userDetailsService))

                // 登录
                .formLogin()
                .loginPage("/auth/login") //指定登录页的路径,默认/login
                .loginProcessingUrl("/login") //指定自定义form表单请求的路径(必须跟login.html中的form action=“url”一致)
                .defaultSuccessUrl("/home", true) // 登录成功后的跳转url地址
                .failureUrl("/auth/login?error") // 登录失败后的跳转url地址
                .permitAll()
                .and()

                .exceptionHandling()
                .accessDeniedPage("/401") // 拒接访问跳转页面
                .and()

                // 退出登录
                .logout()
                .permitAll()
                .and()

                // 访问路径URL的授权策略,如注册、登录免登录认证等
                .authorizeRequests()
                .antMatchers("/", "/home", "/register", "/auth/login").permitAll() //指定url放行
                .antMatchers("/sms/code").permitAll() //获取短信验证码
                .anyRequest().authenticated() //其他任何请求都需要身份认证
        ;
    }
}

6、验证码接口

此次演示只是模拟手机短信登录,手机号既是用户名,验证码是随机数并存储到session中的
在HomeController新增接口

    @GetMapping("/sms/code")
    public String sms(String mobile, HttpSession session) {
        int code = (int) Math.ceil(Math.random() * 9000 + 1000);

        Map<String, Object> map = new HashMap<>(16);
        map.put("mobile", mobile);
        map.put("code", code);

        session.setAttribute("smsCode", map);
        log.info("{}:为 {} 设置短信验证码:{}", session.getId(), mobile, code);
        return "redirect:/auth/login";
    }
.antMatchers("/sms/code").permitAll() //获取短信验证码

7、登录页面

提供两种登录模式

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<h1 th:if="${param.error}">Invalid username or password.</h1>

<h2>用户名密码登录</h2>
<form action="/login" method="post">
    <p>
        <label for="username">Username</label>
        <input type="text" id="username" name="username" placeholder="Username">
    </p>
    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password" placeholder="Password">
    </p>
    <p>
        <label>
            <input type="checkbox" name="remember-me">
        </label> Remember me on this computer.
    </p>
    <button type="submit">Sign in</button>
</form>

<h2>短信登录</h2>
<form method="post" action="/sms/login">
    <div>
        手机号:<label for="mobile"></label><input type="text" id="mobile" name="mobile" value="yzm">
    </div>
    <div>
        验证码:<label><input type="text" name="smsCode"></label>
        <a href="javascript:;" onclick="sendSms()">获取验证码</a>
    </div>
    <div>
        <button type="submit">立即登陆</button>
    </div>
</form>

<script>
    function sendSms() {
        window.location.href = '/sms/code?mobile=' + document.getElementById("mobile").value;
    }
</script>
</body>
</html>

8、测试

启动项目,登录页
在这里插入图片描述

用户名密码登录yzm,没问题
退出/logout 换短信登录,先获取验证码
在这里插入图片描述
输入验证码进行登录,也是没问题的

9、短信认证流程

1 首先入口还是 AbstractAuthenticationProcessingFilter#doFilter

// addFilterAfter 在过滤链中的指定Filter(第二个参数)之后,添加Filter
.addFilterAfter(new SmsAuthenticationFilter(authenticationManagerBean()), UsernamePasswordAuthenticationFilter.class)

new AntPathRequestMatcher("/sms/login", "POST");
new AntPathRequestMatcher("/login", "POST");

这里我们是把短信认证放在用户名密码认证之后,
SmsAuthenticationFilter 拦截路径是/sms/login,而UsernamePasswordAuthenticationFilter是/login
短信登录发送请求/sms/login,
不是UsernamePasswordAuthenticationFilter需要拦截的,所以放行
是被我们自定义的SmsAuthenticationFilter拦截了,所以走了SmsAuthenticationFilter过滤器

2 AbstractAuthenticationProcessingFilter调用SmsAuthenticationFilter#attemptAuthentication()方法
3 SmsAuthenticationFilter构造未认证的SmsAuthenticationToken并提交给认证管理器AuthenticationManager
4 由认证管理器的子类ProviderManager遍历查找有没有支持SmsAuthenticationToken的AuthenticationProvider,发现我们自定义的SmsAuthenticationProvider支持
5 SmsAuthenticationProvider获取用户信息,并构造完全认证的Authentication对象返回
6 AbstractAuthenticationProcessingFilter接收的返回的完全认证Authentication对象之后,进行其他处理

相关链接

首页
上一篇:授权流程篇
下一篇:整合JWT篇

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Security提供了一种简单的方式来实现短信登录功能。下面是一个基本的示例配置: 1. 首先,确保你的项目中已经引入了Spring Security依赖。 2. 创建一个用于处理短信登录的控制器。例如,你可以创建一个`SmsLoginController`类。 ```java @RestController public class SmsLoginController { @PostMapping("/login/sms") public ResponseEntity<String> loginWithSms(@RequestParam("phoneNumber") String phoneNumber, @RequestParam("smsCode") String smsCode) { // 在这里编写验证逻辑和登录逻辑 // 验证手机号码和短信验证码是否匹配,并且登录用户 return ResponseEntity.ok("登录成功"); } } ``` 3. 创建一个Spring Security配置类,并进行相应的配置。例如,你可以创建一个`SecurityConfig`类。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login/sms").permitAll() // 允许短信登录接口的匿名访问 .anyRequest().authenticated() // 其他请求需要认证 .and() .formLogin() .disable() // 禁用表单登录 .logout() .disable() // 禁用注销功能 .csrf() .disable(); // 禁用CSRF保护(仅供示例,实际项目需要启用CSRF保护) } } ``` 4. 设置短信登录的配置。可以通过实现`AuthenticationProvider`接口来自定义短信登录的验证逻辑。例如,你可以创建一个`SmsCodeAuthenticationProvider`类。 ```java @Component public class SmsCodeAuthenticationProvider implements AuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String phoneNumber = authentication.getPrincipal().toString(); String smsCode = authentication.getCredentials().toString(); // 在这里编写验证逻辑,例如调用短信服务商的接口校验短信验证码 // 校验通过后,创建一个认证成功的Authentication对象 SmsCodeAuthenticationToken authenticatedToken = new SmsCodeAuthenticationToken(phoneNumber, null); authenticatedToken.setDetails(authentication.getDetails()); return authenticatedToken; } @Override public boolean supports(Class<?> authentication) { return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication)); } } ``` 5. 注册短信登录配置。在上述的`SecurityConfig`类中,通过重写`configure`方法注册短信登录的配置。 ```java @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SmsCodeAuthenticationProvider smsCodeAuthenticationProvider; @Override protected void configure(HttpSecurity http) throws Exception { // 省略其他配置... // 注册短信登录的配置 SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler()); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SimpleUrlAuthenticationFailureHandler()); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } } ``` 现在,你可以使用`/login/sms`接口来进行短信登录了。向该接口发送POST请求,携带手机号码和短信验证码作为参数即可实现短信登录功能。请注意,以上只是一个简单的示例,实际项目中可能需要进行更多的配置和处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值