spring整合應用安全框架Shiro
Shiro的介紹
Apache Shiro是一個強大易用的Java安全框架,它提供的主要功能有:
認證 -——使用者身份識別,常被稱為使用者“登入”;
授權—— 訪問控制;
密碼加密——保護或隱藏資料防止被偷窺;
會話管理——每使用者相關的時間敏感的狀態。
Shiro的三個核心元件(Subject,SecurityManager 和 Realms)介紹
Subject:“當前操作使用者”。但是,在Shiro中,Subject這一概念並不僅僅指人,也可以是第三方程序、後臺帳戶(Daemon Account)或其他類似事物。它僅僅意味著“當前跟軟體互動的東西”。但考慮到大多數目的和用途,你可以把它認為是Shiro的“使用者”概念。 Subject代表的是當前使用者的安全操作。
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通過SecurityManager來管理內部元件例項,並通過它來提供安全管理的各種服務。
Realm: Realm充當了Shiro與應用安全資料間的“橋樑”或者“聯結器”。也就是說,當對使用者執行認證(登入)和授權(訪問控制)驗證時,Shiro會從應用配置的Realm中查詢使用者及其許可權資訊。(當配置Shiro時,你至少要指定一個Realm,用於認證和(或)授權,至少需要一個。 Shiro內建了可以連線大量安全資料來源(又名目錄)的Realm,如LDAP、關係資料庫(JDBC)、類似INI的文字配置資源以及屬性檔案等。)
下圖為Shiro功能模組結構:
這些模組各有作用:
Authentication:身份認證/登入,驗證使用者是不是擁有相應的身份;
Authorization:授權,即許可權驗證,驗證某個已認證的使用者是否擁有某個許可權;
Session Manager:會話管理,即使用者登入後就是一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通JavaSE環境的,也可以是如Web環境的;
Cryptography:加密,保護資料的安全性,如密碼加密儲存到資料庫,而不是明文儲存;
Web Support:Web支援,可以非常容易的整合到Web環境;
Caching:快取,比如使用者登入後,其使用者資訊、擁有的角色/許可權不必每次去查,這樣可以提高效率;
Concurrency:shiro支援多執行緒應用的併發驗證,即如在一個執行緒中開啟另一個執行緒,能把許可權自動傳播過去;
Testing:提供測試支援;
Run As:允許一個使用者假裝為另一個使用者(如果他們允許)的身份進行訪問;
Remember Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話不用登入了。
Shiro依賴包
maven環境下,pom.xml中依賴包配置:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
web工程中引入Shiro框架,首先要在web.xml中配置:
<!-- Apache Shiro -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.xml,classpath:shiro/spring-shiro.xml</param-value>
</context-param>
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
<init-param>
<param-name>targetFilterLifecycle</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping><filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
web程式啟動時,首先會載入spring-shiro.xml配置檔案,然後執行web中的過濾器,實現安全登入。
配置Realm,進行驗證及授權
定義該一個安全認證的實現類,需要繼承AuthorizingRealm並實現登入驗證和賦予角色許可權的兩個方法
即:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationTokenauthcToken);--------登入認證時使用
protected AuthorizationInfogetAuthorizationInfo(PrincipalCollection principals);---------使用者授權時使用
還可以自定義一些其他業務中使用到的方法,如下:
@SuppressWarnings("restriction")
@Service
//@DependsOn({"userDao","roleDao","menuDao"})
public class SystemAuthorizingRealm extends AuthorizingRealm {
private Logger logger = LoggerFactory.getLogger(getClass());
private SystemService systemService;
public SystemAuthorizingRealm() {
this.setCachingEnabled(false);
}
/**
* 認證回撥函式, 登入時呼叫
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) {
UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
int activeSessionSize = getSystemService().getSessionDao().getActiveSessions(false).size();
if (logger.isDebugEnabled()){
logger.debug("login submit, active session size: {}, username: {}", activeSessionSize, token.getUsername());
}
// 校驗登入驗證碼
if (LoginController.isValidateCodeLogin(token.getUsername(), false, false)){
Session session = UserUtils.getSession();
String code = (String)session.getAttribute(ValidateCodeServlet.VALIDATE_CODE);
if (token.getCaptcha() == null || !token.getCaptcha().toUpperCase().equals(code)){
throw new AuthenticationException("msg:驗證碼錯誤, 請重試.");
}
}
// 校驗使用者名稱密碼
User user = getSystemService().getUserByLoginName(token.getUsername());
if (user != null) {
if (Global.NO.equals(user.getLoginFlag())){
throw new AuthenticationException("msg:該已帳號禁止登入.");
}
byte[] salt = Encodes.decodeHex(user.getPassword().substring(0,16));
return new SimpleAuthenticationInfo(new Principal(user, token.isMobileLogin()),
user.getPassword().substring(16), ByteSource.Util.bytes(salt), getName());
} else {
return null;
}
}
/**
* 獲取許可權授權資訊,如果快取中存在,則直接從快取中獲取,否則就重新獲取, 登入成功後呼叫
*/
protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
if (principals == null) {
return null;
}
AuthorizationInfo info = null;
info = (AuthorizationInfo)UserUtils.getCache(UserUtils.CACHE_AUTH_INFO);
if (info == null) {
info = doGetAuthorizationInfo(principals);
if (info != null) {
UserUtils.putCache(UserUtils.CACHE_AUTH_INFO, info);
}
}
return info;
}
/**
* 授權查詢回撥函式, 進行鑑權但快取中無使用者的授權資訊時呼叫
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
Principal principal = (Principal) getAvailablePrincipal(principals);
// 獲取當前已登入的使用者
if (!Global.TRUE.equals(Global.getConfig("user.multiAccountLogin"))){
Collection<Session> sessions = getSystemService().getSessionDao().getActiveSessions(true, principal, UserUtils.getSession());
if (sessions.size() > 0){
// 如果是登入進來的,則踢出已線上使用者
if (UserUtils.getSubject().isAuthenticated()){
for (Session session : sessions){
getSystemService().getSessionDao().delete(session);
}
}
// 記住我進來的,並且當前使用者已登入,則退出當前使用者提示資訊。
else{
UserUtils.getSubject().logout();
throw new AuthenticationException("msg:賬號已在其它地方登入,請重新登入。");
}
}
}
User user = getSystemService().getUserByLoginName(principal.getLoginName());
if (user != null) {
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
List<Menu> list = UserUtils.getMenuList();
for (Menu menu : list){
if (StringUtils.isNotBlank(menu.getPermission())){
// 新增基於Permission的許可權資訊
for (String permission : StringUtils.split(menu.getPermission(),",")){
info.addStringPermission(permission);
}
}
}
// 新增使用者許可權
info.addStringPermission("user");
// 新增使用者角色資訊
for (Role role : user.getRoleList()){
info.addRole(role.getEnname());
}
// 更新登入IP和時間
getSystemService().updateUserLoginInfo(user);
// 記錄登入日誌
LogUtils.saveLog(Servlets.getRequest(), "系統登入");
return info;
} else {
return null;
}
}
@Override
protected void checkPermission(Permission permission, AuthorizationInfo info) {
authorizationValidate(permission);
super.checkPermission(permission, info);
}
@Override
protected boolean[] isPermitted(List<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermitted(permissions, info);
}
@Override
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
authorizationValidate(permission);
return super.isPermitted(principals, permission);
}
@Override
protected boolean isPermittedAll(Collection<Permission> permissions, AuthorizationInfo info) {
if (permissions != null && !permissions.isEmpty()) {
for (Permission permission : permissions) {
authorizationValidate(permission);
}
}
return super.isPermittedAll(permissions, info);
}
/**
* 授權驗證方法
* @param permission
*/
private void authorizationValidate(Permission permission){
// 模組授權預留介面
}
/**
* 設定密碼校驗的Hash演算法與迭代次數
*/
@PostConstruct
public void initCredentialsMatcher() {
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(SystemService.HASH_ALGORITHM);
matcher.setHashIterations(SystemService.HASH_INTERATIONS);
setCredentialsMatcher(matcher);
}
/**
* 獲取系統業務物件
*/
public SystemService getSystemService() {
if (systemService == null){
systemService = SpringContextHolder.getBean(SystemService.class);
}
return systemService;
}
/**
* 授權使用者資訊
*/
public static class Principal implements Serializable {
private static final long serialVersionUID = 1L;
private String id; // 編號
private String loginName; // 登入名
private String name; // 姓名
private boolean mobileLogin; // 是否手機登入
// private Map<String, Object> cacheMap;
public Principal(User user, boolean mobileLogin) {
this.id = user.getId();
this.loginName = user.getLoginName();
this.name = user.getName();
this.mobileLogin = mobileLogin;
}
public String getId() {
return id;
}
public String getLoginName() {
return loginName;
}
public String getName() {
return name;
}
public boolean isMobileLogin() {
return mobileLogin;
}
/**
* 獲取SESSIONID
*/
public String getSessionid() {
try{
return (String) UserUtils.getSession().getId();
}catch (Exception e) {
return "";
}
}
@Override
public String toString() {
return id;
}
}
}
Shiro配置檔案
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd"
default-lazy-init="true">
<description>Shiro Configuration</description>
<!-- 載入配置屬性檔案 -->
<context:property-placeholder ignore-unresolvable="true" location="classpath:jeesite.properties" />
<!-- Shiro許可權過濾過濾器定義 -->
<bean name="shiroFilterChainDefinitions" class="java.lang.String">
<constructor-arg>
<value>
/static/** = anon
/userfiles/** = anon
${adminPath}/cas = cas
${adminPath}/login = authc
${adminPath}/logout = logout
${adminPath}/** = user
/act/editor/** = user
/ReportServer/** = user
</value>
</constructor-arg>
</bean>
<!-- 安全認證過濾器 -->
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<property name="securityManager" ref="securityManager" />
<property name="loginUrl" value="${adminPath}/login" />
<property name="successUrl" value="${adminPath}?login" />
<property name="filters">
<map>
<entry key="cas" value-ref="casFilter"/>
<entry key="authc" value-ref="formAuthenticationFilter"/>
</map>
</property>
<property name="filterChainDefinitions">
<ref bean="shiroFilterChainDefinitions"/>
</property>
</bean>
<!-- CAS認證過濾器 -->
<bean id="casFilter" class="org.apache.shiro.cas.CasFilter">
<property name="failureUrl" value="${adminPath}/login"/>
</bean>
<!-- 定義Shiro安全管理配置 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="systemAuthorizingRealm" />
<property name="sessionManager" ref="sessionManager" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 自定義會話管理配置 -->
<bean id="sessionManager" class="com.thinkgem.jeesite.common.security.shiro.session.SessionManager">
<property name="sessionDAO" ref="sessionDAO"/>
<!-- 會話超時時間,單位:毫秒 -->
<property name="globalSessionTimeout" value="${session.sessionTimeout}"/>
<!-- 定時清理失效會話, 清理使用者直接關閉瀏覽器造成的孤立會話 -->
<property name="sessionValidationInterval" value="${session.sessionTimeoutClean}"/>
<!-- <property name="sessionValidationSchedulerEnabled" value="false"/> -->
<property name="sessionValidationSchedulerEnabled" value="true"/>
<property name="sessionIdCookie" ref="sessionIdCookie"/>
<property name="sessionIdCookieEnabled" value="true"/>
</bean>
<!-- 指定本系統SESSIONID, 預設為: JSESSIONID 問題: 與SERVLET容器名衝突, 如JETTY, TOMCAT 等預設JSESSIONID,
當跳出SHIRO SERVLET時如ERROR-PAGE容器會為JSESSIONID重新分配值導致登入會話丟失! -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="jeesite.session.id"/>
</bean>
<!-- 自定義Session儲存容器 -->
<bean id="sessionDAO" class="com.thinkgem.jeesite.common.security.shiro.session.CacheSessionDAO">
<property name="sessionIdGenerator" ref="idGen" />
<property name="activeSessionsCacheName" value="activeSessionsCache" />
<property name="cacheManager" ref="shiroCacheManager" />
</bean>
<!-- 自定義系統快取管理器-->
<bean id="shiroCacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
<property name="cacheManager" ref="cacheManager"/>
</bean>
<!-- 保證實現了Shiro內部lifecycle函式的bean執行 -->
<bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<!-- AOP式方法級許可權檢查 -->
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true" />
</bean>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
</beans>
這裡做一下說明,Shiro預設到的許可權驗證類別:
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
logout --org.apache.shiro.web.filter.authc.LogoutFilter
解釋:
anon---例子/admins/**=anon沒有引數,表示可以匿名使用。
authc---例子/admins/user/**=authc表示需要認證(登入)才能使用,沒有引數
roles---例子/admins/user/**=roles[admin],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,當有多個引數時,例如admins/user/**=roles["admin,guest"],每個引數通過才算通過,相當於hasAllRoles()方法。
perms---例子/admins/user/**=perms[user:add:*],引數可以寫多個,多個時必須加上引號,並且引數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個引數時必須每個引數都通過才通過,想當於isPermitedAll()方法。
rest---例子/admins/user/**=rest[user],根據請求的方法,相當於/admins/user/**=perms[user:method],其中method為post,get,delete等。
port---例子/admins/user/**=port[8081],當請求的url的埠不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裡port的埠,queryString是你訪問的url裡的?後面的引數。
authcBasic---例如/admins/user/**=authcBasic沒有引數表示httpBasic認證
ssl---例子/admins/user/**=ssl沒有引數,表示安全的url請求,協議為https
user---例如/admins/user/**=user沒有引數表示必須存在使用者,當登入操作時不做檢查
注意:
Shiro.xml載入配置是從上而下的,也就是向上面的配置,如/** = anon ,如果把這個配置在第一行,那麼下面的配置都沒用。因為是從上往下去匹配,只要匹配中了,就不匹配了所以必須要有序。