SpringBoot整合SpringSecurity(三)验证码登陆

序言

上一篇文章我们跟踪源码了解到了security整个的登陆流程,这一篇我们就基于这个流程来做一个自己定义的流程,并与security对接。本篇中介绍的是短信登陆,当然验证码登陆跟这个原理是一样的就不多说了。

代码请参考 https://github.com/AutismSuperman/springsecurity-example

准备页面

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
<h3>验证码表单登录</h3>
<table>
    <tr>
        <td>用户名:</td>
        <td><input type="text" autocomplete="off" name="mobile"></td>
    </tr>
    <tr>
        <td>验证码:</td>
        <td><input type="password" autocomplete="off" name="sms_code"></td>
    </tr>
    <tr>
        <td colspan="2">
            <a href="javascript:void(0);" onclick="sendSms()">获取验证码</a>
            <button type="button" onclick="loginSms()">登录</button>
        </td>
    </tr>
</table>
<h3>普通表单登录</h3>
<table>
    <tr>
        <td>用户名:</td>
        <td><input type="text" autocomplete="off" name="username"></td>
    </tr>
    <tr>
        <td>密码:</td>
        <td><input type="password" autocomplete="off" name="password"></td>
    </tr>
    <tr>
        <td colspan="2">
            <button type="button" onclick="login()">登录</button>
        </td>
    </tr>
</table>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>

    function sendSms() {
        window.open('/sms/code?mobile=' +  $("input[name=mobile]").val());
    }
    
    function loginSms() {
        var mobile = $("input[name=mobile]").val();
        var smsCode = $("input[name=sms_code]").val();
        if (mobile === "" || smsCode === "") {
            alert("用户名或密码不能为空");
            return;
        }
        $.ajax({
            type: "POST",
            url: "/sms/login",
            data: {
                "mobile": mobile,
                "smsCode": smsCode
            },
            success: function (e) {
                console.log(e);
                alert("登陆成功")
                setTimeout(function () {
                    location.href = '/hello';
                }, 500);
            },
            error: function (e,a,b) {
                console.log(a);
                console.log(b);
                console.log(e.responseText);
                alert("登陆失败zxczxczc")
            }
        });
    }


    function login() {
        var username = $("input[name=username]").val();
        var password = $("input[name=password]").val();
        if (username === "" || password === "") {
            alert("用户名或密码不能为空");
            return;
        }
        debugger
        $.ajax({
            type: "POST",
            url: "/authentication/form",
            data: {
                "username": username,
                "password": password
            },
            success: function (e) {
                console.log(e);
                alert("登陆成功")
                setTimeout(function () {
                    location.href = '/hello';
                }, 500);
            },
            error: function (e,a,b) {
                console.log(e.responseText);
                alert("登陆失败zxczxczc")
            }
        });
    }


</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
<h2>hello world from fulinlin.</h2>
<a href="/logout">退出登录</a>
</body>
</html>
@Controller
public class LoginController {

    @RequestMapping("/login")
    public String login() {
        return "login";
    }
    @RequestMapping("/hello")
    public String hello() {
        return "hello";
    }
}

发短信的接口

@RestController
@Slf4j
public class SmsController {

    @RequestMapping("/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 "你的手机号"+mobile+"验证码是"+code;
    }


}

准备测试用户

public interface IUserService {
    SysUser findByUsername(String userName);
}

实现类

@Service
public class UserServiceImpl implements IUserService {

    private static final Set<SysUser> users = new HashSet<>();


