ldap结合shiro搭建统一登录平台

Posted yuluoxingkong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ldap结合shiro搭建统一登录平台相关的知识,希望对你有一定的参考价值。

一、ldap登录

ldap相关的文档请参考其他资料,再次不再详述。假设ldap已经存储了用户的相关信息数据;目前需要再其他服务中调用ldap的用户信息来登录。如下

1.控制器层

 @ApiOperation(value = "登录", notes = "", httpMethod = "GET")
    @RequestMapping(value = "/login",method = RequestMethod.GET)
    public ResponseBean findByCn(@RequestParam(required = true) String uid,
                                 @RequestParam(required = true) String userPassword) throws InvalidNameException, CetcBigDataException {
        return  new ResponseBean(ErrorCode.SUCCESS.getCode(),ErrorCode.SUCCESS.getMsg(), ldapService.login(uid,userPassword));

    }

2.ldap登录代码;@Service

public class LdapService {

    @Autowired
    private LdapTemplate ldapTemplate;
    @Autowired
    private JWTUtil jwtUtil;

    @Value("${spring.ldap.urls}")
    private String url;

    @Value("${spring.ldap.base}")
    private String basedn;

    @Value("${spring.ldap.domain}")
    private String domain;

    private Hashtable<String, String> env = new Hashtable<String, String>();

    public ActiveUser login(String uid, String userPassword) throws CetcBigDataException {
        boolean loginFlag=ldapAuth(uid,userPassword);
     //   boolean loginFlag=connect(uid,userPassword);

        if (!loginFlag){
            throw new CetcBigDataException("登录失败");
        }

        ActiveUser person=new ActiveUser();
        person.setUserCode(uid);

        person.setToken(jwtUtil.sign(uid,userPassword));

        return person;

    }
    /**
     * 方法一:AD认证
     *
     * @param username 用户名
     * @param password 密码
     */
    boolean ldapAuth(String username, String password) {
        EqualsFilter filter = new EqualsFilter("uid", username);
        return ldapTemplate.authenticate("", filter.toString(), password);
    }
  /**
  * 方法二,
AD认证
  */
public boolean connect(String userName,String passwd) {
        boolean result=false;
        LdapContext ldapContext = null;
        //用户名称,cn,ou,dc 分别:用户,组,域
        env.put(Context.SECURITY_PRINCIPAL, "uid="+userName+","+domain+","+basedn);
        //用户密码 cn 的密码
        env.put(Context.SECURITY_CREDENTIALS, passwd);
        //url 格式:协议://ip:端口/组,域   ,直接连接到域或者组上面
        env.put(Context.PROVIDER_URL, url+"/"+basedn);
        //LDAP 工厂
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        //验证的类型     "none", "simple", "strong"
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        try {
            ldapContext = new InitialLdapContext(env, null);
            result=true;
            System.out.println("---connection is ready----");
        } catch (NamingException e) {
            System.out.println("--- get connection failure ----");
        }
        return result;
    }
}

3.登录时,需要生成token。用于shiro权限验证

 /**
     * 生成签名,24小时后过期
     * @param username 用户名
     * @param secret 用户的密码
     * @return 加密的token
     */
    public  String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + TOKEN_EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // 附带username信息
            String token =  JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
            redisTemplate.opsForValue().set("token::"+username,token);
            redisTemplate.expire("token:"+username,REDIS_TOKEN_EXPIRE_TIME, TimeUnit.DAYS);
            return token;
        } catch (Exception e) {
            LOG.error("签名失败 {}",username);
            return null;
        }
    }

 

二、shiro相关配置

