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>
          
        
maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见