shiro 框架实现 LDAP 登录

1. shiro 框架实现LDAP登录

主要逻辑如下:

1.1 验证用户名和密码

        // 获取传递的用户名称、itcode
        String username = loginParam.getUsername();
        String itCode;
        if (username.endsWith(LdapProperties.REALM)) {
    
    
            //裁掉“@leinovo.com”
            itCode = username.replace(LdapProperties.REALM, "");
        } else {
    
    
            itCode = username;
            //邮箱地址补全
            username = username.concat(LdapProperties.REALM);
        }

        // 获取加密后的登录密码
        String password = loginParam.getPassword();

        // 解析登录密码;
        try {
    
    
            password = AesUtils.aesDecrypt(password, jwtProperties.getSecret());
        } catch (Exception e) {
    
    
            log.error("密码解密异常...");
            return ApiResult.fail(ApiCode.LOGIN_EXCEPTION);
        }

1.2 连接ldap服务器,并校验身份

根据用户名和密码,从ldap 中获取用户

User user;
        try {
    
    
            DirContext dirContext = ldapMapper.connectLDAP(username, password);
            user = ldapMapper.getAccountInfo(dirContext, username);
        } catch (NamingException e) {
    
    
            log.error("登陆失败,userName:{}", loginParam.getUsername());
            return ApiResult.fail(ApiCode.LOGIN_EXCEPTION);
        }
        if (user == null) {
    
    
            log.error("登陆失败,用户信息异常,userName:{}", loginParam.getUsername());
            return ApiResult.fail(ApiCode.LOGIN_EXCEPTION);
        }

1.3 查询本地用户信息

1.3.1 没有,说明是首次登录,可以给用户赋值默认角色;

String itCode = user.getItcode();
        AcUser acUser = userMapper.getByItcode(itCode);

        LoginUserVo loginUserVo;
        Set<String> roleNames = new HashSet<>();
        //dbUser为空说明该用户为首次登录,需要赋予默认的角色
        if (acUser == null) {
    
    
            //将用户信息存入数据库
            userMapper.insert(SysUserConvert.INSTANCE.userToAcUser(user));

            // 查询默认角色
            //授予默认的角色权限
            LambdaQueryWrapper<Role> queryWrapper = Wrappers.lambdaQuery();
            queryWrapper.eq(Role::getName, CommonConstant.DEFAULT_ROLE_NAME);
            Role role = roleMapper.selectOne(queryWrapper);
            if (role == null) {
    
    
                log.error("The default role is missing, please contact the administrator!");
                return ApiResult.fail("The default role is missing, please contact the administrator!");
            }

            // 保存关联关系
            AcRoleUser userRole = new AcRoleUser();
            userRole.setRoleid(role.getId());
            userRole.setUserid(user.getId());
            roleUserMapper.insert(userRole);
        }

1.3.2 有,不用处理

1.4 shiro 登录 生成token

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
 
        // 获取加密的盐
        //使用默认盐值:LEINOVO@QIT^ABC$$
        String salt = jwtProperties.getSecret();
        // 生成token字符串并返回,失效时间1小时;
        Duration expireDuration = Duration.ofSeconds(jwtProperties.getExpireSecond());

        /**
         * 生成token:每个人的token是固定的,只要认证了,下次可以继续使用
         * jwtToken:也是固定的,登录后,下次继续用;
         * shiro认证登录:subject.login(jwtToken)
         */
        String token = JwtUtil.generateToken(loginUserVo.getItCode(), salt, expireDuration);
        log.debug("token:{}", token);


        // 使用 itcode\token\salt\expireTime 完成shiro 认证注册;
        // 创建AuthenticationToken
        JwtToken jwtToken = JwtToken.build(token, loginUserVo.getItCode(), salt, jwtProperties.getExpireSecond());
        // 从SecurityUtils里边创建一个 subject
        Subject subject = SecurityUtils.getSubject();
        // 执行认证登陆
        subject.login(jwtToken);

1.5 token 保存到redis

保存token 后,返回登录成功

        // 缓存 token\jwtToken\salt\loginUserVo\expireTime 到Redis
        loginRedisService.cacheLoginInfo(jwtToken, loginUserVo, true);

        // 设置响应头
        response.setHeader(JwtTokenUtil.getTokenName(), token);
        log.debug("登陆成功,username:{}", loginUserVo.getItCode());


        // 设置返回三大参数(token,loginUserVo, homePage) todo homePage 需要进一步确认
        // 返回token
        Map map = new HashMap();
        map.put("token", token);
        map.put("userMsg", loginUserVo);

        return ApiResult.ok(map, "登陆成功");

