前言
這段時間在學習springboot,在spring security和shiro中選擇了shiro,原因就是shiro學習成本比較低,可能沒有Spring Security做的功能強大,但是在實際工作時可能並不需要那麼複雜的東西,,而且粗粒度也可以根據需要來定製,所以使用小而簡單的Shiro就足夠了。本文主要參考了z77z的SpringBoot+shiro整合學習之登入認證和許可權控制 原始碼專案地址
關於Shiro
Shiro 的核心:
- Subject(主體): 用於記錄當前的操作使用者,Subject在shiro中是一個介面,介面中定義了很多認證授相關的方法,外部程式通過subject進行認證授權,而subject是通過SecurityManager安全管理器進行認證授權
- SecurityManager(安全管理器):對Subject 進行管理,他是shiro的核心SecurityManager是一個介面,繼承了Authenticator, Authorizer, SessionManager這三個介面。
- Authenticator(認證器):對使用者身份進行認證
- Authorizer(授權器):使用者通過認證後,來判斷時候擁有該許可權
- realm:獲取使用者許可權資料
- sessionManager(會話管理):shiro框架定義了一套會話管理,它不依賴web容器的session,所以shiro可以使用在非web應用上,也可以將分散式應用的會話集中在一點管理,此特性可使它實現單點登入。
- CacheManager(快取管理器):將使用者許可權資料儲存在快取,這樣可以提高效能。
學習目標
對使用者進行登入認證,若認證失敗則返回至登入介面,只有認證成功且擁有許可權才可以訪問指定連結,否則跳轉至403頁面。
新增依賴
<!-- shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
複製程式碼
新增shiro配置
配置中還新增了一個自定義的表單攔截器MyFormAuthenticationFilter,用來處理登入異常資訊並通過返回
ShiroConfig.java
/**
* @author wgc
* @date 2018/02/09
*/
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//攔截器.
Map<String,String> filterChainDefinitionMap = new LinkedHashMap<>();
// 配置不會被攔截的連結 順序判斷
filterChainDefinitionMap.put("/assets/**", "anon");
filterChainDefinitionMap.put("/css/**", "anon");
filterChainDefinitionMap.put("/js/**", "anon");
filterChainDefinitionMap.put("/img/**", "anon");
filterChainDefinitionMap.put("/layui/**", "anon");
filterChainDefinitionMap.put("/captcha/**", "anon");
filterChainDefinitionMap.put("/favicon.ico", "anon");
//配置退出 過濾器,其中的具體的退出程式碼Shiro已經替我們實現了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 過濾鏈定義,從上向下順序執行,一般將/**放在最為下邊 -->:這是一個坑呢,一不小心程式碼就不好使了;
// authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問;
// user:認證通過或者記住了登入狀態(remeberMe)則可以通過
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
// 如果不設定預設會自動尋找Web工程根目錄下的"/login.jsp"頁面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登入成功後要跳轉的連結
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授權介面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
//自定義攔截器
Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
filters.put("authc", new MyFormAuthenticationFilter());
return shiroFilterFactoryBean;
}
/**
* 身份認證realm; (這個需要自己寫,賬號密碼校驗;許可權等)
* @return myShiroRealm
*/
@Bean
public MyShiroRealm myShiroRealm(){
MyShiroRealm myShiroRealm = new MyShiroRealm();
return myShiroRealm;
}
/**
* 安全管理器
* @return securityManager
*/
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
}
複製程式碼
Realm實現
Shiro從從Realm獲取安全資料(如使用者、角色、許可權),就是說SecurityManager要驗證使用者身份,那麼它需要從Realm獲取相應的使用者進行比較以確定使用者身份是否合法;也需要從Realm得到使用者相應的角色/許可權進行驗證使用者是否能進行操作;可以把Realm看成DataSource,即安全資料來源。Realm主要有兩個方法:
- doGetAuthorizationInfo(獲取授權資訊)
- doGetAuthenticationInfo(獲取身份驗證相關資訊):
自定義Realm實現
/**
* @author wgc
* @date 2018/02/09
*/
public class MyShiroRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(MyShiroRealm.class);
@Resource
private ShiroService userInfoService;
// 許可權授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
UserInfo userInfo = (UserInfo)principals.getPrimaryPrincipal();
for(SysRole sysRole : userInfoService.findSysRoleListByUsername(userInfo.getUsername())){
authorizationInfo.addRole(sysRole.getRolename());
logger.info(sysRole.toString());
for(SysPermission sysPermission : userInfoService.findSysPermissionListByRoleId(sysRole.getId())){
logger.info(sysPermission.toString());
authorizationInfo.addStringPermission(sysPermission.getUrl());
}
};
return authorizationInfo;
}
//主要是用來進行身份認證的,也就是說驗證使用者輸入的賬號和密碼是否正確。
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
throws AuthenticationException {
//獲取使用者的輸入的賬號.
String username = (String)token.getPrincipal();
logger.info("對使用者[{}]進行登入驗證..驗證開始",username);
//通過username從資料庫中查詢 User物件,如果找到,沒找到.
//實際專案中,這裡可以根據實際情況做快取,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重複執行該方法
UserInfo userInfo = userInfoService.selectUserInfoByUsername(username);
if(userInfo == null){
// 丟擲 帳號找不到異常
throw new UnknownAccountException();
}
return new SimpleAuthenticationInfo(userInfo, userInfo.getPassword(), getName());
}
}
複製程式碼
自定義表單驗證
在這裡我們登入採用的是login頁面post表單的方式,由於shiro已經內建了基於Form表單的身份驗證過濾器FormAuthenticationFilter,如果不自定義會呼叫這個shiro預設的過濾器。因為需要返回登入失敗資訊,所以我們需要繼承自定義一個表單過濾器,重寫setFailureAttribute方法(當登入失敗時會通過呼叫該方法),把登入失敗資訊寫入到request的"shiroLoginFailure"屬性中,由前端頁面取出列印。
表單認證過濾器實現
/**
* 自定義表單認證
* @author wgc
*/
public class MyFormAuthenticationFilter extends FormAuthenticationFilter{
private static final Logger logger = LoggerFactory.getLogger(MyFormAuthenticationFilter.class);
/**
* 重寫該方法, 判斷返回登入資訊
*/
@Override
protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
String className = ae.getClass().getName();
String message;
String userName = getUsername(request);
if (UnknownAccountException.class.getName().equals(className)) {
logger.info("對使用者[{}]進行登入驗證..驗證未通過,未知賬戶", userName);
message = "賬戶不存在";
} else if (IncorrectCredentialsException.class.getName().equals(className)) {
logger.info("對使用者[{}]進行登入驗證..驗證未通過,錯誤的憑證", userName);
message = "密碼不正確";
} else if(LockedAccountException.class.getName().equals(className)) {
logger.info("對使用者[{}]進行登入驗證..驗證未通過,賬戶已鎖定", userName);
message = "賬戶已鎖定";
} else if(ExcessiveAttemptsException.class.getName().equals(className)) {
logger.info("對使用者[{}]進行登入驗證..驗證未通過,錯誤次數過多", userName);
message = "使用者名稱或密碼錯誤次數過多,請十分鐘後再試";
} else if (AuthenticationException.class.getName().equals(className)) {
//通過處理Shiro的執行時AuthenticationException就可以控制使用者登入失敗或密碼錯誤時的情景
logger.info("對使用者[{}]進行登入驗證..驗證未通過,未知錯誤", userName);
message = "使用者名稱或密碼不正確";
} else{
message = className;
}
request.setAttribute(getFailureKeyAttribute(), message);
}
}
複製程式碼
web相關
登入頁面
controller
@RequestMapping("/login")
public String loginForm() {
return "login";
}
複製程式碼
login.html
<!DOCTYPE html>
<html lang="en" class="no-js" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>登入--layui後臺管理模板</title>
<link rel="stylesheet" href="../../layui/css/layui.css" media="all" />
<link rel="stylesheet" href="../css/login.css" media="all" />
</head>
<body>
<div class="login">
<h1>layuiCMS-管理登入</h1>
<form class="layui-form" method="post">
<div class="layui-form-item">
<input class="layui-input" name="username" placeholder="使用者名稱" type="text" autocomplete="off"/>
</div>
<div class="layui-form-item">
<input class="layui-input" name="password" placeholder="密碼" type="password" autocomplete="off"/>
</div>
<button class="layui-btn login_btn" lay-submit="" lay-filter="login">登入</button>
</form>
</div>
<script type="text/javascript" src="../layui/layui.js"></script>
<script th:inline="javascript">
layui.use(['layer'], function(){
var layer = layui.layer;
var message = [[${shiroLoginFailure}]]?[[${shiroLoginFailure}]]:getUrlPara("shiroLoginFailure");
if(message) {
layer.msg(message);
}
});
function getUrlPara(name)
{
var url = document.location.toString();
var arrUrl = url.split("?"+name +"=");
var para = arrUrl[1];
if(para)
return decodeURI(para);
}
</script>
</body>
</html>
複製程式碼
主頁面
controller
@RequestMapping({"/","/index"})
public String index(Model model) {
UserInfo user = (UserInfo)SecurityUtils.getSubject().getPrincipal();
model.addAttribute("user", user);
return "index";
}
複製程式碼
index.html
<!DOCTYPE html>
<html lang="en" class="no-js" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8"/>
<title>主頁面</title>
</head>
<body >
<h3 th:text="${user.username}">user</h3>
</body>
</html>
複製程式碼
測試
任務一 啟動後,開啟locahost:8080下任意連結由於沒登入會跳轉到login頁面,登入成功後會進入index頁面,點選主頁面上的退出,則會登出登入並跳轉至登入頁面
任務二 登入之後訪問add頁面成功訪問,在shiro配置檔案中改變add的訪問許可權為
filterChainDefinitionMap.put("/add","perms[許可權刪除]");
複製程式碼
再重新啟動程式,登入後訪問,會重定向到/403頁面,由於沒有編寫403頁面,報404錯誤。 上面這些操作,會觸發許可權認證方法:MyShiroRealm.doGetAuthorizationInfo(),每訪問一次就會觸發一次。