專案github地址:github.com/pc859107393…
實時專案同步的地址是國內的碼雲:git.oschina.net/859107393/m…
我的簡書首頁是:www.jianshu.com/users/86b79…
上一期是:[手把手教程][第二季]java 後端部落格系統文章系統——No7
時隔這麼久,再次開始更新這個系列的專案,這裡向大家說聲對不起!理想是需要麵包支撐才能走下去!
工具
- IDE為idea2017.1.5
- JDK環境為1.8
- gradle構建,版本:2.14.1
- Mysql版本為5.5.27
- Tomcat版本為7.0.52
- 流程圖繪製(xmind)
- 建模分析軟體PowerDesigner16.5
- 資料庫工具MySQLWorkBench,版本:6.3.7build
本期目標
完成Shiro認證管理和許可權管理
認證模組
談到認證,我們就得明白什麼是認證。傳統的來說,就是使用者登入後獲取到使用者資訊,然後把使用者資訊進行比對,比對結果相同則通過驗證反之則是驗證失敗。
傳統的使用者認證方式是什麼樣的呢?
a、使用者登入(userName和userPassword) -> b、從資料庫獲取使用者資訊(userBean) -> c、把userPassword加密和userBean的密碼對比 -> d、對比相同登陸成功,反之登入失敗
同樣的在這個流程中產生任何其他異常,均是登入失敗!
那麼Shiro認證又是怎麼回事呢?
a、獲得Subject -> b、構造UsernamePasswordToken -> c、呼叫Subject.login() -> d、實現AuthorizingRealm並執行認證方法 -> e、實現SimpleCredentialsMatcher並例項化該物件且執行doCredentialsMatch()方法進行密碼比對
既然上面我們完成了思路流程整理,那麼下面我們直接完成Shiro的登入功能!
@Controller
@Api(description = "使用者相關")
public class SysUserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/user/login", method = RequestMethod.POST, produces = APPLICATION_XHTML_XML_VALUE)
@ApiOperation(value = "/user/login", notes = "登入系統web介面")
public String login(HttpServletRequest request, HttpServletResponse response, ModelMap map,
@ApiParam(value = "使用者名稱不能為空,否則不允許登入"
, required = true) @RequestParam(value = "userLogin", required = false) String userLogin,
@ApiParam(value = "使用者密碼不能為空且必須為16位小寫MD5,否則不允許登入"
, required = true) @RequestParam(value = "userPass", required = false) String userPass) {
User user = null;
try {
//1.得到Subject
Subject subject = SecurityUtils.getSubject();
//2.呼叫登入方法
UsernamePasswordToken token = new UsernamePasswordToken(userLogin, userPass);
subject.login(token);//當這一程式碼執行時,就會自動跳入到AuthRealm中認證方法
user = (User) subject.getPrincipal();
} catch (Exception e) {
e.printStackTrace();
map.addAttribute("msg", e.getMessage());
return "userLogin";
}
userService.updateUserSession(userLogin, request.getRequestedSessionId());
//此處查詢出來的資料已經快取在記憶體中,所以這裡需要將記憶體中user的sessionId改變後再寫到session中
user.setUserSessionId(request.getRequestedSessionId());
request.getSession().setAttribute("userInfo", user);
return "redirect:/endSupport/index";
}
}複製程式碼
上面的程式碼簡單的來說就是構建了一個僅僅支援POST方法的連線,連線地址是:“域名:埠號/user/login”,登陸成功就返回後端主頁,登入失敗返回包含錯誤資訊的登入頁面。
當然,我們可以看到我已經接入了Shiro。在我try裡面的一段,如果找不到使用者或者說密碼比對失敗或者其他異常,我們都可以看作是登入失敗!(就算是正確的賬號和密碼,但是後端出現了異常不能處理,那麼我們也有必要阻止使用者登入系統)
當我們的程式接收到前端傳遞過來的使用者名稱和密碼後,我們構造了UsernamePasswordToken並且進入了驗證流程,那麼接著一起看看Shiro的驗證過程吧!
/**
* created by 程 2016/11/25
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
* 登入資訊和使用者驗證資訊驗證(non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken user = (UsernamePasswordToken) token;
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("開始登入====>
使用者為:" + user.getUsername());
String userLogin = user.getUsername();
User result4Find;
try {
result4Find = userService.findOneById(userLogin);
} catch (NotFoundException e) {
throw new AuthenticationException("使用者不存在!");
}
//進入密碼比對器,第一個引數是資料庫中存在的使用者,第二個引數是資料庫中儲存的加密密碼,第三個引數是realmName直接使用this.getName()就行
return new SimpleAuthenticationInfo(result4Find, result4Find.getUserPass(), this.getName());
}
}複製程式碼
當然這裡的程式碼基本上沒啥難度,畢竟就是查詢到使用者,然後把使用者的資訊和使用者密碼傳遞過去進行比對嘛,基本是毫無難度。
接著進入密碼比對器進行密碼校驗!
/**
* Created by cheng on 17/5/16.
*/
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
@Autowired
private UserService userService;
/**
* 密碼比較方法
*
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
try {
//從ShiroRealm傳遞過來的UsernamePasswordToken,UsernamePasswordToken實現AuthenticationToken
UsernamePasswordToken user = (UsernamePasswordToken) token;
String userPass = new String(user.getPassword());
userPass = userPass.toLowerCase(); //將大寫md5轉換為小寫md5
if (userPass.length() > 16 && userPass.length() == 32) { //32位小寫轉換為16位小寫
userPass = userPass.substring(8, 24).toLowerCase();
}
//取出資料庫中加密的密碼
User result4Find = (User) info.getPrincipals().asList().get(0);
String encryptPassword = EncryptUtils.encryptPassword(userPass, result4Find.getUserActivationKey());
return this.equals(encryptPassword, result4Find.getUserPass());
} catch (Exception e) {
e.printStackTrace();
}
return super.doCredentialsMatch(token, info);
}
}複製程式碼
在上面的密碼比對器中,我們可以看到doCredentialsMatch方法中我們最終返回的是一個equals方法的返回值,那麼說明我們需要把明文密碼轉換為密文和資料庫密文密碼進行比對。我這樣說可能大家一點都不信,但是我們看下原始碼
public class SimpleCredentialsMatcher extends CodecSupport implements CredentialsMatcher {
//···省略若干行程式碼
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
Object tokenCredentials = this.getCredentials(token);
Object accountCredentials = this.getCredentials(info);
return this.equals(tokenCredentials, accountCredentials);
}
}複製程式碼
在SimpleCredentialsMatcher中,doCredentialsMatch方法的返回值也是equals,所以說我們上面直接用equals也是沒有任何毛病!
在上面密碼的比對實現中,可以看到我是先把AuthenticationToken強轉為UsernamePasswordToken(關於這個,我們檢視原始碼可以知道其實UsernamePasswordToken實現AuthenticationToken)。然後獲取到使用者密碼經過加密再和資料庫密碼進行比對。
要是看過我往期專案的童鞋可能會說我前面已經寫過一個login的方法在UserServiceImpl中,所以說我們現在需要用我們以前的方法來實現使用者登入,那麼我們現在需要怎麼做呢?
利用以往程式碼進行登入改進(獨家經驗!)
首先我們前面程式碼中的登入是經過檢驗毫無毛病的正確的登入。程式碼如下:
/**
* Created by mac on 2016/12/15.
*/
@Service("userService")
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
/**
* 登入使用者Service
*
* @param userLogin 使用者名稱
* @param userPass 使用者密碼
* @return
*/
@Override
public User login(String userLogin, String userPass) throws enterInfoErrorException, NotFoundException {
if (StringUtils.isEmpty(userLogin) || StringUtils.isEmpty(userPass)) {
throw new enterInfoErrorException("使用者名稱和密碼不能為空!請檢查!");
}
User result = null;
result = findOneById(userLogin);
if (null == result) throw new NotFoundException("使用者未找到!");
try {
userPass = userPass.toLowerCase(); //將大寫md5轉換為小寫md5
if (userPass.length() > 16 && userPass.length() == 32) { //32位小寫轉換為16位小寫
userPass = userPass.substring(8, 24).toLowerCase();
}
} catch (Exception e) {
e.printStackTrace();
throw new enterInfoErrorException("密碼錯誤!");
}
String encryptPassword = EncryptUtils.encryptPassword(userPass, result.getUserActivationKey());
if (!encryptPassword.equals(result.getUserPass())) {
throw new enterInfoErrorException("使用者名稱和密碼不匹配!");
}
return result;
}
}複製程式碼
我們看上面的登入的服務程式碼把我們前面的密碼比對器這裡的都涵蓋了,所以說,我們完全可以繞過密碼比對器!
OK,思路來了,我們現在需要繞過密碼比對器,那麼我們接下來該怎麼實現呢?
a、獲得Subject -> b、構造UsernamePasswordToken -> c、呼叫Subject.login() -> d、實現AuthorizingRealm並執行認證方法 -> e、在認證方法中呼叫userService.login(); -> f、實現SimpleCredentialsMatcher並例項化該物件且執行doCredentialsMatch()方法進行密碼比對(強行返回true)
所以,我們現在完全不慌,我們現在需要重寫的方法僅僅有doCredentialsMatch和doGetAuthenticationInfo。
public class MyCredentialsMatcher extends SimpleCredentialsMatcher {
/**
* 密碼比較方法
*
* @param token
* @param info
* @return
*/
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
return true;
}
}
/**
* created by 程 2016/11/25
*/
public class ShiroRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/*
* 登入資訊和使用者驗證資訊驗證(non-Javadoc)
* @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(org.apache.shiro.authc.AuthenticationToken)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
UsernamePasswordToken user = (UsernamePasswordToken) token;
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("開始登入====>
使用者為:" + user.getUsername());
String userLogin = user.getUsername();
char[] password = user.getPassword();
User loginResult = null;
try {
loginResult = userService.login(userLogin, new String(password));
} catch (enterInfoErrorException | NotFoundException e) {
e.printStackTrace();
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("登入異常結束====>
使用者為:" + user.getUsername());
throw new AuthenticationException(e.getMessage());
}
LogPrintUtil.getInstance(ShiroRealm.class).logOutLittle("登入成功====>
使用者為:" + user.getUsername());
return new SimpleAuthenticationInfo(loginResult, user.getPassword(), this.getName());
}
}複製程式碼
當我們的程式碼重構過後我們可以看到我們的程式碼更加清晰,同時降低了程式碼的重複率更重要的是我們接入了Shiro的登入驗證!
相對於前面的登入驗證,我們接下來接入的是我們的許可權管理。
在移動端產品中,我們常見的許可權管理就是遮蔽使用者的入口,同樣的web端也是一樣的,所以這裡我們的想法也是遮蔽後端的使用者入口!
我們這裡的思路是什麼呢?
a、重寫doGetAuthorizationInfo方法 -> b、獲取使用者資料 -> c、返回使用者許可權列表
public class ShiroRealm extends AuthorizingRealm {
/*
* 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫,負責在應用程式中決定使用者的訪問控制的方法(non-Javadoc)
* @see org.apache.shiro.realm.AuthorizingRealm#doGetAuthorizationInfo(org.apache.shiro.subject.PrincipalCollection)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
//獲取當前的使用者
User result4Find = (User) pc.asList().get(0);
//構造許可權列表
SimpleAuthorizationInfo perList = new SimpleAuthorizationInfo();
try {
if (result4Find.getUserActivationKey().equals("admin"))
perList.addStringPermissions(PermissionUtil.getAdminPer());
else perList.addStringPermissions(PermissionUtil.getOtherPer());
} catch (Exception e) {
e.printStackTrace();
perList.addStringPermissions(PermissionUtil.getOtherPer());
}
return perList;
}
}複製程式碼
在上面的程式碼中,可以明顯的看到我的使用者許可權粗略的分了兩類別,一個是admin,一個是其他。所以接著我們可以看下我們具體的許可權分為什麼些。
public class PermissionUtil {
private final static String POST_CENTER = "文章中心"
, MSG_CENTER = "留言管理"
, MEDIA_CENTER = "多媒體管理"
, API_CENTER = "API系統"
, USER_CENTER = "使用者管理"
, WEXIN_CENTER = "微信管理"
, SYSTEM_CENTER = "伺服器中心";
/**
* 獲取管理員許可權
* @return 返回管理員許可權集合
*/
public static List<String> getAdminPer(){
List<String> list = new ArrayList<>();
list.add(POST_CENTER);
list.add(MSG_CENTER);
list.add(MEDIA_CENTER);
list.add(API_CENTER);
list.add(USER_CENTER);
list.add(WEXIN_CENTER);
list.add(SYSTEM_CENTER);
return list;
}
public static List<String> getOtherPer(){
List<String> list = new ArrayList<>();
list.add(POST_CENTER);
list.add(MSG_CENTER);
list.add(MEDIA_CENTER);
return list;
}
}複製程式碼
那我們這樣配置了後,我們去哪裡接入使用者許可權呢?當然是去使用者後端模組的地址列表中。在我的專案中使用者功能模組統一配置在 _menuTemplate.jsp中,所以如下:
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<div class="left-sidebar">
<!-- 選單 -->
<ul class="sidebar-nav">
<li class="sidebar-nav-heading">Core <span class="sidebar-nav-heading-info"> 核心功能</span></li>
<li class="sidebar-nav-link">
<a href="/endSupport/index" id="menu-go-index">
<i class="am-icon-home sidebar-nav-link-logo"></i> 首頁
</a>
</li>
<shiro:hasPermission name="文章中心">
<li class="sidebar-nav-link">
<a href="javascript:;" id="menu-go-post" class="sidebar-nav-sub-title">
<i class="am-icon-table sidebar-nav-link-logo"></i> 文章中心
<span class="am-icon-chevron-down am-fr am-margin-right-sm sidebar-nav-sub-ico"></span>
</a>
<ul class="sidebar-nav sidebar-nav-sub" id="menu-post-ul">
<li class="sidebar-nav-link">
<a href="<c:url value="/endSupport/editPost"/>" id="menu-edit-post">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 寫文章
</a>
</li>
<li class="sidebar-nav-link">
<a href="/endSupport/allPost" id="menu-all-post">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 文章列表
</a>
</li>
<li class="sidebar-nav-link">
<a href="/endSupport/allTrash" id="menu-all-trash">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 回收站
</a>
</li>
<li class="sidebar-nav-link">
<a href="#" id="menu-reedit-post">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 修改文章
</a>
</li>
</ul>
</li>
</shiro:hasPermission>
<shiro:hasPermission name="留言管理">
<li class="sidebar-nav-link">
<a href="calendar.html">
<i class="am-icon-calendar sidebar-nav-link-logo"></i> 留言管理
</a>
</li>
</shiro:hasPermission>
<shiro:hasPermission name="多媒體管理">
<li class="sidebar-nav-link">
<a href="form.html">
<i class="am-icon-wpforms sidebar-nav-link-logo"></i> 多媒體管理
</a>
</li>
</shiro:hasPermission>
<shiro:hasPermission name="API系統">
<li class="sidebar-nav-link">
<a href="/apiDocs">
<i class="am-icon-bar-chart sidebar-nav-link-logo"></i> API系統
</a>
</li>
</shiro:hasPermission>
<li class="sidebar-nav-heading">Advance<span class="sidebar-nav-heading-info"> 高階設定</span></li>
<shiro:hasPermission name="使用者管理">
<li class="sidebar-nav-link">
<a href="javascript:;" id="menu-go-user" class="sidebar-nav-sub-title">
<i class="am-icon-user sidebar-nav-link-logo"></i>使用者管理
<span class="am-icon-chevron-down am-fr am-margin-right-sm sidebar-nav-sub-ico"></span>
</a>
<ul class="sidebar-nav sidebar-nav-sub" id="menu-user-ul">
<li class="sidebar-nav-link">
<a href="/endSupport/allUser" id="menu-all-user">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 使用者列表
</a>
</li>
<li class="sidebar-nav-link">
<a href="/endSupport/addUser" id="menu-add-user">
<span class="am-icon-angle-right sidebar-nav-link-logo"></span> 增加使用者
</a>
</li>
</ul>
</li>
</shiro:hasPermission>
<shiro:hasPermission name="微信管理">
<li class="sidebar-nav-link">
<a href="sign-up.html">
<i class="am-icon-clone sidebar-nav-link-logo"></i> 微信管理
<span class="am-badge am-badge-secondary sidebar-nav-link-logo-ico am-round am-fr am-margin-right-sm">6</span>
</a>
</li>
</shiro:hasPermission>
<shiro:hasPermission name="伺服器中心">
<li class="sidebar-nav-link">
<a href="/serverCenter">
<i class="am-icon-key sidebar-nav-link-logo"></i> 伺服器中心
</a>
</li>
</shiro:hasPermission>
<li class="sidebar-nav-link">
<a href="404.html">
<i class="am-icon-tv sidebar-nav-link-logo"></i> 404錯誤
</a>
</li>
</ul>
</div>複製程式碼
可以在上面的選單列表中看到有類似下面的程式碼塊。
<shiro:hasPermission name="文章中心"></shiro:hasPermission>複製程式碼
在這種程式碼塊中,name就是我們後端控制的許可權名稱,同樣的這些程式碼塊中的程式碼也是根據是否存在而決定是否顯示該模組,所以簡單的許可權管理也就到此結束。
總結:
Shiro中的登入驗證和許可權管理都是需要實現AuthorizingRealm,分別對應了doGetAuthenticationInfo和doGetAuthorizationInfo這兩個方法。
密碼比對需要實現SimpleCredentialsMatcher中的doCredentialsMatch方法(當然可以直接繞過!!!)
許可權管理中,思路是控制頁面的功能模組入口的顯示與否。主要是利用了Shiro標籤!