2. 原理

登录后,shiro框架会拦截请求,校验接口 request 中token的正确性,做出权限控制。

3. 其他代码

3.1 LdapMapper

package com.leinovo.npi.auth.shiro.ldap;

import com.leinovo.platform.common.entity.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.Hashtable;

/**
 * @author: Liming
 * @date: 2019/10/17
 */
@Repository
public class LdapMapper {
    
    

    @Autowired
    private LdapProperties ldapProperties;

    /**
     * 获取ldap账户信息
     *
     * @param ctx
     * @param userName
     * @return
     */
    public User getAccountInfo(DirContext ctx, String userName) throws NamingException {
    
    
        String name = userName;
        if (userName.endsWith(LdapProperties.REALM)) {
    
    
            //裁掉“@leinovo.com”
            name = userName.replace(LdapProperties.REALM, "");
        }
        String filter = StringUtils.replace(ldapProperties.getSearchFilter(), "<uid>", name);
        String[] attrPersonArray = {
    
    LdapProperties.EMAIL, LdapProperties.PHONE, LdapProperties.ADDRESS,
                LdapProperties.DISPLAYNAME, LdapProperties.DEPARTMENT, LdapProperties.MANAGER};
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        searchControls.setReturningAttributes(attrPersonArray);
        NamingEnumeration<SearchResult> answer = ctx.search(ldapProperties.getContext(), filter, searchControls);
        if (answer.hasMore()) {
    
    
            SearchResult result = answer.next();
            NamingEnumeration<? extends Attribute> attrs = result.getAttributes().getAll();
            User user = new User();
            while (attrs.hasMore()) {
    
    
                Attribute attr = attrs.next();
                if (LdapProperties.EMAIL.equals(attr.getID())) {
    
    
                    user.setEmail(StringUtils.substring(String.valueOf(attr.get()), 0, 32));
                } else if (LdapProperties.PHONE.equals(attr.getID())) {
    
    
                    user.setPhone(StringUtils.substring(String.valueOf(attr.get()), 0, 20));
                } else if (LdapProperties.ADDRESS.equals(attr.getID())) {
    
    
                    user.setAddress(StringUtils.substring(String.valueOf(attr.get()), 0, 100));
                } else if (LdapProperties.DISPLAYNAME.equals(attr.getID())) {
    
    
                    user.setRealname(StringUtils.substring(String.valueOf(attr.get()), 0, 99));
                } else if (LdapProperties.DEPARTMENT.equals(attr.getID())) {
    
    
                    user.setDepartment(StringUtils.substring(String.valueOf(attr.get()), 0, 100));
                } else if (LdapProperties.MANAGER.equals(attr.getID())) {
    
    
                    String managerAttrValue = String.valueOf(attr.get());
                    String manager = (managerAttrValue.split(",")[0].split("="))[1];
                    user.setManager(manager);
                }
            }
            return user;
        }
        return null;
    }

    /**
     * 使用java连接AD域
     *
     * @param username 用户名
     * @param password 密码
     * @return void
     * @author liming
     */
    public DirContext connectLDAP(String username, String password) throws NamingException {
    
    
        Hashtable<String, String> HashEnv = new Hashtable<String, String>();
        // LDAP访问安全级别(none,simple,strong)
        HashEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
        //AD的用户名,必须确保是邮箱地址
        HashEnv.put(Context.SECURITY_PRINCIPAL, username);
        //AD的密码
        HashEnv.put(Context.SECURITY_CREDENTIALS, password);
        // LDAP工厂类
        HashEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        //连接超时设置为3秒
        //HashEnv.put("com.sun.jndi.ldap.connect.timeout", "3000");
        // 默认端口389
        HashEnv.put(Context.PROVIDER_URL, ldapProperties.getUrl());
        if (ldapProperties.getUrl().contains(":636") || ldapProperties.getUrl().startsWith("ldaps://")) {
    
    
            HashEnv.put(Context.SECURITY_PROTOCOL, "ssl");
            HashEnv.put("java.naming.ldap.factory.socket", "com.leinovo.qit.shiro.ldap.TrustedSocketFactory");
        }
        // 初始化上下文
        return new InitialDirContext(HashEnv);
    }

}

3.2 LdapProperties

