1. 程式人生 > >SSM+Shiro+Bootstrap+Jquery專案實踐之使用者登陸

SSM+Shiro+Bootstrap+Jquery專案實踐之使用者登陸

早在一年前,我就想著自己要寫一個完整的Web專案出來,然後開源,供所有的Web開發者探討當下網際網路企業流行的技術,可是由於種種原因未能付諸實踐,所以在新的一年,我要堅持下去,從現在開始,利用休息時間建立這個系統。這個專案的目的不是為了盲目追求技術,而是能快速、優雅地解決現實中的實際需求,希望廣大Web愛好者和我共同實現這個目標。

 我沒有寫前端頁面的基礎,所以參考了一個比較成熟的框架AdminLTE,裡面介面做的非常好。登陸介面很簡單,就是一個form表單。

<form action="##" method="post" onsubmit="return false" role="form"
				id="login-form">
				<div class="form-group has-feedback">
					<input type="text" class="form-control" name="username"
						placeholder="請輸入登入郵箱/手機號/使用者名稱"> <span
						class="glyphicon glyphicon-envelope form-control-feedback"></span>
				</div>
				<div class="form-group has-feedback">
					<input type="password" class="form-control" name="password"
						placeholder="請輸入密碼"> <span
						class="glyphicon glyphicon-lock form-control-feedback"></span>
				</div>
				<div class="row">
					<div class="col-xs-6">
						<div class="checkbox icheck">
							<label> <input type="checkbox" name="rememberMe">
								記住使用者
							</label>
						</div>
					</div>
					<!-- /.col -->
					<div class="col-xs-6">
						<div class="checkbox pull-right">
							<a href="#">忘記密碼</a> <span> / </span> <a
								href="${basePath}/register" class="text-center">註冊</a>
						</div>
					</div>
					<!-- /.col -->
				</div>
				<div class="row">
					<div class="col-xs-12">
						<button type="button" class="btn btn-danger btn-block btn-flat"
							onclick="javascript:login();">登 錄</button>
					</div>
				</div>
			</form>

 我沒有直接使用form的action+submit去提交這個表單,而是採用手動點選普通按鈕button去觸發login()。是的,一個
JavaScript函式來做這個提交動作,目的就是為了適應現實專案中實際需求,因為很多表單提交前的驗證要放在action之前做。所以這裡要引入一個之前寫好的login.js

function login() {
    // 獲取表單物件
    var bootstrapValidator = $('#login-form').data('bootstrapValidator');
    // 驗證表單
    bootstrapValidator.validate();
    // 是否通過了驗證
    if (!$('#login-form').data('bootstrapValidator').isValid()) {
	return;
    }
    var data = $('#login-form').serializeJSON();
    console.log(data);
    $.ajax({
	type : "POST",
	dataType : "json",
	contentType : "application/json;charset=utf-8",
	url : "/login",
	data : JSON.stringify(data),// 這裡要傳json字串
	success : function(result) {
	    if (result.status == 200) {
		var resultdata = result.data;
		if (resultdata) {
		    // 儲存臨時的cookie
		    setCookie("username", resultdata.username);
		    setCookie("rolename", resultdata.rolename);
		    // 如果點選了記住我,那麼存入localstorage
		    rememberMe($("input[name='rememberMe']").is(":checked"));
		}
		// 跳轉主頁
		window.location.href = "/";
	    } else {
		new LoginValidator({
		   code:result.status,
		   message:result.msg,
		   username:$("input[name='username']").val(),
		   password:$("input[name='password']").val()
		});
	    }
	},
	error : function() {
	    alert("異常!");
	}
    });
 
}
 
function LoginValidator(config) {
    this.code = config.code;
    this.message = config.message;
    this.userName = config.username;
    this.password = config.password;
    this.initValidator();
}
 
// 0 未授權 1 賬號問題 2 密碼錯誤 3 賬號密碼錯誤
LoginValidator.prototype.initValidator = function() {
    if (!this.code)
	return;
    if (this.code == 0) {
	this.addPasswordErrorMsg();
    } else if (this.code == 1) {
	this.addUserNameErrorStyle();
	this.addUserNameErrorMsg();
    } else if (this.code == 2) {
	this.addPasswordErrorStyle();
	this.addPasswordErrorMsg();
    } else if (this.code == 3) {
	this.addUserNameErrorStyle();
	this.addPasswordErrorStyle();
	this.addPasswordErrorMsg();
    }
    return;
}
 
