最近在給公司搭建一個權限系統,在原有的測試管理平臺上集成shiro框架,提供一個登陸和權限控制功能。css
以前是使用用戶表,管理員直接建立用戶,如今要使用員工的工號登陸,公司員工是使用LDAP存儲,java
恰好shiro也提供LDAP的支持,調試了幾天,總算調通了web
使用通用的表設計,先看下權限系統的表設計算法
權限編碼須要本身去實現,下面看下shiro的配置文件spring
<?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:util="http://www.springframework.org/schema/util" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd" default-lazy-init="true"> <!-- 用戶受權信息Cache, 採用EhCache --> <!-- <bean id="ehcacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/> </bean> --> <!-- cache 單機實現 --> <!--<import resource="classpath:shiro/shiro-ehcache.xml" />--> <!-- 自定義的Realm --> <bean id="authShiroRealm" class="com.xn.manage.shiro.WebAuthorizingRealm"> <!--<property name="credentialsMatcher" ref="credentialsMatcher" />--> <property name="cachingEnabled" value="false" /> <property name="authorizationCachingEnabled" value="false"/> </bean> <!-- 基於Form表單的身份驗證過濾器 --> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password" /> <property name="rememberMeParam" value="rememberMe" /> <property name="loginUrl" value="/login"/> </bean> <!-- 會話ID生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator" /> <!-- 指定本系統SESSIONID, 默認爲: JSESSIONID 問題: 與SERVLET容器名衝突, 如JETTY, TOMCAT 等默認JSESSIONID, 當跳出SHIRO SERVLET時如ERROR-PAGE容器會爲JSESSIONID從新分配值致使登陸會話丟失! --> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <property name="httpOnly" value="true" /> <property name="maxAge" value="-1" /> <property name="name" value="sid" /> </bean> <!--多個realm 的認證策略 --> <bean id="authenticator" class="org.apache.shiro.authc.pam.ModularRealmAuthenticator"> <property name="authenticationStrategy"> <bean class="org.apache.shiro.authc.pam.FirstSuccessfulStrategy " /> </property> </bean> <!-- Shiro's main business-tier object for web-enabled applications --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--<property name="realm" ref="authShiroRealm" />--> <!--<property name="realm" ref="ldapAuthorizingRealm" />--> <property name="authenticator" ref="authenticator"></property> <property name="realms"> <list> <ref bean="ldapAuthorizingRealm" /> <!--<ref bean="authShiroRealm" />--> </list> </property> <!--<property name="cacheManager" ref="cacheManager" />--> <property name="sessionManager" ref="sessionManager"/> </bean> <!-- session管理 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- session超時時間設置爲8小時 --> <property name="globalSessionTimeout" value="28800000"></property> <property name="sessionIdCookie" ref="sessionIdCookie" /> <property name="sessionIdCookieEnabled" value="true" /> </bean> <!-- 重寫ldap認證 這裏的rootDN是搜索公司的員工的根目錄--> <bean id="ldapAuthorizingRealm" class="com.xn.manage.shiro.LdapAuthorizingRealm"> <property name="rootDN" value="OU=xxx公司,DC=xxx域,DC=com"/> <property name="userDnTemplate" value="{0}"/> <property name="contextFactory" ref="contextFactory"/> </bean> <!-- 配置ldap路徑及配置一個默認的用戶和密碼 --> <bean id="contextFactory" class="org.apache.shiro.realm.ldap.JndiLdapContextFactory"> <property name="url" value="ldap://LDAP的地址:端口"/> <property name="systemUsername" value="CN=xxx系統用戶,OU=xxx公司,DC=xxx域,DC=com"/> <property name="systemPassword" value="密碼123456"/> </bean> <!-- Shiro Filter --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login" /> <property name="successUrl" value="/index" /> <property name="unauthorizedUrl" value="/403"/> <property name="filters"> <util:map> <entry key="authc"> <bean class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"/> </entry> </util:map> </property> <property name="filterChainDefinitions"> <value> /login = anon /sure_login = anon /logout = logout /picture/** = anon /vendor/** = anon /js/** = anon /css/** = anon /decorators/** = anon /common/** = anon /dist/** = anon /** = user </value> </property> </bean> <!-- 保證明現了Shiro內部lifecycle函數的bean執行 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>
LDAP有不少名詞,DN、CN、OU、DC請自行百度數據庫
再看看LDAP的reamlapache
public class LdapAuthorizingRealm extends JndiLdapRealm { private static final Logger logger = LoggerFactory.getLogger(LdapAuthorizingRealm.class);
private String rootDN; public String getRootDN() { return rootDN; } public void setRootDN(String rootDN) { this.rootDN = rootDN; } /** * 登陸時調用 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info; try { info = queryForAuthenticationInfo(token, getContextFactory()); getAuthorizationInfo(((UsernamePasswordToken) token).getUsername()); } catch (AuthenticationNotSupportedException e) { String msg = "Unsupported configured authentication mechanism"; throw new UnsupportedAuthenticationMechanismException(msg, e); } catch (javax.naming.AuthenticationException e) { String msg = "LDAP authentication failed."; throw new AuthenticationException(msg, e); } catch (NamingException e) { String msg = "LDAP naming error while attempting to authenticate user."; throw new AuthenticationException(msg, e); } catch (UnknownAccountException e) { String msg = "帳號不存在!"; throw new UnknownAccountException(msg, e); } catch (IncorrectCredentialsException e) { String msg = "IncorrectCredentialsException"; throw new IncorrectCredentialsException(msg, e); } return info; } /** * 受權 * * @param principalCollection * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { // 由於非正常退出,即沒有顯式調用 SecurityUtils.getSubject().logout() // (多是關閉瀏覽器,或超時),但此時緩存依舊存在(principals),因此會本身跑到受權方法裏。 if (!SecurityUtils.getSubject().isAuthenticated()) { doClearCache(principalCollection); SecurityUtils.getSubject().logout(); return null; } // 獲取當前登陸的用戶名 String username = (String) principalCollection.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); Session session = SecurityUtils.getSubject().getSession(); authorizationInfo.setStringPermissions((Set<String>) session.getAttribute("permissions")); return authorizationInfo; } /** * 鏈接LDAP查詢用戶信息是否存在 * <p> * 1. 從頁面獲得登錄名和密碼。注意這裏的登錄名和密碼一開始並無被用到。 * 2. 先匿名綁定到LDAP服務器,若是LDAP服務器沒有啓用匿名綁定,通常會提供一個默認的用戶,用這個用戶進行綁定便可。 * 3. 以前輸入的登錄名在這裏就有用了,當上一步綁定成功之後,須要執行一個搜索,而filter就是用登錄名來構造,形如: "CN=*(xn607659)" 。 * 搜索執行完畢後,須要對結果進行判斷,若是隻返回一個entry,這個就是包含了該用戶信息的entry,能夠獲得該 entry的DN,後面使用。 * 若是返回不止一個或者沒有返回,說明用戶名輸入有誤,應該退出驗證並返回錯誤信息。 * 4. 若是能進行到這一步,說明用相應的用戶,而上一步執行時獲得了用戶信息所在的entry的DN,這裏就須要用這個DN和第一步中獲得的password從新綁定LDAP服務器。 * 5. 執行完上一步,驗證的主要過程就結束了,若是能成功綁定,那麼就說明驗證成功,若是不行,則應該返回密碼錯誤的信息。 * 這5大步就是基於LDAP的一個 「兩次綁定」 驗證方法 * * @param token * @param ldapContextFactory * @return * @throws NamingException */ @Override protected AuthenticationInfo queryForAuthenticationInfo( AuthenticationToken token, LdapContextFactory ldapContextFactory) throws NamingException { Object principal = token.getPrincipal();//輸入的用戶名 Object credentials = token.getCredentials();//輸入的密碼 String userName = principal.toString(); String password = new String((char[]) credentials); LdapContext systemCtx = null; LdapContext ctx = null; try { //使用系統配置的用戶鏈接LDAP systemCtx = ldapContextFactory.getSystemLdapContext(); SearchControls constraints = new SearchControls(); constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);//搜索範圍是包括子樹 // String returnedAtts[] = { "uid","displayName","cn","company","department","mailNickname"}; // constraints.setReturningAttributes(returnedAtts); NamingEnumeration results = systemCtx.search(rootDN, "UID=" + principal , constraints); if (results != null && !results.hasMore()) { throw new UnknownAccountException(); } else { while (results.hasMore()) { SearchResult si = (SearchResult) results.next(); principal = si.getName() + "," + rootDN; logger.debug(si.getAttributes().get("company").toString()); logger.debug(si.getAttributes().get("department").toString()); } logger.info("DN=[" + principal + "]"); try { //根據查詢到的用戶與輸入的密碼鏈接LDAP,用戶密碼正確才能鏈接 ctx = ldapContextFactory.getLdapContext(principal, credentials); dealUser(userName, password); } catch (NamingException e) { throw new IncorrectCredentialsException(); } return new SimpleAuthenticationInfo(userName, MD5Util.MD5(userName + password).toLowerCase(), getName()); } } finally { //關閉鏈接 LdapUtils.closeContext(systemCtx); LdapUtils.closeContext(ctx); } } /** * 將LDAP查詢到的用戶保存到sys_user表 * * @param userName */ private void dealUser(String userName, String password) { if (StringUtil.isEmpty(userName)) { return; } //TO DO... } /** * 獲取權限碼 * * @param username * @return */ private Map<String, Set<String>> getAuthorizationInfo(String username) { Map<String, Set<String>> authorizationMap = new HashMap<String, Set<String>>(); Set<String> codeSet = new HashSet<String>(); Session session = SecurityUtils.getSubject().getSession(); //查詢數據庫的用戶權限
//......
authorizationMap.put("permissions", codeSet); session.setAttribute("permissions", codeSet); logger.debug("當前登陸帳戶:{}的權限集合:{}", username, codeSet); return authorizationMap; } /** * 設定Password校驗的Hash算法與迭代次數.這裏使用了自定義的加密算法 */ @PostConstruct public void initCredentialsMatcher() { HashedCredentialsMatcher matcher = new HashedCredentialsMatcher("MD5") { @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object credentials = token.getCredentials(); UsernamePasswordToken token1 = (UsernamePasswordToken) token; if (credentials == null) { String msg = "Argument for byte conversion cannot be null."; throw new IllegalArgumentException(msg); } byte[] bytes = null; if (credentials instanceof char[]) { bytes = CodecSupport.toBytes(new String((char[]) credentials), PREFERRED_ENCODING); } else { bytes = objectToBytes(credentials); } String tokenHashedCredentials = MD5Util.MD5(token1.getUsername() + new String(bytes)).toLowerCase(); String accountCredentials = getCredentials(info).toString(); return tokenHashedCredentials.equals(accountCredentials); } }; matcher.setHashIterations(1); setCredentialsMatcher(matcher); } }
這裏涉及公司具體業務的代碼出於保密沒有寫出來,大概就是這個樣子瀏覽器