栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Security 短信登录篇08

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Security 短信登录篇08

功能

Security默认提供的是用户名密码登录模式,然后我们参考用户名密码登录自定义实现短信登录模式
这样就多了一种登录模式,在登录的时候可以自行选择登录模式
学习本篇之前,最好先了解下上一篇 认证流程篇

1 security07 子工程


    4.0.0

    
        com.yzm
        security
        0.0.1-SNAPSHOT
        ../pom.xml 
    

    security07
    0.0.1-SNAPSHOT
    jar
    security07
    Demo project for Spring Boot

    
        
            com.yzm
            common
            0.0.1-SNAPSHOT
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    



项目结构

application.yml

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

  main:
    allow-bean-definition-overriding: true

mybatis-plus:
  mapper-locations: classpath:/mapper
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;


public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = 554008100412847685L;

    
    private final Object principal;

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

    
    public SmsAuthenticationToken(Object principal, Collection 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();
    }

}
3 短信认证 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;


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 smsMap = (Map) 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;
    }
}
4 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;
    }

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

    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 从数据库获取用户
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    
    @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() //其他任何请求都需要身份认证
        ;
    }
}

5 验证码接口

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

在HomeController新增接口

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

        Map 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() //获取短信验证码
6 登录页面

提供两种登录模式

login.html




    
    登录页


Invalid username or password.

用户名密码登录

Remember me on this computer.

短信登录

手机号:
验证码: 获取验证码
7 测试

启动项目,登录页

用户名密码登录yzm,没问题
退出/logout 换短信登录,先获取验证码

输入验证码进行登录,也是没问题的

8 短信认证流程

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对象之后,进行其他处理

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/351890.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号