早在一年前,我就想着自己要寫一個完整的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聚合工程
(標準的大型電商項目開發結構),我這裏就不多說了。
parent:
<!-- 集中定義依賴版本號 -->
<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; // 需要重新登陸
}
}
至此,前端提交表單,請求,響應已經全部完成。看看效果: