現在的網站基本上都有保存密碼並自動登錄的功能, 那麼密碼到底保存在哪裡呢? 你會發現, 同一個網站, 如果換一台電腦或者換一個浏覽器那就需要重新輸入用戶名和密碼, 從這裡可以看出, 密碼是保存在浏覽器的. 今天就來分析一下博客園的登錄並自己寫一個demo. 密碼是保存在浏覽器的cookie中的, 那什麼叫cookie呢? w3cshool中有這樣的定義: cookie 是存儲於訪問者的計算機中的變量。每當同一台計算機通過浏覽器請求某個頁面時,就會發送這個 cookie。是的, 也就是說, 只要浏覽器訪問服務器並且cookie是存在的(當然, 路徑有效才行 ), 浏覽器就會攜帶cookie到服務器. 這是一種自發的行為, 並不需要設置. 就好像你每天出門一定會帶手機一樣, 並不需要別人提醒你.
好了, 進入正題. 看一下博客園是怎麼保存密碼的. 假如我沒有登錄過博客園, 打開到博客園的登錄界面, 然後再浏覽器中查看一下關於博客園的cookie, 可以看的這樣的:

有3個cookie,分別是這樣的3個:
並沒有關於用戶名密碼的cookie, 嘿嘿, 因為我此時根本沒有登錄. 好了, 現在登錄

開始我並沒有勾選下次自動登錄的選項, 登錄之後重定向到首頁. 在開發者工具的network中看一下

可以看到, 多了一個.CNBlogsCookie的請求cookie, 咦咦咦, 剛才沒有的啊, 現在就有了, 於是我猜測這就是用戶名和密碼的cookie, 這只是暫時的一個猜測. 在內容設置中看一下關於這個cookie更詳細的信息

剛才總共3個, 現在總共5個, 比剛才多了2個, 看一下多的2個

另外一個是這樣的:

可以看到, 2個cookie都是關閉浏覽器時過期, 現在關閉浏覽器? 不不不, 先看一下哪個保存著登錄信息, 先把SERVERID這個cookie刪除, 看一下是否還保持著登錄狀態. 試一下你會發現此時仍保持登錄狀態, 但是如果把.CNBlogsCookie這個cookie刪掉, 你會發現'掉線'了. 由此可以說明登錄信息保存在.CNBlogsCookie這個cookie中, 啊嘞, 我為什麼要說登錄信息呢? 剛才不是說的用戶名密碼麼? 登錄信息不就是指的用戶名和密碼嗎? 我覺得不一定要把密碼存到cookie中, 可以把一串和密碼相關的字符串存到cookie中, 在進行cookie自動登錄時, 在數據庫中查詢用戶名和這個字符串, 在表單提交時, 查詢用戶名和密碼, 個人覺得這樣更安全. 如果是把用戶名和密碼保存在了cookie中, 那麼就算銷毀session退出登錄, 下次登錄時, 在登錄頁面浏覽器應該自動填寫用戶名和密碼才對(畢竟cookie裡面有啊). 就博客園來說, 並不是這樣的, 可能你會反駁: "我明明退出登錄下次再登錄時用戶名和密碼就在那兒啊.", 其實這並不是cookie的功能, 而是浏覽器自己幫你做的, 現在的浏覽器一般都有個
的功能, 好吧, 這個我想這也算個cookie, 不過此cookie非彼cookie, 呃, 有點混亂, 我也不確定這個功能算不算cookie. 總之, 關閉這個功能, 然會退出登錄再打開博客園你會發現登錄信息是沒有的, 所以通過分析我覺得cookie中不一定保存了密碼.
那麼這個cookie是服務器什麼時候傳給浏覽器的, 上面看到的是request cookie, 那麼服務器是什麼時候response的, 登錄是ajax請求登錄的, 你可以用浏覽器打斷點看一下, 可以發現.CNBlogsCookie是登錄請求時response回來的, 如下圖