LoginValidator.prototype.addUserNameErrorStyle = function() {
    this.addErrorStyle('username');
}
 
LoginValidator.prototype.addPasswordErrorStyle = function() {
    this.addErrorStyle('password');
}
 
LoginValidator.prototype.addUserNameErrorMsg = function() {
    this.addErrorMsg('username');
}
 
LoginValidator.prototype.addPasswordErrorMsg = function() {
    this.addErrorMsg('password');
}
 
LoginValidator.prototype.addErrorMsg = function(field) {
    // 清除掉之前的提示內容
    $("input[name='" + field + "']").parent().children('small').remove();
    // 更新錯誤提示資訊
    $("input[name='" + field + "']").parent().append(
	    '<small  data-bv-validator="notEmpty" data-bv-validator-for="'
		    + field + '" class="help-block">' + this.message
		    + '</small>');
}
 
LoginValidator.prototype.addErrorStyle = function(field) {
    $("input[name='" + field + "']").parent().addClass("has-error");
}
 
// 使用本地快取記住使用者名稱密碼
function rememberMe(rm_flag) {
    // remember me
    if (rm_flag) {
	localStorage.username = $("input[name='username']").val();
	localStorage.password = $("input[name='password']").val();
	localStorage.rememberMe = 1;
    }
    // delete remember msg
    else {
	localStorage.userName = null;
	localStorage.password = null;
	localStorage.rememberMe = 0;
    }
}
// 記住回填
function fillbackLoginForm() {
    if (localStorage.rememberMe && localStorage.rememberMe == "1") {
	$("input[name='username']").val(localStorage.username);
	$("input[name='password']").val(localStorage.password);
	$("input[name='rememberMe']").iCheck('check');
	$("input[name='rememberMe']").iCheck('update');
    }
}

 按鈕中click事件觸發的就是上面的login()函式。剛才有提到的表單欄位驗證功能,我這裡用到了一個非常流行、非常好用的驗證外掛:bootstrap-validator.js,在使用前,需要對它初始化驗證配置,即我們要驗證哪些欄位,驗證規則是什麼,這裡直接上程式碼。

// 初始化驗證配置
	    $("#login-form").bootstrapValidator({
		message : '請輸入使用者名稱/密碼',
		fields : {
		    username : {
			validators : {
			    notEmpty : {
				message : '登入郵箱、手機號、使用者名稱不能為空'
			    }
			}
		    },
		    password : {
			validators : {
			    notEmpty : {
				message : '密碼不能為空'
			    }
			}
		    }
		}
	    }); 

這段程式碼是直接寫在login.jsp這個頁面body中的,頁面載入完成後就會初始化它,後面考慮把它單獨拿出來。實際

中的驗證肯定不止上面的欄位值是否為空,這裡我們慢慢來,登陸錯誤的提示包含很多方面的知識,前端錯誤提示的

 動態變換我們還是用bootstrap-validator.js來做,程式碼也在上面login.js中,我封裝好了,主要看後臺。

 依賴jar:這裡貼出pom.xml,順便說一句,在以後的章節中我可能會詳細的講述如何構建一個標準的maven聚合工程

(標準的大型電商專案開發結構),我這裡就不多說了。

<!-- 集中定義依賴版本號 -->
	<properties>
<shiro.version>1.4.0</shiro.version>
	</properties>
<dependencyManagement>
		<dependencies>
<!-- shiro許可權框架 -->
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-core</artifactId>
				<version>${shiro.version}</version>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-web</artifactId>
				<version>${shiro.version}</version>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-spring</artifactId>
				<version>${shiro.version}</version>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-ehcache</artifactId>
				<version>${shiro.version}</version>
			</dependency>
		</dependencies>
	</dependencyManagement>

web模組pom.xml引入以下下依賴:

<!-- shiro許可權框架 -->
		<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-core</artifactId>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-web</artifactId>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-spring</artifactId>
			</dependency>
			<dependency>
				<groupId>org.apache.shiro</groupId>
				<artifactId>shiro-ehcache</artifactId>
			</dependency>

