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

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

登录GeeTest首页,点击行为验证,按照创建一个新的验证。
按照提示完成表单,点击确认创建,随后即可得到API ID和KEY


建议先把下面的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
将这三个类放到自己包下面可以找到的地方

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

修改完成后,接着修改配置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">密 码:</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原理部分,有大佬的话可以来补充补充,剩下的看代码应该都能看懂。