shiro使用LDAP认证

时间:2022-09-23 22:33:36

最近在给公司搭建一个权限系统,在原有的测试管理平台上集成shiro框架,提供一个登录和权限控制功能。

之前是使用用户表,管理员直接创建用户,现在要使用员工的工号登录,公司员工是使用LDAP存储,

刚好shiro也提供LDAP的支持,调试了几天,总算调通了

 

使用通用的表设计,先看下权限系统的表设计

shiro使用LDAP认证

权限编码需要自己去实现,下面看下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: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的reaml

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);
}
}

这里涉及公司具体业务的代码出于保密没有写出来,大概就是这个样子