apache shiro是現在很流行、很棒的許可權框架,包括完整的登陸驗證、許可權管理,與spring完美整合,簡單易懂的api,

很適合快速開發。首先在web.xml新增它的攔截器,攔截所有(“/*”)的請求。

<!-- shiro的filter -->
	<filter>
		<filter-name>shiroFilter</filter-name>
		<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
		<init-param>
			<param-name>targetFilterLifecycle</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>targetBeanName</param-name>
			<param-value>shiroFilter</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>shiroFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

 然後新建一個applicationContext-shiro.xml檔案,來將shiro整合到spring中,最佩服的就是spring的這點,將Ioc做到了極致。配置檔案內容如下,簡單易懂:

<!-- 啟用shrio授權註解攔截方式 -->
	<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
		<!-- 裝配 securityManager -->
		<property name="securityManager" ref="securityManager" />
		<!-- 配置登陸頁面 -->
		<property name="loginUrl" value="/openlogin" />
		<!-- 許可權認證成功跳轉的介面 -->
		<property name="successUrl" value="/" />
		<!-- 沒有認證許可權的介面 -->
		<property name="unauthorizedUrl" value="/unauthorized" />
		<!-- 具體配置需要攔截哪些 URL, 以及訪問對應的 URL 時使用 Shiro 的什麼 Filter 進行攔截. -->
		<property name="filterChainDefinitions">
			<value>
				/css/** = anon
				/js/** = anon
				/login = anon
				/** = authc
			</value>
		</property>
	</bean>
 
	<!-- 配置快取管理器 -->
	<!-- <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
		指定 ehcache 的配置檔案
		<property name="cacheManagerConfigFile" value="classpath:ehcache-shiro.xml" />
	</bean> -->
	<!-- 配置進行授權和認證的 Realm -->
	<bean id="myRealm" class="com.taotao.shiro.ShiroDBRealm">
	</bean>
	<!-- 配置 Shiro 的 SecurityManager Bean. -->
	<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
		<property name="realm" ref="myRealm" />
	</bean>
	<!-- 配置 Bean 後置處理器: 會自動的呼叫和 Spring 整合後各個元件的生命週期方法. -->
	<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" />

 未來我可能會使用redis來管理session過期問題,目前暫時沒有關心session過期問題,預設的是設定5分鐘。好了,接下來ShiroDBRealm.java,這個類繼承了AuthorizingRealm.java,主要重寫它的使用者驗證和許可權驗證(許可權驗證後面會寫,目前不寫)。

public class ShiroDBRealm extends AuthorizingRealm {
 
	private static final String SESSION_USER_KEY = "taotao";
	
	@Autowired
	private UserService userService;
	
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();  
		return info;
	}
 
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(
			AuthenticationToken authcToken) throws AuthenticationException {
		// 把token轉換成User物件  
        TbUser userLogin = tokenToUser((UsernamePasswordToken) authcToken);  
        // 驗證使用者是否可以登入  
        TbUser ui = userService.login(userLogin); 
        if(ui == null){  
        	throw new UnknownAccountException(); // 異常處理,找不到資料
        }
        // 設定session  
        Session session = SecurityUtils.getSubject().getSession();  
        session.setAttribute(SESSION_USER_KEY, ui);
        //設定session有效時間5分鐘
        session.setTimeout(300000);
        //當前 Realm 的 name  
        String realmName = this.getName();  
        //登陸的主要資訊: 可以是一個實體類的物件, 但該實體類的物件一定是根據 token 的 username 查詢得到的.  
        Object principal = authcToken.getPrincipal();  
        return new SimpleAuthenticationInfo(principal, ui.getPassword(), realmName); 
	}
 
	private TbUser tokenToUser(UsernamePasswordToken authcToken) {  
		TbUser user = new TbUser();  
        user.setUsername(authcToken.getUsername());  
        user.setPassword(String.valueOf(authcToken.getPassword()));  
        return user;  
    } 
}

 注入的service:
包含service介面和service實現類

 UserService.java

public interface UserService {
	public TbUser login(TbUser user);
}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {
 
	@Autowired
	private TbUserMapper tbUserMapper;
 
	@Override
	public TbUser login(TbUser user) {
		TbUserExample tbUserExample = new TbUserExample();
		Criteria createCriteria = tbUserExample.createCriteria();
		if (user != null) {
			String username = user.getUsername();
			createCriteria.andUsernameEqualTo(username);
			List<TbUser> selectByExample = tbUserMapper
					.selectByExample(tbUserExample);
			if (selectByExample != null && !selectByExample.isEmpty()) {
				return selectByExample.get(0);
			}
		}
		return null;
	}
 
}

注入的Dao:程式碼沒有太大的價值,因為是使用mybatis逆向工程生成,所以就這麼說說吧。

 Control層:LoginController.java

@Controller
public class LoginController {
 
	@Autowired
	@RequestMapping("/openlogin")
	public String openloginpage() {
		// 已經登入過,直接進入主頁
		try {
			// 這裡有bug,整個伺服器重啟後,會預設進這個handler,
			// 導致securityManager為null,所以加try避免
			if (isRelogin()) {
				// 如果已經登陸,無需重新登入,進入首頁
				// 使用redirect:/是因為瀏覽器地址要改變
				return "redirect:/"; 
			}
		} catch (Exception e) {
		}
		return "login";
	}
 
	@RequestMapping(value = "/login", method = RequestMethod.POST)
	@ResponseBody
	public CommonResult login(@RequestBody TbUser user) {
		return loginUser(user);
	}
 
	private CommonResult loginUser(TbUser user) {
		return shiroLogin(user); // 呼叫shiro的登陸驗證
	}
 
	private CommonResult shiroLogin(TbUser user) {
		// 組裝token,包括客戶公司名稱、簡稱、客戶編號、使用者名稱稱;密碼
		UsernamePasswordToken token = new UsernamePasswordToken(
				user.getUsername(), user.getPassword().toCharArray(), null);
		// 記住這個登陸的資訊
		token.setRememberMe(true);
		String msg;
		// shiro登陸驗證
		try {
			SecurityUtils.getSubject().login(token);
			// 0 未授權 1 賬號問題 2 密碼錯誤 3 賬號密碼錯誤
		} catch (IncorrectCredentialsException e) {
			msg = "登入密碼錯誤. Password for account " + token.getPrincipal()
					+ " was incorrect";
			return ResultGenerator.genLoginResult0(msg, 2, null);
		} catch (ExcessiveAttemptsException e) {
			msg = "登入失敗次數過多";
			return ResultGenerator.genLoginResult0(msg, 3, null);
		} catch (LockedAccountException e) {
			msg = "帳號已被鎖定. The account for username " + token.getPrincipal()
					+ " was locked.";
			return ResultGenerator.genLoginResult0(msg, 1, null);
		} catch (DisabledAccountException e) {
			msg = "帳號已被禁用. The account for username " + token.getPrincipal()
					+ " was disabled.";
			return ResultGenerator.genLoginResult0(msg, 1, null);
		} catch (ExpiredCredentialsException e) {
			msg = "帳號已過期. the account for username " + token.getPrincipal()
					+ "  was expired.";
			return ResultGenerator.genLoginResult0(msg, 1, null);
		} catch (UnknownAccountException e) {
			msg = "帳號不存在. There is no user with username of "
					+ token.getPrincipal();
			return ResultGenerator.genLoginResult0(msg, 1, null);
		} catch (UnauthorizedException e) {
			msg = "您沒有得到相應的授權!" + e.getMessage();
			return ResultGenerator.genLoginResult0(msg, 1, null);
		} catch (AuthenticationException ex) {
			return ResultGenerator.genFailResult(ex.getMessage()); // 自定義報錯資訊
		} catch (Exception ex) {
			return ResultGenerator.genFailResult("內部錯誤,請重試");
		}
		Map<String, Object> data = new HashMap<String, Object>();
		data.put("username", user.getUsername());
		return ResultGenerator.genSuccessResult(data);
	}
 
	private boolean isRelogin() {
		Subject us = SecurityUtils.getSubject();
		if (us != null && us.isAuthenticated()) {
			return true; // 引數未改變,無需重新登入,預設為已經登入成功
		}
		return false; // 需要重新登陸
	}
}

 至此,前端提交表單,請求,響應已經全部完成。看看效果: