java
主页 > 软件编程 > java >

Spring Security实现短信验证码登录功能的教程

2020-05-20 | 秩名 | 点击:

开发短信验证码接口

获取验证码

短信验证码的发送获取逻辑和图片验证码类似,这里直接贴出代码。

?
1
2
3
4
5
6
7
8
9
10
@GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 创建验证码
        ValidateCode smsCode = createCodeSmsCode(request);
        // 将验证码放到session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, smsCode);
        String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        // 发送验证码
        smsCodeSender.send(mobile, smsCode.getCode());
    }
 

前端代码

?
1
2
3
4
5
6
7
8
9
10
11
<tr>
                <td>手机号:</td>
                <td><input type="text" name="mobile" value="13012345678"></td>
            </tr>
            <tr>
                <td>短信验证码:</td>
                <td>
                    <input type="text" name="smsCode">
                    <a href="/code/sms?mobile=13012345678" rel="external nofollow" >发送验证码</a>
                </td>
            </tr>
 

短信验证码流程原理

短信验证码登录和用户名密码登录对比

流程对比

步骤流程

代码实现

SmsCodeAuthenticationToken

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
    private final Object principal;
    /**
     * 进入SmsAuthenticationFilter时,构建一个未认证的Token
     *
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }
    /**
     * 认证成功以后构建为已认证的Token
     *
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(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();
    }
}
 

SmsCodeAuthenticationFilter

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    private String mobileParameter = "mobile";
    private boolean postOnly = true;
 /**
 * 表示要处理的请求路径
 */
    public SmsCodeAuthenticationFilter() {
 super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }
 @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        String mobile = obtainMobile(request);
        if (mobile == null) {
            mobile = "";
        }
        mobile = mobile.trim();
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
        // 把请求信息设到Token中
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    /**
     * 获取手机号
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }
    public void setMobileParameter(String usernameParameter) {
        Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
        this.mobileParameter = usernameParameter;
    }
    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
    public final String getMobileParameter() {
        return mobileParameter;
    }
}
 

SmsAuthenticationProvider

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
    private UserDetailsService userDetailsService;
 /**
 * 进行身份认证的逻辑
 *
 * @param authentication
 * @return
 * @throws AuthenticationException
 */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        
        UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("无法获取用户信息");
        }
        
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }
 /**
 * 表示支持校验的Token,这里是SmsCodeAuthenticationToken
 *
 * @param authentication
 * @return
 */
    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}
 

ValidateCodeFilter

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    /**
     * 验证码校验失败处理器
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 系统配置信息
     */
    @Autowired
    private SecurityProperties securityProperties;
    /**
     * 系统中的校验码处理器
     */
    @Autowired
    private ValidateCodeProcessorHolder validateCodeProcessorHolder;
    /**
     * 存放所有需要校验验证码的url
     */
    private Map<String, ValidateCodeType> urlMap = new HashMap<>();
    /**
     * 验证请求url与配置的url是否匹配的工具类
     */
    private AntPathMatcher pathMatcher = new AntPathMatcher();
    /**
     * 初始化要拦截的url配置信息
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        urlMap.put("/authentication/mobile", ValidateCodeType.SMS);
        addUrlToMap(securityProperties.getCode().getSms().getUrl(), ValidateCodeType.SMS);
    }
    /**
     * 讲系统中配置的需要校验验证码的URL根据校验的类型放入map
     *
     * @param urlString
     * @param type
     */
    protected void addUrlToMap(String urlString, ValidateCodeType type) {
        if (StringUtils.isNotBlank(urlString)) {
            String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ",");
            for (String url : urls) {
                urlMap.put(url, type);
            }
        }
    }
    /**
     * 验证短信验证码
     *
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        ValidateCodeType type = getValidateCodeType(request);
        if (type != null) {
            logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
            try {
                // 进行验证码的校验
                validateCodeProcessorHolder.findValidateCodeProcessor(type)
                        .validate(new ServletWebRequest(request, response));
                logger.info("验证码校验通过");
            } catch (ValidateCodeException exception) {
                // 如果校验抛出异常,则交给我们之前文章定义的异常处理器进行处理
                authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
                return;
            }
        }
        // 继续调用后边的过滤器
        chain.doFilter(request, response);
    }
    /**
     * 获取校验码的类型,如果当前请求不需要校验,则返回null
     *
     * @param request
     * @return
     */
    private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
        ValidateCodeType result = null;
        if (!StringUtils.equalsIgnoreCase(request.getMethod(), "GET")) {
            Set<String> urls = urlMap.keySet();
            for (String url : urls) {
                if (pathMatcher.match(url, request.getRequestURI())) {
                    result = urlMap.get(url);
                }
            }
        }
        return result;
    }
}
 

添加配置

SmsCodeAuthenticationSecurityConfig

作用:配置SmsCodeAuthenticationFilter,后面需要把这些配置加到主配置类BrowserSecurityConfig

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    
    @Autowired
    private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
    
    @Autowired
    private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
    
    @Autowired
    private UserDetailsService userDetailsService;
    
    @Autowired
    private PersistentTokenRepository persistentTokenRepository;
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        // 设置AuthenticationManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 设置登录成功处理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(meicloudAuthenticationSuccessHandler);
        // 设置登录失败处理器
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(meicloudAuthenticationFailureHandler);
        String key = UUID.randomUUID().toString();
        smsCodeAuthenticationFilter.setRememberMeServices(new PersistentTokenBasedRememberMeServices(key, userDetailsService, persistentTokenRepository));
        
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
        // 将自己写的Provider加到Provider集合里去
        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
 

BrowserSecurityConfig

作用:主配置类;添加短信验证码配置类、添加SmsCodeAuthenticationSecurityConfig配置

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private DataSource dataSource;
    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private AuthenticationSuccessHandler meicloudAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler meicloudAuthenticationFailureHandler;
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 验证码校验过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        // 将验证码校验过滤器加到 UsernamePasswordAuthenticationFilter 过滤器之前
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                // 当用户登录认证时默认跳转的页面
                .loginPage("/authentication/require")
                // 以下这行 UsernamePasswordAuthenticationFilter 会知道要处理表单的 /authentication/form 请求,而不是默认的 /login
                .loginProcessingUrl("/authentication/form")
                .successHandler(meicloudAuthenticationSuccessHandler)
                .failureHandler(meicloudAuthenticationFailureHandler)
                // 配置记住我功能
                .and()
                .rememberMe()
                // 配置TokenRepository
                .tokenRepository(persistentTokenRepository())
                // 配置Token过期时间
                .tokenValiditySeconds(3600)
                // 最终拿到用户名之后,使用UserDetailsService去做登录
                .userDetailsService(userDetailsService)
                .and()
                .authorizeRequests()
                // 排除对 "/authentication/require" 和 "/meicloud-signIn.html" 的身份验证
                .antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/*").permitAll()
                // 表示所有请求都需要身份验证
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable()// 暂时把跨站请求伪造的功能关闭掉
                // 相当于把smsCodeAuthenticationSecurityConfig里的配置加到上面这些配置的后面
                .apply(smsCodeAuthenticationSecurityConfig);
    }
    /**
     * 记住我功能的Token存取器配置
     *
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 启动的时候自动创建表,建表语句 JdbcTokenRepositoryImpl 已经都写好了
        tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
}

原文链接:https://blog.csdn.net/qq_36221788/article/details/106169271
相关文章
最新更新