03 身份验证:用户登录那些事
安全637 字
提交凭证
场景:用户通过PC或者APP进行登录时,需要保护用户凭证避免被泄露。
说明:用户凭证必须经过加密且以POST方式提交,建议用HTTPS协议来加密通道、认证服务器。
HTML示例:用户登录功能。
<html>
<form action="LoginServlet" method="post" onSubmit="return validate(this)">
用户名:<input type="text" name="name">&l;tbr>
密 码:<input type="password" name="password"><br>
<input type="submit" value="登录">
<input type="reset" value="重置">
</form>
</html>
■ 错误提示+异常处理
场景:用户通过PC或者APP进行登录时,登录失败提示信息避免泄露过多信息。同时能够防止撞库等发生。
说明:安全地处理失败的身份验证,如使用“用户名或密码错误”来提示失败,防止泄露过多信息。登录入口应具有防止暴力或撞库猜解(利用已泄露的密码字典进行批量登录尝试)的措施,超过1次验证失败自动启用图灵测试,超过多次验证失败自动启用账户锁定机制限制其访问。
Java示例:用户登录失败提示,异常登录锁定等。
public class Login extends HttpServlet {
private static final long serialVersionUID = 1L;
public boolean localdateLtDate(String date) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
Date dates = sdf.parse(date);
Date now = sdf.parse(sdf.format(new Date()));
if (now.getTime() - dates.getTime() > 24*60*60*1000) {
return true;
} else {
return false;
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
String username = request.getParameter("username").trim();
String password = request.getParameter("password").trim();
if (username==null || "".equals(username)) {
response.sendRedirect("login.html");
}
if (password==null || "".equals(password)) {
response.sendRedirect("login.html");
}
String lockFlag = "";
String failureNum = "";
String loginDate = "";
String nowDate = "";
if (service.checkLoginRecord(userName)) {
ResultSet rs = service.getLatestLoginRecord(userName);
if (rs != null && rs.next()) {
lockFlag = rs.getString("LOCK_FLAG");
failureNum = rs.getString("FAILURE_NUM");
loginDate = rs.getString("LOGIN_DATE");
}
if ("1".equals(lockFlag)) {
// lock time long than 1 day to unlock.
if (service.localdateLtDate(loginDate)) {
service.deleteLoginRecord(userName);
lockFlag = "";
failureNum = "";
loginDate = "";
}
}
}
User user = new User();
user.setName(username);
user.setPassword(password);
List<String> info = new ArrayList<String>();
UserPassPort userPassPort = new UserPassPort();
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd HH:mm:ss");
String nowDate = sdf.format(new Date());
try {
if(UserPassPort.findValidUser(user)){
service.insertLoginRecord(userName, "0", "0", nowDate);
request.getSession().setAttribute("LoginFlag", "1");
info.add("登录成功!");
request.setAttribute("info", info);
request.getSession().setMaxInactiveInterval(30*60);
request.getRequestDispatcher("index.jsp").forward(request, response);
} else {
if ("".equals(failureNum)) {
service.insertLoginRecord(userName, "0", "1", nowDate);
info.add("用户名或密码错误!");
} else {
if ("1".equals(lockFlag)) {
response.sendRedirect("login.html");
info.add("用户名或密码错误!");
request.setAttribute("info", info);
response.sendRedirect("login.html");
}
failt = Integer.parseInt(failureNum);
if (failt < 5) {
service.updateLoginRecord(userName, "0", String.valueOf((failt+1)), nowDate);
info.add("用户名或密码错误!");
request.setAttribute("info", info);
response.sendRedirect("login.html");
} else {
service.insertLoginRecord(userName, "1", "1", nowDate);
info.add("超过最大登录失败次数,已被锁定!");
request.setAttribute("info", info);
response.sendRedirect("login.html");
}
}
}
} catch (Exception e) {
response.sendRedirect("login.html");
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}
二次验证
场景:重要操作需要二次校验防止跨站请求伪造攻击。
说明:在执行关键操作(如账户密码修改、资料更新、交易支付等)时,先启动图灵测试,再对用户身份进行二次验证。交易支付过程还应该形成完整的证据链,待交易数据应经过发起方数字签名。
Java示例:验证码的生成和校验实现。
A.验证码生成和确认:
public class drawCode extends HttpServlet {
public drawCode() {
super();
}
public void destroy() {
super.destroy();
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setHeader("Pragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
int width = 60, height = 20;
BufferedImage image = new BufferedImage(width, height,BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
g.setColor(new Color(200, 200, 200));
g.fillRect(0, 0, width, height);
Random rnd = new Random();
int randNum = rnd.nextInt(8999) + 1000;
String randStr = String.valueOf(randNum);
request.getSession().setAttribute("randStr", randStr);
g.setColor(Color.black);
g.setFont(new Font("", Font.PLAIN, 20));
g.drawString(randStr, 10, 17);
for (int i = 0; i < 100; i++){
int x = rnd.nextInt(width);
int y = rnd.nextInt(height);
g.drawOval(x, y, 1, 1);
}
ImageIO.write(image, "JPEG", response.getOutputStream());
}
public void init() throws ServletException {
// Put your code here
}
}
验证码校验ValidateServlet.java文件:
public class ValidateServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
String code = request.getParameter("code");
String randStr = request.getSession().getAttribute("randStr").toString();
System.out.println(randStr);
if (!code.equals(randStr)) {
response.sendRedirect("/example/login.jsp?codeErro=yes");
return ;
} else {
response.sendRedirect("/example/login.jsp?codeErro=no");
return ;
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
B.登录页面JSP文件:
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<body>
<script type="text/javascript">
function refresh() {
url = "/example/servlet/drawCode?" + parseInt(100*Math.random());
document.getElementById("imgValidate").src = url;
}
</script>
<br>
<form name="loginForm" action="/example/servlet/ValidateServlet">
旧密码:<input type="password" name="oldpasswd" style="height:25px"/><br/><br/>
新密码:<input type="password" name="newpasswd" style="height:25px" /><br/><br/>
确认新密码:<input type="password" name="confirmpasswd" style="height:25px" /><br/><br/>
验证码:
<input type="text" name="code" size="10" style="height:25px;vertical-align:middle">
<img id="imgValidate" src="/example/servlet/drawCode" style="vertical-align:middle">
<img src="/example/image/refresh.jpg" width="25" onclick="refresh()" style="vertical-align:middle"><br/><br/>
<input type="submit" value="确定修改">
</form>
<c:set var="codeErro" value="${param.codeErro}"/>
<c:if test="${codeErro=='yes'}"><p><span style="color:red;">验证码错误</span></p></c:if>
<c:if test="${codeErro=='no'}"><p><span style="color:green;">验证码正确</span></p></c:if>
</body>
</html>
备注:http://cighao.com/2015/08/21/validateCode-of-java/
PHP示例:验证码的生成和校验实现。
<?php
header('Content-Type: text/html; charset=utf-8');
session_start();
require dirname(__FILE__).'/includes/global.func.php';
if ($_GET['action'] == 'verification') {
if (!($_POST['code'] == $_SESSION['code'])) {
_alert_back('验证码不正确!');
}else{
_alert_back('验证码通过!');
}
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>verification code</title>
<link rel="stylesheet" type="text/css" href="style/basic.css" />
<script type="text/javascript" src="js/codeimg.js"></script>
</head>
<body>
<div id="testcode">
<form method="post" name="verification" action="verification-code.php?action=verification">
验证码:<input type="text" name="code" class="code" />
<img src="codeimg.php" id="codeimg" />
<input type="submit" class="submit" value="验证" />
</form>
</div>
</body>
</html>
■ 多因子认证
场景:在类似提交交易订单等环节需要防止跨站请求伪造攻击。
说明:高度敏感或核心的业务系统,建议使用多因子身份验证机制,如短信验证码、软硬件Token等。
Java示例:以使用软件Token为例进行说明服务器和客户端交互实现。
A.首先在服务器端Servlet中添加如下代码:
package com.example.util;
import java.util.ArrayList;
import javax.servlet.http.HttpSession;
public class Token {
private static final String TOKEN_LIST = "tokenList";
public static final String TOKEN_STRING = "token";
private static ArrayList getTokenList(HttpSession session) {
Object obj = session.getAttribute(TOKEN_LIST);
if (obj != null) {
return (ArrayList) obj;
} else {
ArrayList tokenList = new ArrayList();
session.setAttribute(TOKEN_LIST, tokenList);
return tokenList;
}
}
public static String getTokenString(HttpSession session) {
String tokenStr = (new Long(System.currentTimeMillis()).toString());
ArrayList tokenList = getTokenList(session);
tokenList.add(tokenStr);
session.setAttribute(TOKEN_LIST, tokenList);
return tokenStr;
}
public static boolean isTokenStringValid(String tokenStr, HttpSession session) {
boolean valid = false;
if (session != null) {
ArrayList tokenList = getTokenList(session);
if (tokenList.contains(tokenStr)) {
valid = true;
tokenList.remove(tokenStr);
}
}
return valid;
}
}
if (Token.isTokenStringValid(request.getParameter(Token.TOKEN_STRING_NAME), request.getSession())) {
//To Do Here.
}
B. 然后在JSP页面增加以下代码:
<%@ page import="com.example.util.Token" %>
<form>
<input type="hidden" name="<%=Token.TOKEN_STRING %>" value="<%=Token.getTokenString(session) %>">
</form>
PHP示例:以使用软件Token为例进行说明服务器和客户端交互实现。
<?php
session_start();
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = base64_encode(openssl_random_pseudo_bytes(32));
}
if (isset($_POST['csrf_token']) && $_POST['csrf_token'] === $_SESSION['csrf_token']) {
exit("POST data is valid.");
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>PHP CSRF Protection</title>
<script>
window.csrf = { csrf_token: '<?php echo $_SESSION['csrf_token']; ?>' };
$.ajaxSetup({
data: window.csrf
});
$(document).ready(function() {
$.post('/awesome/ajax/url', { foo: 'bar' }, function(data) {
console.log(data);
});
});
</script>
</head>
<body>
<form action="index.php" method="post" accept-charset="utf-8">
<input type="text" name="foo" />
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>" />
<input type="submit" value="Submit" />
</form>
</body>
</html>