作者:admin,发布日期:2020-05-27
阅读:2559;评论: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原理部分,有大佬的话可以来补充补充,剩下的看代码应该都能看懂。