經過前面學習 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 {
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionValidationSchedulerEnabled(true);
sessionManager.setSessionIdCookieEnabled(true);
return sessionManager;
}
/** * 注冊安全管理,必須設置 SecurityManager * * @param oAuth2Realm 認證 * @param sessionManager 緩存 * @return */
@Bean
public SecurityManager securityManager(OAuth2Realm oAuth2Realm, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 可以添加多個認證,執行順序是有影響的
securityManager.setRealm(oAuth2Realm);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public 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");
// 登陸的 url
shiroFilter.setLoginUrl("/sys/login");
// 登陸成功跳轉的 url
shiroFilter.setSuccessUrl("/");
// 未授權的 url
// shiroFilter.setUnauthorizedUrl("/login.html");
//未授權界面;
shiroFilter.setUnauthorizedUrl("/403");
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
/** * Shiro生命周期處理器 * @return */
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
/** * 開啟Shiro的注解, * (如@RequiresRoles,@RequiresPermissions),需借助SpringAOP掃描使用Shiro注解的類, * 並在必要時進行安全邏輯驗證 * 配置以下兩個bean(DefaultAdvisorAutoProxyCreator(可選)和AuthorizationAttributeSourceAdvisor)即可實現此功能 * * @return */
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true);
return proxyCreator;
}
/** * 開啟 shiro aop注解支持. * * @param securityManager * @return */
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
/** * 憑證匹配器 * (由於我們的密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理了 * 所以我們需要修改下doGetAuthenticationInfo中的代碼; * ) * <b>需要在身份認證中添加 realm.setCredentialsMatcher(hashedCredentialsMatcher())</b> * @return */
/*@Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:這里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次數,比如散列兩次,相當於 md5(md5("")); return hashedCredentialsMatcher; }*/
}
Filter Chain定義說明:
1. 一個URL可以配置多個Filter,使用逗號分隔
2. 當設置多個過濾器時,全部驗證通過,才視為通過
3. 部分過濾器可指定參數,如perms,roles
Shiro內置的FilterChain:
Filter Name | Class |
---|---|
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);
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
//獲取請求token
String token = getRequestToken((HttpServletRequest) request);
if (StringUtils.isBlank(token)) {
return null;
}
return new OAuth2Token(token);
}
/** * shiro權限攔截核心方法 返回true允許訪問resource, * @param request * @param response * @param mappedValue * @return */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return false;
}
/** * 當訪問拒絕時是否已經處理了; * 如果返回true表示需要繼續處理; * 如果返回false表示該攔截器實例已經處理完成了,將直接返回即可。 * @param request * @param response * @return * @throws Exception */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
//獲取請求token,如果token不存在,直接返回401
String 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 */
@Override
protected 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中獲取token
String token = httpRequest.getHeader("token");
//如果header中不存在token,則從參數中獲取token
if (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)
方法。
該方法主要執行以下操作:
1. 檢查提交的進行認證的令牌信息
2. 根據令牌信息從數據源(通常為數據庫)中獲取用戶信息
3. 對用戶信息進行匹配驗證。
4. 驗證通過將返回一個封裝了用戶信息的AuthenticationInfo實例。
5. 驗證失敗則拋出AuthenticationException異常信息。
而在我們的應用程序中要做的就是自定義一個Realm類,繼承 AuthorizingRealm
抽象類,重載 doGetAuthenticationInfo ()
,重寫獲取用戶信息的方法。
@Component
public class OAuth2Realm extends AuthorizingRealm {
@Resource
private ShiroService shiroService;
@Resource
private SysUserService sysUserService;
/** * 此方法調用 hasRole,hasPermission的時候才會進行回調. * * 權限信息.(授權): * 1、如果用戶正常退出,緩存自動清空; * 2、如果用戶非正常退出,緩存自動清空; * 3、如果我們修改了用戶的權限,而用戶不退出系統,修改的權限無法立即生效。 * (需要手動編程進行實現;放在service進行調用) * 在權限修改后調用realm中的方法,realm已經由spring管理,所以從spring中獲取realm實例, * 調用clearCached方法; * :Authorization 是授權訪問控制,用於對用戶進行的操作授權,證明該用戶是否允許進行當前操作,如訪問某個鏈接,某個資源文件等。 * * * 當沒有使用緩存的時候,不斷刷新頁面的話,這個代碼會不斷執行, * 當其實沒有必要每次都重新設置權限信息,所以我們需要放到緩存中進行管理; * 當放到緩存中時,這樣的話,doGetAuthorizationInfo就只會執行一次了, * 緩存過期之后會再次執行。 * * @param principals * @return */
@Override
protected 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進行判斷密碼是否匹配, */
@Override
protected 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)
方法中,執行查詢該用戶的所有權限。
驗證失敗后最后程序結果如下:
Caused by: org.apache.shiro.authz.AuthorizationException: Not authorized to invoke method: public org.springframework.http.ResponseEntity com.wuwii.module.sys.controller.SysUserController.query(java.lang.String)
at org.apache.shiro.authz.aop.AuthorizingAnnotationMethodInterceptor.assertAuthorized(AuthorizingAnnotationMethodInterceptor.java:90)
... 77 common frames omitted
權限注解
@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。
參考資料
- Role-Based Access Control ↩