javascript
学习Spring Boot:(十三)配置 Shiro 权限认证
經過前面學習 Apache Shiro ,現在結合 Spring Boot 使用在項目里,進行相關配置。
正文
添加依賴
在 pom.xml 文件中添加 shiro-spring 的依賴:
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>${shiro.version}</version></dependency>RBAC
RBAC 1 是基于角色的訪問控制,權限與角色關聯,給用戶配置相關角色,來獲取權限信息。
Shiro 配置
新建一個新的 Shiro 配置類 ShiroConfig:
package com.wuwii.common.config;import com.wuwii.module.sys.autho2.OAuth2Realm; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.mgt.SessionManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap; import java.util.Map;/*** Apache Shiro 核心通過 Filter 來實現,就好像SpringMvc 通過DispachServlet 來主控制一樣。* 既然是使用 Filter 一般也就能猜到,是通過URL規則來進行過濾和權限校驗,* 所以我們需要定義一系列關于URL的規則和訪問權限。** @author KronChan* @version 1.0* @since <pre>2018/2/9 10:35</pre>*/ @Configuration public class ShiroConfig {@Beanpublic SessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionValidationSchedulerEnabled(true);sessionManager.setSessionIdCookieEnabled(true);return sessionManager;}/*** 注冊安全管理,必須設置 SecurityManager** @param oAuth2Realm 認證* @param sessionManager 緩存* @return*/@Beanpublic SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 可以添加多個認證,執行順序是有影響的securityManager.setRealm(oAuth2Realm);securityManager.setSessionManager(sessionManager);return securityManager;}@Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();shiroFilter.setSecurityManager(securityManager);//自定義一個oauth2攔截器,不設置就是使用默認的攔截器/*Map<String, Filter> filters = new HashMap<>();filters.put("oauth2", new OAuth2Filter());shiroFilter.setFilters(filters);*///攔截器//<!-- 過濾鏈定義,從上向下順序執行,一般將 /**放在最為下邊 -->//<!-- authc:所有url都必須認證通過才可以訪問; anon:所有url都都可以匿名訪問-->Map<String, String> filterMap = new LinkedHashMap<>();//配置退出過濾器,其中的具體的退出代碼Shiro已經替我們實現了filterMap.put("/sys/logout", "logout");// 驗證碼filterMap.put("/sys/captcha.jpg", "anon");// 設置系統模塊下訪問需要權限filterMap.put("/sys/login", "anon");// 自定義的攔截//filterMap.put("/sys/**", "oauth2");filterMap.put("/sys/**", "authc");// 登陸的 urlshiroFilter.setLoginUrl("/sys/login");// 登陸成功跳轉的 urlshiroFilter.setSuccessUrl("/");// 未授權的 url// shiroFilter.setUnauthorizedUrl("/login.html");//未授權界面;shiroFilter.setUnauthorizedUrl("/403");shiroFilter.setFilterChainDefinitionMap(filterMap);return shiroFilter;}/*** Shiro生命周期處理器* @return*/@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*** 開啟Shiro的注解,* (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類,* 并在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能** @return*/@Beanpublic DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();proxyCreator.setProxyTargetClass(true);return proxyCreator;}/*** 開啟 shiro aop注解支持.** @param securityManager* @return*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();advisor.setSecurityManager(securityManager);return advisor;}/*** 憑證匹配器* (由于我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了* 所以我們需要修改下doGetAuthenticationInfo中的代碼;* )* <b>需要在身份認證中添加 realm.setCredentialsMatcher(hashedCredentialsMatcher())</b>* @return*//*@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher() {HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法;hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當于 md5(md5(""));return hashedCredentialsMatcher;}*/ }Filter Chain定義說明:
Shiro內置的FilterChain:
| anon | org.apache.shiro.web.filter.authc.AnonymousFilter |
| authc | org.apache.shiro.web.filter.authc.FormAuthenticationFilter |
| authcBasic | org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter |
| perms | org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter |
| port | org.apache.shiro.web.filter.authz.PortFilter |
| rest | org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter |
| roles | org.apache.shiro.web.filter.authz.RolesAuthorizationFilter |
| ssl | org.apache.shiro.web.filter.authz.SslFilter |
| user | org.apache.shiro.web.filter.authc.UserFilter |
* anon:所有url都都可以匿名訪問
* authc: 需要認證才能進行訪問
* user:配置記住我或認證通過可以訪問
自定義的攔截器(可選)
如果需要按照自己的需要定義一個 oauth2 的攔截器,則需要 繼承 AuthenticatingFilter 實現幾個方法即可。
/*** oauth2過濾器*/ public class OAuth2Filter extends AuthenticatingFilter {/*** logger*/private static final Logger LOGGER = LoggerFactory.getLogger(OAuth2Filter.class);@Overrideprotected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {//獲取請求tokenString token = getRequestToken((HttpServletRequest) request);if (StringUtils.isBlank(token)) {return null;}return new OAuth2Token(token);}/*** shiro權限攔截核心方法 返回true允許訪問resource,* @param request* @param response* @param mappedValue* @return*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {return false;}/*** 當訪問拒絕時是否已經處理了;* 如果返回true表示需要繼續處理;* 如果返回false表示該攔截器實例已經處理完成了,將直接返回即可。* @param request* @param response* @return* @throws Exception*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {//獲取請求token,如果token不存在,直接返回401String token = getRequestToken((HttpServletRequest) request);if (StringUtils.isBlank(token)) {HttpServletResponse httpResponse = (HttpServletResponse) response;((HttpServletResponse) response).setStatus(401);response.getWriter().print("沒有權限,請聯系管理員授權");return false;}return executeLogin(request, response);}/*** 鑒定失敗,返回錯誤信息* @param token* @param e* @param request* @param response* @return*/@Overrideprotected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {try {((HttpServletResponse) response).setStatus(401);response.getWriter().print("沒有權限,請聯系管理員授權");} catch (IOException e1) {LOGGER.error(e1.getMessage(), e1);}return false;}/*** 獲取請求的token*/private String getRequestToken(HttpServletRequest httpRequest) {//從header中獲取tokenString token = httpRequest.getHeader("token");//如果header中不存在token,則從參數中獲取tokenif (StringUtils.isBlank(token)) {return httpRequest.getParameter("token");}// 還可以實現從 cookie 獲取Cookie[] cookies = httpRequest.getCookies();if(null==cookies||cookies.length==0){return null;}for (Cookie cookie : cookies) {if (cookie.getName().equals("token")) {token = cookie.getValue();continue;}}return token;} }具體實現可以參考我的上篇文章 《Apache Shiro的攔截器和認證》
認證實現
Shiro的認證過程最終會交由Realm執行,這時會調用Realm的 getAuthenticationInfo(token) 方法。
該方法主要執行以下操作:
而在我們的應用程序中要做的就是自定義一個Realm類,繼承 AuthorizingRealm 抽象類,重載 doGetAuthenticationInfo (),重寫獲取用戶信息的方法。
@Component public class OAuth2Realm extends AuthorizingRealm {@Resourceprivate ShiroService shiroService;@Resourceprivate SysUserService sysUserService;/*** 此方法調用 hasRole,hasPermission的時候才會進行回調.** 權限信息.(授權):* 1、如果用戶正常退出,緩存自動清空;* 2、如果用戶非正常退出,緩存自動清空;* 3、如果我們修改了用戶的權限,而用戶不退出系統,修改的權限無法立即生效。* (需要手動編程進行實現;放在service進行調用)* 在權限修改后調用realm中的方法,realm已經由spring管理,所以從spring中獲取realm實例,* 調用clearCached方法;* :Authorization 是授權訪問控制,用于對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。*** 當沒有使用緩存的時候,不斷刷新頁面的話,這個代碼會不斷執行,* 當其實沒有必要每次都重新設置權限信息,所以我們需要放到緩存中進行管理;* 當放到緩存中時,這樣的話,doGetAuthorizationInfo就只會執行一次了,* 緩存過期之后會再次執行。** @param principals* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {SysUserEntity user =(SysUserEntity) (principals.getPrimaryPrincipal());;// 獲取該用戶權限列表Set<String> permsSet = shiroService.getUserPermissions(user.getId());SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setStringPermissions(permsSet);return info;}/*** 認證回調函數,登錄時調用* 首先根據傳入的用戶名獲取User信息;然后如果user為空,那么拋出沒找到帳號異常UnknownAccountException;* 如果user找到但鎖定了拋出鎖定異常LockedAccountException;最后生成AuthenticationInfo信息,* 交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配,*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) token;SysUserEntity user = sysUserService.queryByUsername(usernamePasswordToken.getUsername());//賬號不存在、密碼錯誤if (user == null) {throw new KCException("賬號或密碼不正確");}// 交給 shiro 自己去驗證,// 明文驗證return new SimpleAuthenticationInfo(user, // 存入憑證的信息,登陸成功后可以使用 SecurityUtils.getSubject().getPrincipal();在任何地方使用它user.getPassword(),getName());// 加密的方式// 交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,如果覺得人家的不好可以自定義實現/*return new SimpleAuthenticationInfo(user,user.getPassword(),ByteSource.Util.bytes(user.getSalt()), // 加鹽,可以注冊憑證匹配器 HashedCredentialsMatcher 告訴它怎么加密的getName());*/} }實現上面兩個方法即可完成身份驗證和權限驗證。
登陸實現
@PostMapping("/login")@ApiOperation("系統登陸")public ResponseEntity<String> login(@RequestBody SysUserLoginForm userForm) {String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);if (!userForm.getCaptcha().equalsIgnoreCase(kaptcha)) {throw new KCException("驗證碼不正確!");}UsernamePasswordToken token = new UsernamePasswordToken(userForm.getUsername(), userForm.getPassword());Subject currentUser = SecurityUtils.getSubject();currentUser.login(token);//賬號鎖定if (getUser().getStatus() == SysConstant.SysUserStatus.LOCK) {throw new KCException("賬號已被鎖定,請聯系管理員");}return ResponseEntity.status(HttpStatus.OK).body("登陸成功!");}權限驗證
@ApiOperation("用于測試,查詢")@ApiImplicitParam(name = "string", value = "id", dataType = "string")@RequiresPermissions("sys:user:list1")@GetMapping()public ResponseEntity<List<SysUserEntity>> query(@CustomValid String string) {return new ResponseEntity<>(sysUserService.query(new SysUserEntity()), OK);}簡單測試一個例子,sys:user:list1 多加一個 1 肯定會驗證失敗,查看程序會做什么,它會去我們定義的 Realm 中的 doGetAuthorizationInfo(PrincipalCollection principals) 方法中,執行查詢該用戶的所有權限。
驗證失敗后最后程序結果如下:
權限注解
@RequiresAuthentication 表示當前Subject已經通過login進行了身份驗證;即Subject. isAuthenticated()返回true。 @RequiresUser 表示當前Subject已經身份驗證或者通過記住我登錄的。@RequiresGuest 表示當前Subject沒有身份驗證或通過記住我登錄過,即是游客身份。@RequiresRoles(value={“admin”, “user”}, logical= Logical.AND) 表示當前Subject需要角色admin和user。@RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR) 表示當前Subject需要權限user:a或user:b。參考資料
- [Spring Boot 集成-Shiro]
- 39.2. Spring Boot Shiro權限管理【從零開始學Spring Boot】
總結
以上是生活随笔為你收集整理的学习Spring Boot:(十三)配置 Shiro 权限认证的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开源python-文档撰写
- 下一篇: 学习Spring Boot:(十一) 自