六 SpringBoot集成Shiro認證
1 分析
??Shiro提供認證授權功能,所以SpringBoot中不需再編寫自定義注解,權限攔截,登錄攔截,登錄登出。Shiro 環境中有三個封裝對象Subject ,SecurityManager和Realms,SpringBoot 集成 Shiro 時需要配置相對應的Bean(Subject 不用)
2 導入依賴
<properties><java.version>8</java.version><shiro.version>1.7.1</shiro.version><thymeleaf.extras.shiro.version>2.0.0</thymeleaf.extras.shiro.version>
</properties>
<!--Shiro核心框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-core</artifactId><version>${shiro.version}</version>
</dependency>
<!-- Shiro使用Spring框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>${shiro.version}</version>
</dependency>
<!-- Shiro使用EhCache緩存框架 -->
<dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-ehcache</artifactId><version>${shiro.version}</version>
</dependency>
<!-- thymeleaf模板引擎和shiro框架的整合 -->
<dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>${thymeleaf.extras.shiro.version}</version>
</dependency>
3 創建數據源
// 有類才能生成Bean
public class EmployeeRealm extends AuthorizingRealm {@Autowiredprivate IEmployeeService employeeService;@Autowiredprivate IPermissionService permissionService;@Autowiredprivate IRoleService roleService;//授權方法@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {Employee currentEmployee= (Employee) principalCollection.getPrimaryPrincipal();SimpleAuthorizationInfo info=new SimpleAuthorizationInfo();if(currentEmployee.isAdmin()){List<Role> roles=roleService.listAll();for(Role role:roles){info.addRole(role.getSn());}info.addStringPermission("*:*");}else{List<Role> roleList=roleService.queryByEmployeeId(currentEmployee.getId());for(Role role:roleList){info.addRole(role.getSn());}//查詢該用戶的權限集合List<String> permissionList=permissionService.queryByEmployeeId(currentEmployee.getId());info.addStringPermissions(permissionList);}return info;}//認證方法@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {// 根據token獲取用戶名String username = (String) authenticationToken.getPrincipal();// 根據用戶名查詢用戶Employee currentEmployee=employeeService.getByUsername(username);// 根據查詢結果返回對應數據if(currentEmployee==null){return null;}return new SimpleAuthenticationInfo(currentEmployee,currentEmployee.getPassword(), ByteSource.Util.bytes(currentEmployee.getSalt()),getName());}
}
4 創建Shiro配置類
// 配置類注解
@Configuration
public class ShiroConfig {// 1.Realm 數據源從數據庫中查詢數據(先有Realm才能配置Bean,配置這個Bean需要先有這個類)// Bean一定是對象,對象不一定是Bean,對象需要基于類創建@Beanpublic EmployeeRealm employeeRealm(){EmployeeRealm realm = new EmployeeRealm();return realm;}// 2.SecurityManager 安全管理器(基于web環境下的)// 此處可用set調本類方法或傳參的方式聯系Realm // 傳參是在spring容器中查找這個Bean先類型再名字,去掉@Bean注解會報錯(參數名與方法名盡量一致)// 調用方法首先不會運行該方法,會看方法的返回值類型,在容器中查找該類型,找到多個再按照名字去找,找到了就直接用// 不會運行該方法,若在容器中沒找到該方法,就運行該方法并把返回值放到容器中,然后再拿過來用(無@Bean注解也行)@Beanpublic DefaultWebSecurityManager defaultWebSecurityManager(EmployeeRealm employeeRealm){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();securityManager.setRealm(employeeRealm);return securityManager;}// 請求攔截器 shiro過濾器 由于創建麻煩此處使用工廠類創建過濾器對象// 若想知道一個工廠類返回什么類型的Bean 可查詢其getObject()方法@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){ShiroFilterFactoryBean shiroFilterFactoryBean=new ShiroFilterFactoryBean();// 配置登錄頁面shiroFilterFactoryBean.setLoginUrl("/static/login.html");// 配置安全管理器shiroFilterFactoryBean.setSecurityManager(securityManager);// 配置攔截規則(過濾鏈) 底層為雙鏈表組成(有序,就是我們放入的順序,過濾器根據順序執行)LinkedHashMap<String,String> filterChainDefinitionMap=new LinkedHashMap<>();// 對靜態資源設置匿名訪問(瀏覽器圖標 html css js)放行filterChainDefinitionMap.put("/favicon.ico**","anon");filterChainDefinitionMap.put("/static/**","anon");// 不需攔截的訪問(公共資源)放行filterChainDefinitionMap.put("/login","anon");// 退出并且shiro清除session信息(上下文對象也就是用戶信息) 執行退出方法// 無需在再編寫退出方法,直接調用logout即可踢回登陸頁面filterChainDefinitionMap.put("/logout","logout");// 進行攔截filterChainDefinitionMap.put("/**","authc");// 將攔截規則設置給攔截器鏈(shiro生成了很多攔截器,看我們選用哪個)shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);// 此時無登錄信息 訪問任何頁面都應該被踢回登錄頁面return shiroFilterFactoryBean;}
}
5 LoginController – 登錄方法
??登錄認證實際上是shiro在做,但數據需要在realm中提供
@Controller
public class LoginController {@RequestMapping("/login")@ResponseBodypublic JsonResult login(String username, String password){// 他會自動從Spring容器中拿到SecurityManager設置給SecurityUtils// 然后再將SecurityManager設置給subjectSubject subject = SecurityUtils.getSubject();// 將用戶名密碼封裝到tokenUsernamePasswordToken token = new UsernamePasswordToken(username,password);// 此處返回異常不精確到某一項,防止有人試錯(先試帳號再試密碼)try {// 登錄失敗就拋異常subject.login(token);subject.getSession().setAttribute("user_in_session",subject.getPrincipal());} catch (UnknownAccountException e) {return new JsonResult(false,"賬號密碼有誤");} catch (IncorrectCredentialsException e) {return new JsonResult(false,"帳號密碼有誤");} catch (Exception e) {return new JsonResult(false,"系統異常,稍后再試");}return new JsonResult(true,"登錄成功");}
}
6 數據源查詢方法(service) – getByUsername
// IEmployeeService
Employee getByUsername(String username);
// EmployeeServiceImpl
public Employee getByUsername(String username) {return employeeMapper.getByUsername(username);
}
7 數據源查詢方法(mapper) – getByUsername
// mapper
Employee getByUsername(String username);
// xml
<select id="getByUsername" resultMap="BaseResultMap">select e.id, e.username, e.name, e.password, e.email, e.age, e.admin,d.id d_id,d.name d_name,d.sn d_sn,e.saltfrom employee e left join department d on e.dept_id = d.idwhere username=#{username}
</select>
8 shiro 內置過濾器
??shiro 啟動時會默認將以下這些類(過濾器)加載到程序中,然后使用Map將這些數據的關系以key value的方式存儲起來。
過濾器的名稱(key) | Java 類(value) |
---|---|
anon | org.apache.shiro.web. lter.authc.AnonymousFilter |
authc | org.apache.shiro.web. lter.authc.FormAuthenticationFilter |
authcBasic | org.apache.shiro.web. lter.authc.BasicHttpAuthenticationFilter |
roles | org.apache.shiro.web. lter.authz.RolesAuthorizationFilter |
perms | org.apache.shiro.web. lter.authz.PermissionsAuthorizationFilter |
user | org.apache.shiro.web. lter.authc.UserFilter |
logout | org.apache.shiro.web. lter.authc.LogoutFilter |
port | org.apache.shiro.web. lter.authz.PortFilter |
rest | org.apache.shiro.web. lter.authz.HttpMethodPermissionFilter |
ssl | org.apache.shiro.web. lter.authz.SslFilter |
anon: 匿名攔截器,即不需要登錄即可訪問(誰都可以訪問,不需要攔截);一般用于靜態資源過濾;示例“/static/**=anon”
authc: 表示需要認證(登錄)才能使用,該路徑所有請求都需登錄后才能訪問;示例“/**=authc”
authcBasic:Basic HTTP身份驗證攔截器
roles: 角色授權攔截器,驗證用戶是否擁有資源角色;示例“/admin/**=roles[admin]”
perms: 權限授權攔截器,驗證用戶是否擁有資源權限;示例“/user/create=perms[“user:create”]”
user: 用戶攔截器,用戶已經身份驗證/記住我登錄的都可;示例“/index=user”
logout: 退出攔截器,登出后自動清理session中的用戶信息。主要屬性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”
port: 端口攔截器,主要屬性:port(80):可以通過的端口;示例“/test= port[80]”,如果用戶訪問該頁面是非80,將自動將請求端口改為80并重定向到該80端口,其他路徑/參數等都一樣
rest: rest風格攔截器;
ssl: SSL攔截器,只有請求協議是https才能通過;否則自動跳轉會https端口(443);其他和port攔截器一樣;
9 400錯誤問題解決
??當傳入的參數 SpringMVC 無法轉換時,就會出現400問題(第一次訪問時出現),session是在瀏覽器第一次訪問服務器時,由服務器創建并生成一個sessionID,通過response響應給瀏覽器。
??瀏覽器訪問服務器時,服務器中有一個session池,當找到session后,將返回對應的JsessionID。
??但第一次訪問時會首先經過shiro過濾器,其中有一個會話管理器(SessionManager),他發現當前是第一次訪問,因此會進行一次URL重寫,服務器會生成session并把sessionID返回給安全管理器,會話管理器通過重定向回到瀏覽器,再次發起申請訪問服務器,此時第一次訪問服務器實際上是沒有訪問到服務器,因此服務器無法接收攜帶的參數(JsessionID)報400錯誤。(去掉url中的JsessionID即可訪問)
??第二次訪問時,session已經存在,通過id尋找session,可以正常訪問。或者告訴會話管理器,不需要做url重寫,可在shiroconfig中生成會話管理器,將 setSessionIdUrlRewritingEnabled 設置為false即可(默認為true)。
@Configuration
public class ShiroConfig {// 略// 會話管理器@Beanpublic DefaultWebSecurityManager sessionManager(){DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();// url重寫開關sessionManager.setSessionIdUrlRewritingEnabled(false);return sessionManager;}
}
??此處需注意,編寫好 sessionManager 會話管理器后,需要將其設置給 SecurityManager 安全管理器(通過參數)
@Configuration
public class ShiroConfig {// 略@Bean//安全管理器public DefaultWebSecurityManager defaultWebSecurityManager(EmployeeRealm employeeRealm, DefaultWebSessionManager sessionManager){// 創建安全管理器DefaultWebSecurityManager securityManager=new DefaultWebSecurityManager();// 設置realmsecurityManager.setRealm(employeeRealm);// 設置會話管理器securityManager.setSessionManager(sessionManager);return securityManager;}//會話管理器@Beanpublic DefaultWebSecurityManager sessionManager(){DefaultWebSessionManager sessionManager=new DefaultWebSessionManager();sessionManager.setSessionIdUrlRewritingEnabled(false);return sessionManager;}
}