好了, 現在也知道登錄信息保存在哪裡了, 現在關閉浏覽器再打開, 你會發現此時登錄狀態已經沒有了, 因為保存登錄信息的cookie已經消失了, 關閉浏覽器即代表結束一次會話(銷毀session). 嗯, 到這裡我們基本知道博客園是怎麼保存密碼的(可能並沒有把密碼保存在cookie中, 可能只是保存了一個與密碼相關的字符串, 也可能保存的是經過加密的密碼), 只是沒有將cookie保存在客戶端硬盤中而已(沒有勾選下次自動登錄), 那麼是怎麼實現自動登錄呢? 想想cookie的工作原理: 浏覽器每次請求服務器, 如果存在cookie, 並且域和路徑符合要求, 都會將cookie攜帶至服務器.(域和路徑看上面的圖) 所以只要訪問該網站, 服務器判斷cookie中的值, 然後判斷用戶是否可以改為登錄狀態就行了.
所以登錄頁面勾選下次自動登錄就是表示將cookie存到硬盤中, 大概清楚了博客園保存密碼並自動登錄的方式: 首次登錄時將登錄信息保存到cookie中, 下次登錄時浏覽器攜帶包含登錄信息的cookie到服務器校驗從而顯示自動登錄
自己寫一個保存密碼並自動登錄的demo, 兩個頁面loginUI.jsp和successUI.jsp, loginUI.jsp用於登錄, successUI.jsp只有成功登錄之後才能訪問
loginUI.jsp:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<base href="<%=basePath%>">
<title>cookie login</title>
<script type="text/javascript" src="js/jquery-1.8.3.min.js"></script>
<script type="text/javascript">
function checkCookie(){
//把用戶名取出來
var username = getCookie("loginInfo");
$(":text").val(username);
}
function getCookie(c_name){
if(document.cookie.length > 0){
var coo = document.cookie;
var start = coo.indexOf(c_name+"=")+c_name.length+1;
var end = coo.indexOf(";", start);
if(end == -1)
end = coo.length;
var cookieValue = decodeURIComponent(coo.substring(start, end));
return cookieValue.split(",")[0];
}
}
</script>
</head>
<body onload="checkCookie()">
<form action="cookie_login.action" method="post">
用戶名<input type="text" name="user.username"><br>
密碼<input type="password" name="user.password"><br>
<input type="submit" value="登錄">
</form>
</body>
</html>
View Code
這裡也涉及到了用js操作cookie
successUI.jsp

<body>
登陸後才能看見我
</body>
View Code
後台是struts2, 為了方便, 沒有寫service, 直接寫的dao查詢數據庫
struts.xml

<package name="cookie-action" namespace="/" extends="struts-default">
<action name="cookie_*" class="top.bwcx.cookie.action.LoginAction" method="{1}">
<result name="loginUI">/WEB-INF/jsp/cookie/loginUI.jsp</result>
<result name="successUI">/WEB-INF/jsp/cookie/successUI.jsp</result>
<result name="success" type="redirectAction">
<param name="actionName">cookie_successUI</param>
</result>
</action>
</package>
View Code
LoginAction.java