shiro配置文件如下,将CustomRealm注入DefaultWebSecurityManager。

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(CustomRealm realm, @Value("${spring.redis.host}") String host,
                                                @Value("${spring.redis.port}") Integer port) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        // 使用自己的realm
        manager.setRealm(realm);
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        // 自定义缓存实现 使用redis
        manager.setCacheManager(cacheManager(host, port));
        // 自定义session管理 使用redis
        manager.setSessionManager(sessionManager(host, port));

        manager.setSubjectDAO(subjectDAO);
        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
        // 添加自己的过滤器并且取名为jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);
        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");
        /*
         * 自定义url规则 http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>(16);
        // 放行swagger
        filterRuleMap.put("/swagger-ui.html", "anon");
        filterRuleMap.put("/swagger-resources", "anon");
        filterRuleMap.put("/v2/api-docs", "anon");
        filterRuleMap.put("/webjars/springfox-swagger-ui/**", "anon");
        filterRuleMap.put("/wechatLogin", "anon");
        filterRuleMap.put("/wechatInfo", "anon");
        filterRuleMap.put("/get-message", "anon");
        filterRuleMap.put("/get-appMessage", "anon");
        filterRuleMap.put("/label-get-token", "anon");
        // 访问401和404页面不通过我们的Filter
        filterRuleMap.put("/401", "anon");
        filterRuleMap.put("/**", "jwt");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

    /**
     * Shiro生命周期处理器
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     */
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        // 强制使用cglib,防止重复代理和可能引起代理出错的问题
        // https://zhuanlan.zhihu.com/p/29161098
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(
            DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }

    /**
     * cacheManager 缓存 redis实现 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager(String host, Integer port) {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager(host, port));
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisManager redisManager(String host, Integer port) {
        RedisManager redisManager = new RedisManager();
        redisManager.setHost(host);
        redisManager.setPort(port);
        // 配置缓存过期时间
        redisManager.setExpire(1800);
        return redisManager;
    }

    /**
     * Session Manager 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager(String host, Integer port) {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO(host, port));
        return sessionManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO(String host, Integer port) {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager(host, port));
        return redisSessionDAO;
    }
CustomRealm主要用于接口验证用户正确与否,需要和shiro相关注解搭配使用,如下realm配置好后,控制器接口加入注解@RequiresAuthentication 就会对token进行验证
*/
@Component
public class CustomRealm  extends AuthorizingRealm {
    private final Logger LOG = LoggerFactory.getLogger(CustomRealm.class) ;
    @Lazy
    @Autowired
    private LdapService ldapService;

    @Lazy
    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 必须重写此方法,不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        String getPrimaryPrincipal=(String) principals.getPrimaryPrincipal();
        LOG.info(getPrimaryPrincipal);
        LOG.info(JWTUtil.getUsername(getPrimaryPrincipal));

        Person user = ldapService.findByUid(username);

        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        // simpleAuthorizationInfo.addStringPermissions(permission);
        // redisTemplate.opsForValue().getAndSet("user",user);
        return simpleAuthorizationInfo;
    }

    /**
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth)  throws AuthenticationException {

        //前端传过来的token
        String token = (String) auth.getCredentials();
        // 解密获得username,用于和数据库进行对比
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            // return null;
            LOG.warn("token invalid");
            throw new IncorrectCredentialsException();
        }
        Person userBean = ldapService.findByUid(username);
        if (userBean == null) {
            LOG.warn("User {} didn‘t existed!",username);
            throw new UnknownAccountException("User didn‘t existed!");
        }
        try {
            JWTUtil.verifyEx(token, username ,userBean.getUserPassword());
        }catch (Exception e) {
            if (e instanceof TokenExpiredException){
                Boolean tokenFlag = redisTemplate.hasKey("token:" + username);
                if (!tokenFlag){
                    throw new AuthenticationException("Authorization 过期");
                }
            }else{
                LOG.warn("Username  {} or password error",username);
                throw new AuthenticationException("token error");
            }
        }
        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }

}
 @ApiOperation(value = "查询所有person", notes = "", httpMethod = "GET")
    @RequestMapping(value = "/findAll",method = RequestMethod.GET)
    @RequiresAuthentication
    public ResponseBean findAll(HttpServletRequest request) throws InvalidNameException {
        String token = request.getHeader("Authorization");
        String userCode = JWTUtil.getUsername(token);
        if (userCode == null) {
            return new ResponseBean(ErrorCode.UNAUTHORIZED.getCode(), ErrorCode.UNAUTHORIZED.getMsg(),  null);
        }

        return  new ResponseBean(ErrorCode.SUCCESS.getCode(),ErrorCode.SUCCESS.getMsg(), ldapService.getAllPersonNames());

    }

 

查询ldap用户的方法如下:

  public Person findByUid(String uid)  {

        List<Person>  personList= ldapTemplate.search((LdapQuery) query().where("uid").is(uid),new PersonAttributesMapper());
        if (CollectionUtils.isEmpty(personList)){
            return null;
        }

        Person person=personList.get(0);

        return person;
    }
PersonAttributesMapper代码如下:

public class PersonAttributesMapper  implements AttributesMapper<Person> {


    @Override
    public Person mapFromAttributes(Attributes attrs) throws NamingException {
        Person person = new Person();
        person.setSuerName((String) attrs.get("sn").get());
        person.setCommonName((String) attrs.get("cn").get());
        person.setUid((String) attrs.get("uid").get());
        byte[] bytes= (byte[]) attrs.get("userPassword").get();

        person.setUserPassword(new String(bytes));

        return person;

    }
}

以上是关于ldap结合shiro搭建统一登录平台的主要内容,如果未能解决你的问题,请参考以下文章

Zabbix对接LDAP实现用户统一登录

Deepin系统基于LDAP统一认证

Zabbix 对接 LDAP 实现用户统一登录的方法

LDAP统一账户认证系统的密码自助修改

Grails Shiro LDAP用户/角色身份验证:如何/如何捕获和存储以供重用

Apache Shiro LDAP 配置(两步验证)