1. 程式人生 > >SpringSecurity(六)簡訊驗證碼登入

SpringSecurity(六)簡訊驗證碼登入

由 SpringSecurity(四)認證流程 我們已經知道了Spring Security使用者名稱和密碼的登入流程。仿照使用者名稱和密碼登入編寫一個簡訊驗證碼登入

 

手機驗證碼登入流程圖

 

簡訊驗證碼

新建一個SmsCode類,裡面有三個屬性:String code(驗證碼字串)、LocalDateTime expireTime(過期時間)、String mobile(手機號)。省略簡訊驗證碼的傳送過程。大概步驟如下:

1. 使用者點擊發送驗證碼按鈕

2. 隨機生成一個code字串,新建SmsCode物件,設定驗證碼字串code、手機號mobile和過期時間expireTime

3. 將SmsCode物件存入session

 

實現手機驗證碼登入

1. 新建 SmsCodeAuthenticationToken,對應使用者名稱密碼登入的UsernamePasswordAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = 500L;
    private final Object principal;

    public SmsCodeAuthenticationToken(Object mobile) {
        super((Collection)null);
        this.principal = mobile;
        this.setAuthenticated(false);
    }

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

    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");
        } else {
            super.setAuthenticated(false);
        }
    }

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

2. 新建  SmsCodeAuthenticationFilter,對應使用者名稱密碼登入的UsernamePasswordAuthenticationFilter

public class SmsCodeAuthenticationFilter  extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/authentication/mobile", "POST"));
    }

    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String mobile = this.obtainMobile(request);
            if (mobile == null) {
                mobile = "";
            }
            mobile = mobile.trim();
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(this.mobileParameter);
    }

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

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


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

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

3. 新建 SmsCodeAuthenticationProvider,對應使用者名稱密碼登入的DaoAuthenticationProvider

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private MyUserService myUserService;

    public MyUserService getMyUserService() {
        return myUserService;
    }

    public void setMyUserService(MyUserService myUserService) {
        this.myUserService = myUserService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken smsCodeAuthenticationToken = (SmsCodeAuthenticationToken)authentication;
        UserDetails user = myUserService.loadUserByMobile((String)smsCodeAuthenticationToken.getPrincipal());
        if (user == null) {
            throw new InternalAuthenticationServiceException("無法獲取使用者資訊");
        }
        SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(user, user.getAuthorities());
        result.setDetails(smsCodeAuthenticationToken.getDetails());
        return result;
    }

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

4. 修改 MyUserService

@Component
public class MyUserService implements UserDetailsService {
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        
        List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
        LoginUser loginUser = new LoginUser(s,new BCryptPasswordEncoder().encode("123456"),authorityLists);
        loginUser.setNickName("成");
        return loginUser;
    }

    public UserDetails loadUserByMobile(String mobile) throws UsernameNotFoundException {
        //  通過手機號mobile去資料庫裡查詢使用者以及使用者許可權
        List<GrantedAuthority> authorityLists = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN,ROLE_USER");
        LoginUser loginUser = new LoginUser(mobile,new BCryptPasswordEncoder().encode("123456"),authorityLists);
        loginUser.setNickName("成");
        return loginUser;
    }
}

5. 新建 SmsCodeAuthenticationSecurityConfig

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired
    private MyAuthenticationFailHandler myAuthenticationFailHandler;

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyUserService MyUserService;


    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setMyUserService(MyUserService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
            .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

6.  新建SmsCodeFilter過濾器,用於驗證簡訊驗證碼是否正確。使用過濾器來驗證簡訊驗證碼的好處是,可以任意設定哪些地址需要簡訊驗證碼驗證之後才能訪問,不僅僅只使用於登入。

@Component
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private Set<String> urls = new HashSet<>();

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        // 這裡配置需要攔截的地址 ......
        urls.add("/authentication/mobile"); //

    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        boolean action = false;
        for (String url : urls) {
            if (antPathMatcher.match(url, httpServletRequest.getRequestURI())) {
                action = true;
                break;
            }
        }
        if (action) {
            try {
                validate(httpServletRequest);
            } catch (SmsCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validate(HttpServletRequest request) {
        SmsCode smsCode = (SmsCode)request.getSession().getAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
        String smsCodeRequest = request.getParameter("smsCode");
        if (smsCodeRequest == null || smsCodeRequest.isEmpty()) {
            throw new SmsCodeException("簡訊驗證碼不能為空");
        }
        if (smsCode == null) {
            throw new SmsCodeException("簡訊驗證碼不存在");
        }
        if (smsCode.isExpired()) {
            request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
            throw new SmsCodeException("簡訊驗證碼已過期");
        }
        if(!smsCodeRequest.equalsIgnoreCase(smsCode.getCode())) {
            throw new SmsCodeException("簡訊驗證碼錯誤");
        }
        if(!smsCode.getMobile().equals(request.getParameter("mobile"))) {
            throw new SmsCodeException("輸入的手機號與傳送簡訊驗證碼的手機號不一致");
        }
        request.getSession().removeAttribute(ValidateCodeProcessor.SESSION_KEY_PREFIX + "SMS");
    }
}

7.  SpringSecurityConfig配置檔案,將SmsCodeAuthenticationSecurityConfig和SmsCodeFilter注入

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyUserService myUserService;

    @Autowired
    private MyAuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private MyAuthenticationFailHandler authenticationFailHandler;

    @Autowired
    private ImageCodeFilter imageCodeFilter;

    @Autowired
    private SmsCodeFilter smsCodeFilter;

    // 注入簡訊登入的相關配置
    @Autowired
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(imageCodeFilter, UsernamePasswordAuthenticationFilter.class) // 將ImageCodeFilter過濾器設定在UsernamePasswordAuthenticationFilter之前
                .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .authorizeRequests()
                .antMatchers("/authentication/*","/login/*","/code/*") // 不需要登入就可以訪問
                .permitAll()
                .antMatchers("/user/**").hasAnyRole("USER") // 需要具有ROLE_USER角色才能訪問
                .antMatchers("/admin/**").hasAnyRole("ADMIN") // 需要具有ROLE_ADMIN角色才能訪問
                .anyRequest().authenticated()
                .and()
                    .formLogin()
                    .loginPage("/authentication/login") // 訪問需要登入才能訪問的頁面,如果未登入,會跳轉到該地址來
                    .loginProcessingUrl("/authentication/form")
                    .successHandler(authenticationSuccessHandler)
                    .failureHandler(authenticationFailHandler)
                ;
        http.apply(smsCodeAuthenticationSecurityConfig);
    }

    // 密碼加密方式
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    // 重寫方法,自定義使用者
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//        auth.inMemoryAuthentication().withUser("lzc").password(new BCryptPasswordEncoder().encode("123456")).roles("ADMIN","USER");
//        auth.inMemoryAuthentication().withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("USER");
        auth.userDetailsService(myUserService); // 注入MyUserService,這樣SpringSecurity會呼叫裡面的loadUserByUsername(String s)
    }
}

 

程式碼地址    https://github.com/923226145/SpringSecurity/tree/master/chapter5