    static {
        users.add(new SysUser(1L, "fulin", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
        users.add(new SysUser(2L, "xiaohan", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
        users.add(new SysUser(3L, "longlong", "123456", Arrays.asList("ROLE_ADMIN", "ROLE_DOCKER")));
    }

    @Override
    public SysUser findByUsername(String userName) {
        return users.stream().filter(o -> StringUtils.equals(o.getUserName(), userName)).findFirst().orElse(null);
    }
}

UserDetailsService

@Service
public class UserService implements UserDetailsService {

    @Autowired
    private  IUserService iUserService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        SysUser user = iUserService.findByUsername(s);
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在");
        }
        //把角色放入认证器里
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        List<String> roles = user.getRoles();
        for (String role : roles) {
            authorities.add(new SimpleGrantedAuthority(role));
        }
        return new User(user.getUserName(), user.getPassword(), authorities);
    }

}

AuthenticationFilter

首先呢我们要有一个自定义的 AuthenticationFilter 来实现对自定义登陆的拦截,我们模仿源码去写,拿到请求中的 mobile 构建一个没有认证的 SmsCodeAuthenticationToken

/**
 * 短信登录的鉴权过滤器,模仿 UsernamePasswordAuthenticationFilter 实现
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    /**
     * form表单中手机号码的字段name
     */
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    /**
     * 是否仅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        // 短信登录的请求 post 方式的 /sms/login
        super(new AntPathRequestMatcher("/sms/login", "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);

        // Allow subclasses to set the "details" property
        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 String getMobileParameter() {
        return mobileParameter;
    }

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

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

AuthenticationToken

上面说到了构建一个没有认证的SmsCodeAuthenticationToken 那么我们就来自己写一个

/*
 *这一步的作用是为了替换原有系统的 UsernamePasswordAuthenticationToken 用来做验证
 *
 * 代码都是从UsernamePasswordAuthenticationToken 里粘贴出来的
 *
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

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

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

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



    // ~ Methods
    // 剩下的方法不用动就行了 就是从 UsernamePasswordAuthenticationToken 里粘贴出来的
    // ========================================================================================================

    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");
        }

        super.setAuthenticated(false);
    }

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

AuthenticationProvider

有了AuthenticationFilter AuthenticationToken 都有了,按照上一篇的逻辑走,肯定要有一个AuthenticationProvider 来进行验证吧。那么我们也来写一个

/**
 * 短信登陆鉴权 Provider,要求实现 AuthenticationProvider 接口
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
	//上下文中的 userDetailsService
    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal();

        checkSmsCode(mobile);

        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    private void checkSmsCode(String mobile) {
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
      
        String inputCode = request.getParameter("smsCode");
        
        //这里的验证码我们放session里,这里拿出来跟用户输入的做对比
        Map<String, Object> smsCode = (Map<String, Object>) request.getSession().getAttribute("smsCode");
        if (smsCode == null) {
            throw new BadCredentialsException("未检测到申请验证码");
        }

        String applyMobile = (String) smsCode.get("mobile");

        int code = (int) smsCode.get("code");

        if (!applyMobile.equals(mobile)) {
            throw new BadCredentialsException("申请的手机号码与登录手机号码不一致");
        }
        if (code != Integer.parseInt(inputCode)) {
            throw new BadCredentialsException("验证码错误");
        }
    }

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

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

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

SecurityConfigurerAdapter

AuthenticationFilter AuthenticationToken AuthenticationProvider 全部都有了,那么还差啥?

就差跟 security做绑定了怎么做呢? 不慌我们先定义下成功处理器和失败处理器

成功处理器

@Component
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
        log.info("登录成功");
        response.setStatus(HttpStatus.OK.value());
        ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.OK.value(), "登录成功");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(modelMap));
    }

}

失败处理器

@Component
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        log.info("登录失败!");
        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
        ModelMap modelMap = GenerateModelMap.generateMap(HttpStatus.INTERNAL_SERVER_ERROR.value(), "验证失败");
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(modelMap));
    }
}

加入到过滤链里

SecurityConfigurerAdapter 顾名思义就是 SecurityConfigurer的适配器,我们只需要吧我们刚才写的 AuthenticationFilter AuthenticationToken AuthenticationProvider 都放进来就可以与security挂上了。

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    @Autowired //我们自己定义的UserDetailsService
    private UserService userService;
    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;


    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        //设置AuthenticationManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        //设置失败成功处理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);
        //设置UserDetailsService
        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userService);
        //这里说明要把我们自己写的Provider放在过滤链的哪里
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

这样只是加入到了security的过滤链里 但是并没有生效,那么怎么配置呢?对就是还要在 WebSecurityConfigurerAdapter 里配置一下。

WebSecurityConfigurerAdapter

要想让 咱们自定义的配置生效,必须在配置中加入 http.apply(config) 才可以。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
    @Autowired
    private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
    @Autowired //注入咱们自己定义的登陆流程
    private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig;
    @Autowired
    private UserService userService;


    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(
                new PasswordEncoder() {
                    @Override
                    public String encode(CharSequence charSequence) {
                        return charSequence.toString();
                    }

                    @Override
                    public boolean matches(CharSequence charSequence, String s) {
                        return s.equals(charSequence.toString());
                    }
                });
    }


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单登陆配置
        http.formLogin()
                .failureHandler(customAuthenticationFailureHandler)
                .successHandler(customAuthenticationSuccessHandler)
                .loginPage("/login")
                .loginProcessingUrl("/authentication/form")
                .and();


        http.apply(smsCodeAuthenticationSecurityConfig)
                .and()
                .logout()
                .logoutUrl("/logout")
                .and()
                .authorizeRequests()
                // 如果有允许匿名的url,填在下面
                .antMatchers("/login", "/sms/**", "/authentication/form").permitAll()
                .anyRequest().authenticated();

        // 关闭CSRF跨域
        http.csrf().disable();
    }
}

至此我们已经完成了我们自定义的登陆流程。

本博文是基于springboot2.x 和security 5 如果有什么不对的请在下方留言。

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值