import java.net.URLEncoder;
import java.util.UUID;
import javax.servlet.http.Cookie;
import org.apache.struts2.ServletActionContext;
import top.bwcx.cookie.dao.UserDao;
import top.bwcx.cookie.entity.User;
import com.opensymphony.xwork2.ActionSupport;
@SuppressWarnings("serial")
public class LoginAction extends ActionSupport {
private User user;
private UserDao userDao = new UserDao();
public String loginUI(){
return "loginUI";
}
public String successUI(){
return "successUI";
}
//用於表單登錄
public String login(){
try {
if(user != null){//表單登錄
//生成一串字符串與密碼關聯保存到cookie中, 並存到數據庫
user = userDao.findByUsernameAndPassword(user);
if(user != null){
//登錄成功, 將登錄信息保存在session中
ServletActionContext.getRequest().getSession().setAttribute("loginUser", user);
//生成一串uuid並保存到hobby中, 這裡應該是注冊就生成的, 可是這裡沒有注冊, 就第一次登錄的時候生成
//這個條件不用管, 根據你的實際情況判斷就行了
if(user.getHobby() == null || user.getHobby().equals("") || user.getHobby().contains("^")){
String hob = UUID.randomUUID().toString().replace("-", "");
user.setHobby(hob);
//更新
userDao.updateUser(user);
}
//將用戶名和uuid添加到cookie
String s = user.getUsername()+","+user.getHobby();
//編碼, 因為cookie不能包含逗號、分號或空格,也不能以 $ 字符開頭
String loginInfo = URLEncoder.encode(s, "UTF-8");
Cookie loginCookie = new Cookie("loginInfo", loginInfo);
loginCookie.setMaxAge(60*60);
loginCookie.setPath("/");
ServletActionContext.getResponse().addCookie(loginCookie);
}
}else
return "loginUI";
} catch (Exception e) {
e.printStackTrace();
}
return "success";
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
View Code
我的表示本來就存在的表, 裡面有id, 用戶名, 密碼和愛好幾個字段, 我把愛好改成了由UUID生成的一串字符串, cookie自動登錄的時候就判斷這個字符串和用戶名. 我做了一個登陸的過濾器, cookie的自動登錄就放在裡面了
setMaxAge(int expiry)表示cookie的過期時間, 負數表示關閉浏覽器cookie過期, 0表示立刻刪除cookie, 正數表示在該時間後過期, 單位秒.
LoginFilter.java

/**
* 服務器會為每個訪問它的的浏覽器創建一個session, 那麼服務器怎麼識別每個浏覽器呢? 這時就需要用到JSESSIONID, 所以說JSESSIONID的作用是:
* JSESSIONID作為服務器識別每個浏覽器的唯一標識
* 在浏覽器端將JSESSIONID刪除, 服務器端的session會失效
*/
public class LoginFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
String uri = req.getRequestURI();
// System.out.println(uri);
if(uri.contains("cookie_login"))
chain.doFilter(req, resp);
else{
//查看session
//登錄後在浏覽器端將JSESSIONID刪除, 看一下這裡的session
Object user = req.getSession().getAttribute("loginUser");
if(user == null){
//如果session為空, 就嘗試cookie登錄
boolean isLogin = CookieUtil.loginByCookies(req, resp);
if(isLogin)
chain.doFilter(request, response); //cookie登錄成功
else
resp.sendRedirect(req.getContextPath()+"/cookie_loginUI.action");
}
else
chain.doFilter(req, resp);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}
View Code
用於cookie登錄的代碼
CookieUtil.java

public class CookieUtil {
private static UserDao userDao = new UserDao();
public static boolean loginByCookies(HttpServletRequest request, HttpServletResponse response) {
try {
Cookie[] cookies = request.getCookies();
if(cookies != null && cookies.length > 0){
for (Cookie cookie : cookies) {
String cookieName = cookie.getName();
if(cookieName.equals("loginInfo")){
String cookieValue = URLDecoder.decode(cookie.getValue(), "UTF-8");
// System.out.println(cookieValue);
User user = userDao.findByUsernameAndHobby(cookieValue.split(",")[0], cookieValue.split(",")[1]);
if(user != null){
//登錄成功, 將登錄信息保存在session中
request.getSession().setAttribute("loginUser", user);
return true;
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}
View Code
以上是這個案例的主要代碼, 下面把一些其他用到的代碼也貼到下面
UserDao.java

public class UserDao {
public User findByUsernameAndPassword(User user) {
try {
String sql = "select * from user where username = ? and password = ?";
return DbTools.getQueryRunner().query(sql, new BeanHandler<User>(User.class), user.getUsername(), user.getPassword());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public void updateUser(User user) {
try {
String sql = "update user set hobby = ? where id = ?";
DbTools.getQueryRunner().update(sql, user.getHobby(), user.getId());
} catch (SQLException e) {
e.printStackTrace();
}
}
public User findByUsernameAndHobby(String username, String hobby) {
try {
String sql = "select * from user where username = ? and hobby = ?";
return DbTools.getQueryRunner().query(sql, new BeanHandler<User>(User.class), username, hobby);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
View Code
操作數據庫用的是c3p0和dbutils, 這方面的內容可以在我以前的隨筆中找到, 裡面獲取QueryRunner對象時這樣得到的

public class DbTools {
private static QueryRunner qr;
static{
ComboPooledDataSource dataSource = new ComboPooledDataSource();
qr = new QueryRunner(dataSource);
}
public static QueryRunner getQueryRunner(){
return qr;
}
}
View Code
User.java

public class User {
private Integer id;
private String username;
private String password;
private String hobby;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getHobby() {
return hobby;
}
public void setHobby(String hobby) {
this.hobby = hobby;
}
}
View Code
當然不要忘了web.xml的配置

<!-- 登錄過濾器放在struts過濾器的前面 -->
<filter>
<filter-name>loginFilter</filter-name>
<filter-class>top.bwcx.cookie.filter.LoginFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>loginFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
View Code
這就是一個保存密碼並且能夠自動登錄的案例了. 由於本人水平有限, 如有錯誤不當之處請各位不吝指出.