作者:admin,发布日期:2020-05-27
阅读:2559;评论:0

Snipaste_2020-05-27_17-00-08.png

写在开头

极验行为验证基于生物的行为特征,结合人工智能技术,帮助 29 万家网站和 APP 智能区分人机,阻绝恶意程序带来的业务损失。本文中将使用Spring Boot + Spring Security方案,并在登陆过程中整合极验验证码,来防止恶意登录,保证安全。

极验官网

https://www.geetest.com/

操作步骤

创建验证,获取ID和KEY

Snipaste_2020-05-27_17-02-21.png

登录GeeTest首页,点击行为验证,按照创建一个新的验证。

按照提示完成表单,点击确认创建,随后即可得到API ID和KEY

Snipaste_2020-05-27_17-03-59.png

Snipaste_2020-05-27_17-04-41.png

建议先把下面的ID和KEY记录下来,后面会用到。

配置SDK

参考官方文档:http://docs.geetest.com/sensebot/deploy/server/java

官方SDK及DEMO:https://github.com/GeeTeam/gt3-server-maven-sdk

由于官方DEMO为jsp版本的,我们这里对其稍作修改,使其更加符合spring的设计理念。

从上方SDK地址下载代码包,提取内部文件。

需要SDK目录下的两个库文件GeetestLib.java GeetestLibResult.java

src\main\java\com\geetest\sdk

以及demo中的配置类GeetestConfig.java

src\main\java\com\geetest\demo

将这三个类放到自己包下面可以找到的地方

Snipaste_2020-05-27_17-10-29.png

复制完成后,我们需要修改库文件的包名为自己的包名,三个文件都需要修改。

Snipaste_2020-05-27_17-12-07.png

修改完成后,接着修改配置GeetestConfig.java

配置geetestId,geetestKey为刚刚获取到的ID和KEY

package cn.craftyun.studentsmanager.sdk;

/**
 * 配置文件,可合理选择properties等方式自行设计
 */
public class GeetestConfig {

    /**
     * 填入自己在极验官网申请的账号id和key
     */
    public static final String geetestId = "";

    public static final String geetestKey = "";

    /**
     * 调试开关,是否输出调试日志
     */
    public static final boolean isDebug = false;

    /**
     * 以下字段值与前端交互,基本无需改动。
     * 极验验证API服务状态Session Key
     */
    public static final String GEETEST_SERVER_STATUS_SESSION_KEY = "gt_server_status";

    /**
     * 极验二次验证表单传参字段 chllenge
     */
    public static final String GEETEST_CHALLENGE = "geetest_challenge";

    /**
     * 极验二次验证表单传参字段 validate
     */
    public static final String GEETEST_VALIDATE = "geetest_validate";

    /**
     * 极验二次验证表单传参字段 seccode
     */
    public static final String GEETEST_SECCODE = "geetest_seccode";

}

配置完成后,需要安装org.json依赖,我们通过maven来导入

<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20200518</version>
</dependency>

配置SDK初始化

GeeTest前端在加载过程中,会请求后端的接口,获取初始化信息,所以说我们需要有路由来接收前端发送过来的请求。

    @GetMapping("/start_captcha")
    @ResponseBody
    public String startCaptcha(HttpServletRequest request) {
//        一次验证
        GeetestLib gtLib = new GeetestLib(GeetestConfig.geetestId,
                GeetestConfig.geetestKey);

        //自定义参数,可选择添加
        String userId = request.getSession().getId();
        HashMap<String, String> paramMap = new HashMap<>();
        paramMap.put("user_id", userId); //网站用户id
        paramMap.put("client_type", "web"); //web:电脑上的浏览器;h5:手机上的浏览器,包括移动应用内完全内置的web_view;native:通过原生SDK植入APP应用的方式
        paramMap.put("ip_address", request.getRemoteAddr()); //传输用户请求验证时所携带的IP

        // 进行一次验证,得到结果
        GeetestLibResult result = gtLib.register(paramMap, "md5");
        //将结果状态设置到session中
        request.getSession().setAttribute(
                GeetestConfig.GEETEST_SERVER_STATUS_SESSION_KEY,
                result.getStatus());
        // 将userid设置到session中
        // 注意,此处api1接口存入session,api2会取出使用,格外注意session的存取和分布式环境下的应用场景
        request.getSession().setAttribute("userId", userId);

        return result.getData();
    }