package com.leinovo.npi.auth.shiro.ldap;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @author: Liming
 * @date: 2019/10/17
 */
@Data
@Component
@ConfigurationProperties(prefix = "pss-portal.ldap")
public class LdapProperties {
    
    

	public static final String EMAIL = "mail";
	public static final String PHONE = "telephoneNumber";
	public static final String ADDRESS = "physicalDeliveryOfficeName";
	public static final String MANAGER = "manager";
	public static final String REALNAME = "msExchExtensionAttribute16";
	public static final String DISPLAYNAME = "displayName";
	public static final String DEPARTMENT = "department";
	public static final String REALM = "@leinovo.com";

	private String searchFilter;

	private String context;

	private String url;

}

3.3 JwtToken

package com.leinovo.npi.auth.shiro.jwt;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.leinovo.platform.util.JwtUtil;
import com.leinovo.npi.auth.shiro.util.IpUtil;
import lombok.Data;
import lombok.experimental.Accessors;
import org.apache.shiro.authc.HostAuthenticationToken;

import java.util.Date;

/**
 * Shiro JwtToken对象
 *
 * @author geekidea
 * @date 2019-09-27
 * @since 1.3.0.RELEASE
 **/
@Data
@Accessors(chain = true)
public class JwtToken implements HostAuthenticationToken {
    
    
    /**
     * 登陆ip
     */
    private String host;
    /**
     * 登陆用户名称
     */
    private String username;
    /**
     * 登陆盐值
     */
    private String salt;
    /**
     * 登陆token
     */
    private String token;
    /**
     * 创建时间
     */
    private Date createDate;
    /**
     * 多长时间过期,默认一小时
     */
    private long expireSecond;
    /**
     * 过期日期
     */
    private Date expireDate;

    private String principal;

    private String credentials;

    @Override
    public Object getPrincipal() {
    
    
        return token;
    }

    @Override
    public Object getCredentials() {
    
    
        return token;
    }

    public static JwtToken build(String token, String username, String salt, long expireSecond) {
    
    
        DecodedJWT decodedJWT = JwtUtil.getJwtInfo(token);
        Date createDate = decodedJWT.getIssuedAt();
        Date expireDate = decodedJWT.getExpiresAt();
        return new JwtToken()
                .setUsername(username)
                .setToken(token)
                .setHost(IpUtil.getRequestIp())
                .setSalt(salt)
                .setCreateDate(createDate)
                .setExpireSecond(expireSecond)
                .setExpireDate(expireDate);

    }

}

3.4 JwtUtil

    /**
     * 生成JWT Token
     *
     * @param username       用户名
     * @param salt           盐值
     * @param expireDuration 过期时间和单位
     * @return token
     */
    public static String generateToken(String username, String salt, Duration expireDuration) {
    
    
        try {
    
    
            if (StringUtils.isBlank(username)) {
    
    
                log.error("username不能为空");
                return null;
            }
            log.debug("username:{}", username);

            // 如果盐值为空,则使用默认值:LEiNOVO@QIT^ABC$$
            if (StringUtils.isBlank(salt)) {
    
    
                salt = jwtProperties.getSecret();
            }
            log.debug("salt:{}", salt);

            // 过期时间,单位:秒
            Long expireSecond;
            // 默认过期时间为1小时
            if (expireDuration == null) {
    
    
                expireSecond = jwtProperties.getExpireSecond();
            } else {
    
    
                expireSecond = expireDuration.getSeconds();
            }
            log.debug("expireSecond:{}", expireSecond);
            Date expireDate = DateUtils.addSeconds(new Date(), expireSecond.intValue());
            log.debug("expireDate:{}", expireDate);

            // 生成token
            Algorithm algorithm = Algorithm.HMAC256(salt);
            String token = JWT.create()
                    .withClaim(CommonConstant.JWT_USERNAME, username)
                    .withJWTId(UUIDUtil.getUUID())              // jwt唯一id
                    .withIssuer(jwtProperties.getIssuer())      // 签发人
                    .withSubject(jwtProperties.getSubject())    // 主题
                    .withAudience(jwtProperties.getAudience())  // 签发的目标
                    .withIssuedAt(new Date())                   // 签名时间
                    .withExpiresAt(expireDate)                  // token过期时间
                    .sign(algorithm);                           // 签名
            return token;
        } catch (Exception e) {
    
    
            log.error("generateToken exception", e);
        }
        return null;
    }

猜你喜欢

转载自blog.csdn.net/leinminna/article/details/112606994