springboot + shiro 實現登入認證和許可權控制

gcyml發表於2019-04-23

前言

這段時間在學習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(),每訪問一次就會觸發一次。

相關文章