配置Spring Security

先实现自己的配置类,接着实现AuthenticationProvider,利用其中的authentication.getDetails()来获取前端表单提交上来的验证参数,接着调用GeeTest的SDK验证参数,验证失败抛出异常,成功则验证用户名密码,进行下一步操作。Spring Security具体可以参考另一个大佬的文章:

http://blog.didispace.com/xjf-spring-security-5/

package cn.craftyun.studentsmanager.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import javax.annotation.Resource;
import java.util.HashMap;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private SecurityAuthenticationProvider authenticationProvider;
    @Resource
    private ObjectMapper objectMapper;
    @Resource
    private GeeTestAuthenticationDetailsSource detailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/", "/error", "/login", "/doLogin", "/start_captcha", "/js/**").permitAll()
                .antMatchers("/student/**", "/api/student/**").hasRole("USERADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .authenticationDetailsSource(detailsSource)
                .loginPage("/login")
                .loginProcessingUrl("/doLogin")
                .failureHandler((httpServletRequest, httpServletResponse, e) -> {
                    httpServletResponse.setStatus(200);
                    httpServletResponse.setContentType("application/json;charset=UTF-8");
                    HashMap<String, Object> map = new HashMap<>();
                    map.put("code", 400);
                    map.put("data", e.getMessage());
                    httpServletResponse.getWriter().write(objectMapper.writeValueAsString(map));
                    httpServletResponse.flushBuffer();
                })
                .and()
                .logout()
                .and()
                .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
package cn.craftyun.studentsmanager.config;

import cn.craftyun.studentsmanager.sdk.GeetestConfig;
import cn.craftyun.studentsmanager.sdk.GeetestLib;
import cn.craftyun.studentsmanager.sdk.GeetestLibResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.util.HashMap;

@Component
public class SecurityAuthenticationProvider implements AuthenticationProvider {
    private final Logger logger = LoggerFactory.getLogger(SecurityAuthenticationProvider.class);
    @Resource
    private SecurityUserDetailsService userDetailsService;
    @Resource
    private PasswordEncoder passwordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//        Student student = studentService.getOne(new QueryWrapper<Student>().eq("studentno", username));

        GeeTestWebAuthenticationDetails details = (GeeTestWebAuthenticationDetails) authentication.getDetails();
        GeetestLib gtLib = new GeetestLib(GeetestConfig.geetestId,
                GeetestConfig.geetestKey);

        int gt_server_status_code;
        String userId;
        try {
            // 从session中获取一次验证状态码和userId
            gt_server_status_code = (Integer) details.getServerStatus();
            userId = (String) details.getUserId();
        } catch (Exception e) {
            throw new BadCredentialsException("session取key发生异常");
        }

        //自定义参数,可选择添加
        HashMap<String, String> param = new HashMap<>();
        param.put("user_id", userId); //网站用户id
        param.put("client_type", "web"); //web:电脑上的浏览器;h5:手机上的浏览器,包括移动应用内完全内置的web_view;native:通过原生SDK植入APP应用的方式
        param.put("ip_address", details.getRemoteAddress()); //传输用户请求验证时所携带的IP


        if (StringUtils.isEmpty(details.getChallenge())
                || StringUtils.isEmpty(details.getValidate())
                || StringUtils.isEmpty(details.getSeccode())) {
            throw new BadCredentialsException("请进行验证");
        }

        GeetestLibResult result;
        // gt_server_status_code 为1代表一次验证register正常,走正常二次验证模式;为0代表一次验证异常,走failback模式
        if (gt_server_status_code == 1) {
            //gt-server正常,向极验服务器发起二次验证
            result = gtLib.successValidate(details.getChallenge(), details.getValidate(), details.getSeccode(), param, "md5");
        } else {
            // gt-server非正常情况,进行failback模式本地验证
            result = gtLib.failValidate(details.getChallenge(), details.getValidate(), details.getSeccode());
        }

        if (result.getStatus() == 0) {
            throw new BadCredentialsException("验证失败" + result.getMsg());
        }

        UsernamePasswordAuthenticationToken securityUserInfo = (UsernamePasswordAuthenticationToken) authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername(securityUserInfo.getName());

        String password1 = passwordEncoder.encode(userDetails.getPassword());
        if (!passwordEncoder.matches((String) securityUserInfo.getCredentials(), password1)) {
            throw new BadCredentialsException("密码错误");
        }
        return new UsernamePasswordAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
    }

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

实现完上面两个,我们需要实现WebAuthenticationDetails,来给认证过程提供额外的信息。

package cn.craftyun.studentsmanager.config;

import cn.craftyun.studentsmanager.sdk.GeetestConfig;
import org.springframework.security.web.authentication.WebAuthenticationDetails;

import javax.servlet.http.HttpServletRequest;

public class GeeTestWebAuthenticationDetails extends WebAuthenticationDetails {
    private String challenge;
    private String validate;
    private String seccode;
    private Object userId;
    private Object serverStatus;

    public GeeTestWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        challenge = request.getParameter(GeetestConfig.GEETEST_CHALLENGE);
        validate = request.getParameter(GeetestConfig.GEETEST_VALIDATE);
        seccode = request.getParameter(GeetestConfig.GEETEST_SECCODE);

        userId = request.getSession().getAttribute("userId");
        serverStatus = request.getSession().getAttribute(GeetestConfig.GEETEST_SERVER_STATUS_SESSION_KEY);
    }

    public String getChallenge() {
        return challenge;
    }

    public void setChallenge(String challenge) {
        this.challenge = challenge;
    }

    public String getValidate() {
        return validate;
    }

    public void setValidate(String validate) {
        this.validate = validate;
    }

    public String getSeccode() {
        return seccode;
    }

    public void setSeccode(String seccode) {
        this.seccode = seccode;
    }

    public Object getUserId() {
        return userId;
    }

    public void setUserId(Object userId) {
        this.userId = userId;
    }

    public Object getServerStatus() {
        return serverStatus;
    }

    public void setServerStatus(Object serverStatus) {
        this.serverStatus = serverStatus;
    }
}
package cn.craftyun.studentsmanager.config;

import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class GeeTestAuthenticationDetailsSource implements AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {
    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest request) {
        return new GeeTestWebAuthenticationDetails(request);
    }
}

配置前端登录页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form class="popup" action="/doLogin" method="post">
    <h2>嵌入式Demo,使用表单形式提交二次验证所需的验证结果值</h2>
    <br>
    <p>
        <label for="username">用户名:</label>
        <input class="inp" id="username" type="text" name="username" value="teststu">
    </p>
    <br>
    <p>
        <label for="password">密&nbsp;&nbsp;&nbsp;&nbsp;码:</label>
        <input class="inp" id="password" type="password" name="password" value="teststu">
    </p>

    <div id="embed-captcha"></div>
    <input class="btn" id="embed-submit" type="submit" value="提交">
</form>

<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="/js/gt.js"></script>
<script>
    const handlerEmbed = function (captchaObj) {
        $("#embed-submit").click(function (e) {
            const validate = captchaObj.getValidate();
            if (!validate) {
                alert("请验证");
                e.preventDefault();
            }
        });
        // 将验证码加到id为captcha的元素里,同时会有三个input的值:geetest_challenge, geetest_validate, geetest_seccode
        captchaObj.appendTo("#embed-captcha");
    };
    $.ajax({
        // 获取id,challenge,success(是否启用failback)
        url: "/start_captcha?t=" + (new Date()).getTime(), // 加随机数防止缓存
        type: "get",
        dataType: "json",
        success: function (data) {
            console.log(data);
            // 使用initGeetest接口
            // 参数1:配置参数
            // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件
            initGeetest({
                gt: data.gt,
                challenge: data.challenge,
                new_captcha: data.new_captcha,
                product: "embed", // 产品形式,包括:float,embed,popup。注意只对PC版验证码有效
                offline: !data.success // 表示用户后台检测极验服务器是否宕机,一般不需要关注
            }, handlerEmbed);
        }
    });
</script>
</body>
</html>

增加登录页面路由,渲染登录页面。

@GetMapping("/login")
public String login() {
    return "posttest";
}

至此就全部配置完成,可以愉快的玩耍了,具体效果可以参考文章开头图片。

结尾总结

少了spring security原理部分,有大佬的话可以来补充补充,剩下的看代码应该都能看懂。